aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/app
diff options
context:
space:
mode:
author简律纯 <hsiangnianian@outlook.com>2023-04-18 03:02:17 +0800
committer简律纯 <hsiangnianian@outlook.com>2023-04-18 03:02:17 +0800
commit4919f028c884a041da7ff098abb02389b4eac598 (patch)
treeb0f482568c4b8c8a680ce6e2e70a7b7ca87dc190 /app
parentb135aac8531c1e1488147ad8c6f98eddbdbe0c99 (diff)
downloadHydroRoll-4919f028c884a041da7ff098abb02389b4eac598.tar.gz
HydroRoll-4919f028c884a041da7ff098abb02389b4eac598.zip
✨add envshare docs
Diffstat (limited to 'app')
-rw-r--r--app/[compositeKey]/page.tsx7
-rw-r--r--app/components/analytics.tsx22
-rw-r--r--app/components/error.tsx11
-rw-r--r--app/components/stats.tsx57
-rw-r--r--app/components/testimony.tsx125
-rw-r--r--app/components/title.tsx9
-rw-r--r--app/deploy/page.tsx89
-rw-r--r--app/globals.css15
-rw-r--r--app/head.tsx42
-rw-r--r--app/header.tsx60
-rw-r--r--app/layout.tsx58
-rw-r--r--app/page.tsx50
-rw-r--r--app/share/page.tsx227
-rw-r--r--app/unseal/page.tsx159
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">&rarr;</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">&rarr;</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>
- );
-}