diff options
Diffstat (limited to 'app')
| -rw-r--r-- | app/[compositeKey]/page.tsx | 7 | ||||
| -rw-r--r-- | app/components/analytics.tsx | 22 | ||||
| -rw-r--r-- | app/components/error.tsx | 11 | ||||
| -rw-r--r-- | app/components/stats.tsx | 57 | ||||
| -rw-r--r-- | app/components/testimony.tsx | 125 | ||||
| -rw-r--r-- | app/components/title.tsx | 9 | ||||
| -rw-r--r-- | app/deploy/page.tsx | 89 | ||||
| -rw-r--r-- | app/globals.css | 15 | ||||
| -rw-r--r-- | app/head.tsx | 42 | ||||
| -rw-r--r-- | app/header.tsx | 60 | ||||
| -rw-r--r-- | app/layout.tsx | 58 | ||||
| -rw-r--r-- | app/page.tsx | 50 | ||||
| -rw-r--r-- | app/share/page.tsx | 227 | ||||
| -rw-r--r-- | app/unseal/page.tsx | 159 |
14 files changed, 0 insertions, 931 deletions
diff --git a/app/[compositeKey]/page.tsx b/app/[compositeKey]/page.tsx deleted file mode 100644 index d91f182..0000000 --- a/app/[compositeKey]/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { redirect } from "next/navigation"; - -// This page is here for backwards compatibility with old links. -// Old links were of the form /{compositeKey} and now they are of the form /unseal#{compositeKey} -export default function Page(props: { params: { compositeKey: string } }) { - return redirect(`/unseal#${props.params.compositeKey}`); -} diff --git a/app/components/analytics.tsx b/app/components/analytics.tsx deleted file mode 100644 index ef6a2ae..0000000 --- a/app/components/analytics.tsx +++ /dev/null @@ -1,22 +0,0 @@ -"use client"; -import { Analytics as VercelAnalytics } from "@vercel/analytics/react"; - -const track = ["/", "/share", "/deploy", "/unseal"]; - -export function Analytics() { - return ( - <VercelAnalytics - beforeSend={(event) => { - const url = new URL(event.url); - if (!track.includes(url.pathname)) { - url.pathname = "/__redacted"; - return { - ...event, - url: url.href, - }; - } - return event; - }} - /> - ); -} diff --git a/app/components/error.tsx b/app/components/error.tsx deleted file mode 100644 index acf36d7..0000000 --- a/app/components/error.tsx +++ /dev/null @@ -1,11 +0,0 @@ -type Props = { - message: string; -}; - -export const ErrorMessage: React.FC<Props> = ({ message }) => { - return ( - <div className="flex items-center justify-center my-8 lg:my-16"> - <span className="px-4 py-2 text-red-500 border rounded border-red-500/50 bg-red-500/10"> {message}</span> - </div> - ); -}; diff --git a/app/components/stats.tsx b/app/components/stats.tsx deleted file mode 100644 index 31d74bc..0000000 --- a/app/components/stats.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Redis } from "@upstash/redis"; - -const redis = Redis.fromEnv(); -export const revalidate = 60; - -export const Stats = asyncComponent(async () => { - const [reads, writes] = await redis - .pipeline() - .get("envshare:metrics:reads") - .get("envshare:metrics:writes") - .exec<[number, number]>(); - const stars = await fetch("https://api.github.com/repos/chronark/envshare") - .then((res) => res.json()) - .then((json) => json.stargazers_count as number); - - const stats = [ - { - label: "Documents Encrypted", - value: writes, - }, - { - label: "Documents Decrypted", - value: reads, - }, - ] satisfies { label: string; value: number }[]; - - if (stars) { - stats.push({ - label: "GitHub Stars", - value: stars, - }); - } - - return ( - <section className="container mx-auto"> - <ul className="grid grid-cols-1 gap-4 sm:grid-cols-3 "> - {stats.map(({ label, value }) => ( - <li - key={label} - className="flex items-center justify-between gap-2 px-4 py-3 overflow-hidden rounded m sm:flex-col" - > - <dd className="text-2xl font-bold tracking-tight text-center sm:text-5xl text-zinc-200"> - {Intl.NumberFormat("en-US", { notation: "compact" }).format(value)} - </dd> - <dt className="leading-6 text-center text-zinc-500">{label}</dt> - </li> - ))} - </ul> - </section> - ); -}); - -// stupid hack to make "server components" actually work with components -// https://www.youtube.com/watch?v=h_9Vx6kio2s -function asyncComponent<T, R>(fn: (arg: T) => Promise<R>): (arg: T) => R { - return fn as (arg: T) => R; -} diff --git a/app/components/testimony.tsx b/app/components/testimony.tsx deleted file mode 100644 index 757a953..0000000 --- a/app/components/testimony.tsx +++ /dev/null @@ -1,125 +0,0 @@ -"use client"; -import Image from "next/image"; -import Link from "next/link"; -import { Props } from "next/script"; -import React, { PropsWithChildren } from "react"; - -const TwitterHandle: React.FC<PropsWithChildren> = ({ children }) => { - return <span className="text-blue-500">{children}</span>; -}; - -const Author: React.FC<PropsWithChildren<{ href: string }>> = ({ children, href }) => ( - <Link target="_blank" rel="noopener noreferrer" href={href} className="duration-150 text-zinc-200 hover:text-zinc-50"> - {children} - </Link> -); - -const Title: React.FC<PropsWithChildren<{ href: string }>> = ({ children, href }) => ( - <Link - target="_blank" - rel="noopener noreferrer" - href={href} - className="text-sm duration-150 text-zinc-500 hover:text-zinc-300" - > - {children} - </Link> -); - -export const Testimonials = () => { - const posts: { - content: React.ReactNode; - link: string; - author: { - name: React.ReactNode; - title?: React.ReactNode; - image: string; - }; - }[] = [ - { - content: ( - <div> - <p> - My cursory audit of <TwitterHandle>@chronark_</TwitterHandle>'s envshare: - </p> - <p> - It is light, extremely functional, and does its symmetric block cipher correctly, unique initialization - vectors, decryption keys derived securely. - </p> - <br /> - <p>Easily modified to remove minimal analytics. Superior to Privnote.</p> - <br /> - <p>Self-hosting is easy. 👏</p> - </div> - ), - link: "https://twitter.com/FrederikMarkor/status/1615299856205250560", - author: { - name: <Author href="https://twitter.com/FrederikMarkor">Frederik Markor</Author>, - title: <Title href="https://discreet.net">CEO @discreet</Title>, - image: "https://pbs.twimg.com/profile_images/1438061314010664962/NecuMIGR_400x400.jpg", - }, - }, - { - content: ( - <div> - <p>I'm particularly chuffed about this launch, for a couple of reasons:</p> - <ul> - <li> - ◆ Built on <TwitterHandle>@nextjs</TwitterHandle> + <TwitterHandle>@upstash</TwitterHandle>, hosted on{" "} - <TwitterHandle>@vercel</TwitterHandle> - </li> - <li>◆ 100% free to use & open source</li> - <li>◆ One-click deploy via Vercel + Upstash integration</li> - </ul> - <p>Deploy your own → http://vercel.fyi/envshare</p> - </div> - ), - link: "https://twitter.com/steventey/status/1615035241772482567?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1615035241772482567%7Ctwgr%5E1db44bb10c690189e24c980fcd787299961c34c6%7Ctwcon%5Es1_&ref_url=https%3A%2F%2Fpublish.twitter.com%2F%3Fquery%3Dhttps3A2F2Ftwitter.com2Fsteventey2Fstatus2F1615035241772482567widget%3DTweet", - author: { - name: <Author href="https://twitter.com/steventey">Steven Tey</Author>, - title: <Title href="https://vercel.com">Senior Developer Advocate at Vercel</Title>, - image: "https://pbs.twimg.com/profile_images/1506792347840888834/dS-r50Je_400x400.jpg", - }, - }, - { - content: ( - <div> - <p> - Congratulations on the launch <TwitterHandle>@chronark_</TwitterHandle>👏! This is such a valuable product - for developers. Icing on the cake is that it's open source! ✨ - </p> - </div> - ), - link: "https://twitter.com/DesignSiddharth/status/1615293209164546048", - author: { - name: <Author href="https://twitter.com/DesignSiddharth">@DesignSiddharth</Author>, - image: "https://pbs.twimg.com/profile_images/1613772710009765888/MbSblJYf_400x400.jpg", - }, - }, - ]; - - return ( - <section className="container mx-auto"> - <ul role="list" className="grid max-w-2xl grid-cols-1 gap-16 mx-auto sm:gap-8 lg:max-w-none lg:grid-cols-3"> - {posts.map((post, i) => ( - <div - key={i} - className="flex flex-col justify-between duration-150 border rounded border-zinc-500/30 hover:border-zinc-300/30 hover:bg-zinc-900/30 group" - > - <Link href={post.link} className="whitespace-pre-line text text-zinc-500 p-6"> - {post.content} - </Link> - <div className="relative flex items-start justify-between p-6 duration-150 border-t bg-zinc-900/40 border-zinc-500/30 group-hover:border-zinc-300/30"> - <div> - <div className="text-base font-display text-zinc-200">{post.author.name}</div> - <div className="mt-1 text-sm text-zinc-500">{post.author.title}</div> - </div> - <div className="overflow-hidden rounded-full bg-zinc-50"> - <Image className="object-cover h-14 w-14" src={post.author.image} alt="" width={56} height={56} /> - </div> - </div> - </div> - ))} - </ul> - </section> - ); -}; diff --git a/app/components/title.tsx b/app/components/title.tsx deleted file mode 100644 index b9b7f3c..0000000 --- a/app/components/title.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React, { PropsWithChildren } from "react"; - -export const Title: React.FC<PropsWithChildren> = ({ children }): JSX.Element => { - return ( - <h1 className="py-4 text-5xl font-bold text-center text-transparent bg-gradient-to-t bg-clip-text from-zinc-100/60 to-white"> - {children} - </h1> - ); -}; diff --git a/app/deploy/page.tsx b/app/deploy/page.tsx deleted file mode 100644 index b515144..0000000 --- a/app/deploy/page.tsx +++ /dev/null @@ -1,89 +0,0 @@ -"use client"; -import { ArrowTopRightOnSquareIcon } from "@heroicons/react/20/solid"; -import Link from "next/link"; -import { Title } from "@components/title"; -import React from "react"; -const steps: { - name: string; - description: string | React.ReactNode; - cta?: React.ReactNode; -}[] = [ - { - name: "Create a new Redis database on Upstash", - description: ( - <> - Upstash offers a serverless Redis database with a generous free tier of up to 10,000 requests per day. That's - more than enough. - <br /> - Click the button below to sign up and create a new Redis database on Upstash. - </> - ), - cta: ( - <Link - href="https://console.upstash.com/redis" - className="flex items-center justify-center w-full gap-2 px-4 py-2 text-sm text-center transition-all duration-150 rounded text-zinc-800 hover:text-zinc-100 bg-zinc-200 hover:bg-transparent ring-1 ring-zinc-100" - > - <span>Create Database</span> - <ArrowTopRightOnSquareIcon className="w-4 h-4" /> - </Link> - ), - }, - { - name: "Copy the REST connection credentials", - description: ( - <p> - After creating the database, scroll to the bottom and make a note of <code>UPSTASH_REDIS_REST_URL</code> and{" "} - <code>UPSTASH_REDIS_REST_TOKEN</code>, you need them in the next step - </p> - ), - }, - { - name: "Deploy to Vercel", - description: "Deploy the app to Vercel and paste the connection credentials into the environment variables.", - cta: ( - <Link - href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fchronark%2Fenvshare&env=UPSTASH_REDIS_REST_URL,UPSTASH_REDIS_REST_TOKEN&demo-title=Share%20Environment%20Variables%20Securely&demo-url=https%3A%2F%2Fcryptic.vercel.app" - className="flex items-center justify-center w-full gap-2 px-4 py-2 text-sm text-center transition-all duration-150 rounded text-zinc-800 hover:text-zinc-100 bg-zinc-200 hover:bg-transparent ring-1 ring-zinc-100" - > - <span>Deploy</span> - <ArrowTopRightOnSquareIcon className="w-4 h-4" /> - </Link> - ), - }, -]; - -export default function Deploy() { - return ( - <div className="container px-8 mx-auto mt-16 lg:mt-32 "> - <Title>Deploy EnvShare for Free</Title> - <p className="mt-4 text-sm text-center text-zinc-600"> - You can deploy your own hosted version of EnvShare, you just need an Upstash and Vercel account. - </p> - <ol className="flex flex-col items-center justify-center mt-8 md:mt-16 xl:mt-24"> - {steps.map((step, stepIdx) => ( - <li key={step.name} className="relative flex flex-col items-center gap-4 pb-16 group md:gap-8 md:pb-24"> - <span - className="absolute top-4 h-full w-0.5 bg-gradient-to-b from-blue-500/60 via-blue-500/10 to-transparent" - aria-hidden="true" - /> - <span className="flex items-center h-9" aria-hidden="true"> - <span className="relative z-10 flex items-center justify-center w-8 h-8 text-sm text-blue-400 duration-150 border border-blue-400 rounded-full bg-zinc-900 group-hover:border-blue-500 drop-shadow-blue"> - {stepIdx + 1} - </span> - </span> - <div className="z-10 flex flex-col items-center"> - <h2 className="text-xl font-medium duration-150 lg:text-2xl text-zinc-200 group-hover:text-white"> - {step.name} - </h2> - - <div className="mt-4 text-sm text-center duration-1000 text-zinc-500 group-hover:text-zinc-400"> - {step.description} - </div> - <div className="w-full mt-8 md:w-auto">{step.cta}</div> - </div> - </li> - ))} - </ol> - </div> - ); -} diff --git a/app/globals.css b/app/globals.css deleted file mode 100644 index b764a11..0000000 --- a/app/globals.css +++ /dev/null @@ -1,15 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer utilities { - input[type="number"]::-webkit-inner-spin-button, - input[type="number"]::-webkit-outer-spin-button { - @apply appearance-none; - } - - - input[type="file"] { - @apply appearance-none; - } -}
\ No newline at end of file diff --git a/app/head.tsx b/app/head.tsx deleted file mode 100644 index aecaa44..0000000 --- a/app/head.tsx +++ /dev/null @@ -1,42 +0,0 @@ -export default function Head({ title, subtitle }: { title: string; subtitle: string }) { - // Fallback tagline - title ??= "Share Environment Variables Securely"; - subtitle ??= "EnvShare"; - - const baseUrl = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3000"; - - const url = new URL("/api/v1/og", baseUrl); - url.searchParams.set("title", title); - url.searchParams.set("subtitle", subtitle); - - return ( - <> - <title>EnvShare</title> - <meta content="width=device-width, initial-scale=1" name="viewport" /> - <meta name="description" content={subtitle} /> - <meta name="theme-color" content="#000000" /> - <meta name="title" content={title} /> - <meta name="keywords" content="envshare, secure, secrets, share, environment, variables" /> - <meta name="language" content="English" /> - <meta name="revisit-after" content="7 days" /> - <meta name="robots" content="all" /> - <meta httpEquiv="Content-Type" content="text/html; charset=utf-8" /> - - {/* Open Graph / Facebook */} - <meta property="og:type" content="website" /> - <meta property="og:url" content={baseUrl} /> - <meta property='og:image' content={url.toString()} /> - <meta property='og:title' content={title} /> - <meta property='og:description' content={subtitle} /> - <meta property="og:image:width" content="1200" /> - <meta property="og:image:height" content="630" /> - - {/* Twitter */} - <meta property="twitter:card" content="summary_large_image" /> - <meta property="twitter:url" content={baseUrl} /> - <meta property="twitter:title" content={title} /> - <meta property="twitter:description" content={subtitle} /> - <meta property="twitter:image" content={url.toString()} /> - </> - ); -} diff --git a/app/header.tsx b/app/header.tsx deleted file mode 100644 index 872459a..0000000 --- a/app/header.tsx +++ /dev/null @@ -1,60 +0,0 @@ -"use client"; -import React from "react"; -import Link from "next/link"; -import { usePathname } from "next/navigation"; - -const navigation = [ - { - name: "Share", - href: "/share", - }, - { - name: "Unseal", - href: "/unseal", - }, - - { - name: "Deploy", - href: "/deploy", - }, - { - name: "GitHub", - href: "https://github.com/chronark/envshare", - external: true, - }, -] satisfies { name: string; href: string; external?: boolean }[]; - -export const Header: React.FC = () => { - const pathname = usePathname(); - return ( - <header className="top-0 z-30 w-full px-4 sm:fixed backdrop-blur bh-zinc-900/50"> - <div className="container mx-auto"> - <div className="flex flex-col items-center justify-between gap-2 pt-6 sm:h-20 sm:flex-row sm:pt-0"> - <Link href="/" className="text-2xl font-semibold duration-150 text-zinc-100 hover:text-white"> - EnvShare - </Link> - {/* Desktop navigation */} - <nav className="flex items-center grow"> - <ul className="flex flex-wrap items-center justify-end gap-4 grow"> - {navigation.map((item) => ( - <li className="" key={item.href}> - <Link - className={`flex items-center px-3 py-2 duration-150 text-sm sm:text-base hover:text-zinc-50 - ${pathname === item.href ? "text-zinc-200" : "text-zinc-400"}`} - href={item.href} - target={item.external ? "_blank" : undefined} - rel={item.external ? "noopener noreferrer" : undefined} - > - {item.name} - </Link> - </li> - ))} - </ul> - </nav> - </div> - </div> - - {/* Fancy fading bottom border */} - </header> - ); -}; diff --git a/app/layout.tsx b/app/layout.tsx deleted file mode 100644 index 557407d..0000000 --- a/app/layout.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import "./globals.css"; -import { Inter } from "@next/font/google"; -import Link from "next/link"; -import { Header } from "./header"; - -import { Analytics } from "@components/analytics"; -const inter = Inter({ subsets: ["latin"], variable: "--font-inter" }); - -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { - return ( - <html lang="en" className={inter.variable}> - <head /> - <body className="relative min-h-screen bg-black bg-gradient-to-tr from-zinc-900/50 to-zinc-700/30"> - { - // Not everyone will want to host envshare on Vercel, so it makes sense to make this opt-in. - process.env.ENABLE_VERCEL_ANALYTICS ? <Analytics /> : null - } - - <Header /> - - <main className=" min-h-[80vh] ">{children}</main> - - <footer className="bottom-0 border-t inset-2x-0 border-zinc-500/10"> - <div className="flex flex-col gap-1 px-6 py-12 mx-auto text-xs text-center text-zinc-700 max-w-7xl lg:px-8"> - <p> - Built by{" "} - <Link href="https://twitter.com/chronark_" className="font-semibold duration-150 hover:text-zinc-200"> - @chronark_ - </Link> - and{" "} - <Link - href="https://github.com/chronark/envshare/graphs/contributors" - className="underline duration-150 hover:text-zinc-200" - > - many others{" "} - </Link> - </p> - <p> - EnvShare is deployed on{" "} - <Link target="_blank" href="https://vercel.com" className="underline duration-150 hover:text-zinc-200"> - Vercel - </Link>{" "} - and uses{" "} - <Link target="_blank" href="https://upstash.com" className="underline duration-150 hover:text-zinc-200"> - Upstash - </Link>{" "} - for storing encrypted data. - </p> - </div> - </footer> - </body> - </html> - ); -} diff --git a/app/page.tsx b/app/page.tsx deleted file mode 100644 index ba659ca..0000000 --- a/app/page.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import Link from "next/link"; -import { Stats } from "./components/stats"; -import { Testimonials } from "./components/testimony"; - -export default function Home() { - return ( - <div className="flex flex-col gap-8 pb-8 md:gap-16 md:pb-16 xl:pb-24"> - <div className="flex flex-col items-center justify-center max-w-3xl px-8 mx-auto mt-8 sm:min-h-screen sm:mt-0 sm:px-0"> - <div className="hidden sm:mb-8 sm:flex sm:justify-center"> - <Link - href="https://github.com/chronark/envshare" - className="text-zinc-400 relative overflow-hidden rounded-full py-1.5 px-4 text-sm leading-6 ring-1 ring-zinc-100/10 hover:ring-zinc-100/30 duration-150" - > - EnvShare is Open Source on{" "} - <span className="font-semibold text-zinc-200"> - GitHub <span aria-hidden="true">→</span> - </span> - </Link> - </div> - <div> - <h1 className="py-4 text-5xl font-bold tracking-tight text-center text-transparent bg-gradient-to-t bg-clip-text from-zinc-100/50 to-white sm:text-7xl"> - Share Environment Variables Securely - </h1> - <p className="mt-6 leading-5 text-zinc-600 sm:text-center"> - Your document is encrypted in your browser before being stored for a limited period of time and read - operations. Unencrypted data never leaves your browser. - </p> - <div className="flex flex-col justify-center gap-4 mx-auto mt-8 sm:flex-row sm:max-w-lg "> - <Link - href="/deploy" - className="sm:w-1/2 sm:text-center inline-block space-x-2 rounded px-4 py-1.5 md:py-2 text-base font-semibold leading-7 text-white ring-1 ring-zinc-600 hover:bg-white hover:text-zinc-900 duration-150 hover:ring-white hover:drop-shadow-cta" - > - Deploy - </Link> - <Link - href="/share" - className="sm:w-1/2 sm:text-center inline-block transition-all space-x-2 rounded px-4 py-1.5 md:py-2 text-base font-semibold leading-7 text-zinc-800 bg-zinc-50 ring-1 ring-transparent hover:text-zinc-100 hover:ring-zinc-600/80 hover:bg-zinc-900/20 duration-150 hover:drop-shadow-cta" - > - <span>Share</span> - <span aria-hidden="true">→</span> - </Link> - </div> - </div> - </div> - <h2 className="py-4 text-3xl font-bold text-center text-zinc-300 ">Used and trusted by a growing community</h2> - <Stats /> - <Testimonials /> - </div> - ); -} diff --git a/app/share/page.tsx b/app/share/page.tsx deleted file mode 100644 index b6089f0..0000000 --- a/app/share/page.tsx +++ /dev/null @@ -1,227 +0,0 @@ -"use client"; -import { toBase58 } from "util/base58"; -import { useState, Fragment } from "react"; -import { Cog6ToothIcon, ClipboardDocumentIcon, ClipboardDocumentCheckIcon } from "@heroicons/react/24/outline"; -import { Title } from "@components/title"; -import { encrypt } from "pkg/encryption"; -import { ErrorMessage } from "@components/error"; -import { encodeCompositeKey } from "pkg/encoding"; -import { LATEST_KEY_VERSION } from "pkg/constants"; - -export default function Home() { - const [text, setText] = useState(""); - const [reads, setReads] = useState(999); - - const [ttl, setTtl] = useState(7); - const [ttlMultiplier, setTtlMultiplier] = useState(60 * 60 * 24); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(""); - const [copied, setCopied] = useState(false); - - const [link, setLink] = useState(""); - - const onSubmit = async () => { - try { - setError(""); - setLink(""); - setLoading(true); - - const { encrypted, iv, key } = await encrypt(text); - - const { id } = (await fetch("/api/v1/store", { - method: "POST", - body: JSON.stringify({ - ttl: ttl * ttlMultiplier, - reads, - encrypted: toBase58(encrypted), - iv: toBase58(iv), - }), - }).then((r) => r.json())) as { id: string }; - - const compositeKey = encodeCompositeKey(LATEST_KEY_VERSION, id, key); - - const url = new URL(window.location.href); - url.pathname = "/unseal"; - url.hash = compositeKey; - setCopied(false); - setLink(url.toString()); - } catch (e) { - console.error(e); - setError((e as Error).message); - } finally { - setLoading(false); - } - }; - - return ( - <div className="container px-8 mx-auto mt-16 lg:mt-32 "> - {error ? <ErrorMessage message={error} /> : null} - - {link ? ( - <div className="flex flex-col items-center justify-center w-full h-full mt-8 md:mt-16 xl:mt-32"> - <Title>Share this link with others</Title> - <div className="relative flex items-stretch flex-grow mt-16 focus-within:z-10"> - <pre className="px-4 py-3 font-mono text-center bg-transparent border rounded border-zinc-600 focus:border-zinc-100/80 focus:ring-0 sm:text-sm text-zinc-100"> - {link} - </pre> - <button - type="button" - className="relative inline-flex items-center px-4 py-2 -ml-px space-x-2 text-sm font-medium duration-150 border text-zinc-700 border-zinc-300 rounded-r-md bg-zinc-50 hover focus:border-zinc-500 focus:outline-none focus:ring-1 focus:ring-zinc-500 hover:text-zinc-900 hover:bg-white" - onClick={() => { - navigator.clipboard.writeText(link); - setCopied(true); - }} - > - {copied ? ( - <ClipboardDocumentCheckIcon className="w-5 h-5" aria-hidden="true" /> - ) : ( - <ClipboardDocumentIcon className="w-5 h-5" aria-hidden="true" /> - )}{" "} - <span>{copied ? "Copied" : "Copy"}</span> - </button> - </div> - </div> - ) : ( - <form - className="max-w-3xl mx-auto" - onSubmit={(e) => { - e.preventDefault(); - if (text.length <= 0) return; - onSubmit(); - }} - > - <Title>Encrypt and Share</Title> - - <pre className="px-4 py-3 mt-8 font-mono text-left bg-transparent border rounded border-zinc-600 focus:border-zinc-100/80 focus:ring-0 sm:text-sm text-zinc-100"> - <div className="flex items-start px-1 text-sm"> - <div aria-hidden="true" className="pr-4 font-mono border-r select-none border-zinc-300/5 text-zinc-700"> - {Array.from({ - length: text.split("\n").length, - }).map((_, index) => ( - <Fragment key={index}> - {(index + 1).toString().padStart(2, "0")} - <br /> - </Fragment> - ))} - </div> - - <textarea - id="text" - name="text" - value={text} - minLength={1} - onChange={(e) => setText(e.target.value)} - rows={Math.max(5, text.split("\n").length)} - placeholder="DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres" - className="w-full p-0 text-base bg-transparent border-0 appearance-none resize-none hover:resize text-zinc-100 placeholder-zinc-500 focus:ring-0 sm:text-sm" - /> - </div> - </pre> - - <div className="flex flex-col items-center justify-center w-full gap-4 mt-4 sm:flex-row"> - <div className="w-full sm:w-1/5"> - <label - className="flex items-center justify-center h-16 px-3 py-2 text-sm whitespace-no-wrap duration-150 border rounded hover:border-zinc-100/80 border-zinc-600 focus:border-zinc-100/80 focus:ring-0 text-zinc-100 hover:text-white hover:cursor-pointer " - htmlFor="file_input" - > - Upload a file - </label> - <input - className="hidden" - id="file_input" - type="file" - onChange={(e) => { - const file = e.target.files![0]; - if (file.size > 1024 * 16) { - setError("File size must be less than 16kb"); - return; - } - - const reader = new FileReader(); - reader.onload = (e) => { - const t = e.target!.result as string; - setText(t); - }; - reader.readAsText(file); - }} - /> - </div> - - <div className="w-full h-16 px-3 py-2 duration-150 border rounded sm:w-2/5 hover:border-zinc-100/80 border-zinc-600 focus-within:border-zinc-100/80 focus-within:ring-0 "> - <label htmlFor="reads" className="block text-xs font-medium text-zinc-100"> - READS - </label> - <input - type="number" - name="reads" - id="reads" - className="w-full p-0 text-base bg-transparent border-0 appearance-none text-zinc-100 placeholder-zinc-500 focus:ring-0 sm:text-sm" - value={reads} - onChange={(e) => setReads(e.target.valueAsNumber)} - /> - </div> - <div className="relative w-full h-16 px-3 py-2 duration-150 border rounded sm:w-2/5 hover:border-zinc-100/80 border-zinc-600 focus-within:border-zinc-100/80 focus-within:ring-0 "> - <label htmlFor="reads" className="block text-xs font-medium text-zinc-100"> - TTL - </label> - <input - type="number" - name="reads" - id="reads" - className="w-full p-0 text-base bg-transparent border-0 appearance-none text-zinc-100 placeholder-zinc-500 focus:ring-0 sm:text-sm" - value={ttl} - onChange={(e) => setTtl(e.target.valueAsNumber)} - /> - <div className="absolute inset-y-0 right-0 flex items-center"> - <label htmlFor="ttlMultiplier" className="sr-only" /> - <select - id="ttlMultiplier" - name="ttlMultiplier" - className="h-full py-0 pl-2 bg-transparent border-0 border-transparent rounded pr-7 text-zinc-500 focus:ring-0 sm:text-sm" - onChange={(e) => setTtlMultiplier(parseInt(e.target.value))} - defaultValue={60 * 60 * 24} - > - <option value={60}>{ttl === 1 ? "Minute" : "Minutes"}</option> - <option value={60 * 60}>{ttl === 1 ? "Hour" : "Hours"}</option> - <option value={60 * 60 * 24}>{ttl === 1 ? "Day" : "Days"}</option> - </select> - </div> - </div> - </div> - <button - type="submit" - disabled={loading || text.length <= 0} - className={`mt-6 w-full h-12 inline-flex justify-center items-center transition-all rounded px-4 py-1.5 md:py-2 text-base font-semibold leading-7 bg-zinc-200 ring-1 ring-transparent duration-150 ${ - text.length <= 0 - ? "text-zinc-400 cursor-not-allowed" - : "text-zinc-900 hover:text-zinc-100 hover:ring-zinc-600/80 hover:bg-zinc-900/20" - } ${loading ? "animate-pulse" : ""}`} - > - <span>{loading ? <Cog6ToothIcon className="w-5 h-5 animate-spin" /> : "Share"}</span> - </button> - - <div className="mt-8"> - <ul className="space-y-2 text-xs text-zinc-500"> - <li> - <p> - <span className="font-semibold text-zinc-400">Reads:</span> The number of reads determines how often - the data can be shared, before it deletes itself. 0 means unlimited. - </p> - </li> - <li> - <p> - <span className="font-semibold text-zinc-400">TTL:</span> You can add a TTL (time to live) to the - data, to automatically delete it after a certain amount of time. 0 means no TTL. - </p> - </li> - <p> - Clicking Share will generate a new symmetrical key and encrypt your data before sending only the - encrypted data to the server. - </p> - </ul> - </div> - </form> - )} - </div> - ); -} diff --git a/app/unseal/page.tsx b/app/unseal/page.tsx deleted file mode 100644 index 0b63e6b..0000000 --- a/app/unseal/page.tsx +++ /dev/null @@ -1,159 +0,0 @@ -"use client"; -import React, { Fragment, useState, useEffect } from "react"; -import { ClipboardDocumentCheckIcon, ClipboardDocumentIcon, Cog6ToothIcon } from "@heroicons/react/24/outline"; - -import { Title } from "@components/title"; - -import { decodeCompositeKey } from "pkg/encoding"; -import { decrypt } from "pkg/encryption"; -import Link from "next/link"; -import { ErrorMessage } from "@components/error"; - -export default function Unseal() { - const [compositeKey, setCompositeKey] = useState<string>(""); - useEffect(() => { - if (typeof window !== "undefined") { - setCompositeKey(window.location.hash.replace(/^#/, "")); - } - }, []); - - const [text, setText] = useState<string | null>(null); - const [loading, setLoading] = useState(false); - const [remainingReads, setRemainingReads] = useState<number | null>(null); - const [error, setError] = useState<string | null>(null); - const [copied, setCopied] = useState(false); - - const onSubmit = async () => { - try { - setError(null); - setText(null); - setLoading(true); - - if (!compositeKey) { - throw new Error("No id provided"); - } - - const { id, encryptionKey, version } = decodeCompositeKey(compositeKey); - const res = await fetch(`/api/v1/load?id=${id}`); - if (!res.ok) { - throw new Error(await res.text()); - } - const json = (await res.json()) as { - iv: string; - encrypted: string; - remainingReads: number | null; - }; - setRemainingReads(json.remainingReads); - - const decrypted = await decrypt(json.encrypted, encryptionKey, json.iv, version); - - setText(decrypted); - } catch (e) { - console.error(e); - setError((e as Error).message); - } finally { - setLoading(false); - } - }; - - return ( - <div className="container px-8 mx-auto mt-16 lg:mt-32 "> - {error ? <ErrorMessage message={error} /> : null} - {text ? ( - <div className="max-w-4xl mx-auto"> - {remainingReads !== null ? ( - <div className="text-sm text-center text-zinc-600"> - {remainingReads > 0 ? ( - <p> - This document can be read <span className="text-zinc-100">{remainingReads}</span> more times. - </p> - ) : ( - <p className="text-zinc-400"> - This was the last time this document could be read. It was deleted from storage. - </p> - )} - </div> - ) : null} - <pre className="px-4 py-3 mt-8 font-mono text-left bg-transparent border rounded border-zinc-600 focus:border-zinc-100/80 focus:ring-0 sm:text-sm text-zinc-100"> - <div className="flex items-start px-1 text-sm"> - <div aria-hidden="true" className="pr-4 font-mono border-r select-none border-zinc-300/5 text-zinc-700"> - {Array.from({ - length: text.split("\n").length, - }).map((_, index) => ( - <Fragment key={index}> - {(index + 1).toString().padStart(2, "0")} - <br /> - </Fragment> - ))} - </div> - <div> - <pre className="flex overflow-x-auto"> - <code className="px-4 text-left">{text}</code> - </pre> - </div> - </div> - </pre> - - <div className="flex items-center justify-end gap-4 mt-4"> - <Link - href="/share" - type="button" - className="relative inline-flex items-center px-4 py-2 -ml-px space-x-2 text-sm font-medium duration-150 border rounded text-zinc-300 border-zinc-300/40 hover:border-zinc-300 focus:outline-none hover:text-white" - > - Share another - </Link> - <button - type="button" - className="relative inline-flex items-center px-4 py-2 -ml-px space-x-2 text-sm font-medium duration-150 border rounded text-zinc-700 border-zinc-300 bg-zinc-50 hover focus:border-zinc-500 focus:outline-none hover:text-zinc-50 hover:bg-zinc-900" - onClick={() => { - navigator.clipboard.writeText(text); - setCopied(true); - }} - > - {copied ? ( - <ClipboardDocumentCheckIcon className="w-5 h-5" aria-hidden="true" /> - ) : ( - <ClipboardDocumentIcon className="w-5 h-5" aria-hidden="true" /> - )}{" "} - <span>{copied ? "Copied" : "Copy"}</span> - </button> - </div> - </div> - ) : ( - <form - className="max-w-3xl mx-auto " - onSubmit={(e) => { - e.preventDefault(); - onSubmit(); - }} - > - <Title>Decrypt a document</Title> - - <div className="px-3 py-2 mt-8 border rounded border-zinc-600 focus-within:border-zinc-100/80 focus-within:ring-0 "> - <label htmlFor="id" className="block text-xs font-medium text-zinc-100"> - ID - </label> - <input - type="text" - name="compositeKey" - id="compositeKey" - className="w-full p-0 text-base bg-transparent border-0 appearance-none text-zinc-100 placeholder-zinc-500 focus:ring-0 sm:text-sm" - value={compositeKey} - onChange={(e) => setCompositeKey(e.target.value)} - /> - </div> - - <button - type="submit" - disabled={loading} - className={`mt-8 w-full h-12 inline-flex justify-center items-center transition-all rounded px-4 py-1.5 md:py-2 text-base font-semibold leading-7 text-zinc-800 bg-zinc-200 ring-1 duration-150 hover:text-black hover:drop-shadow-cta hover:bg-white ${ - loading ? "animate-pulse" : "" - }`} - > - <span>{loading ? <Cog6ToothIcon className="w-5 h-5 animate-spin" /> : "Unseal"}</span> - </button> - </form> - )} - </div> - ); -} |
