From 4919f028c884a041da7ff098abb02389b4eac598 Mon Sep 17 00:00:00 2001 From: 简律纯 Date: Tue, 18 Apr 2023 03:02:17 +0800 Subject: ✨add envshare docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- envshare/pages/api/v1/load.ts | 36 +++++++++++++ envshare/pages/api/v1/og.tsx | 64 +++++++++++++++++++++++ envshare/pages/api/v1/secret/[id].ts | 58 +++++++++++++++++++++ envshare/pages/api/v1/secret/index.ts | 96 +++++++++++++++++++++++++++++++++++ envshare/pages/api/v1/store.ts | 38 ++++++++++++++ 5 files changed, 292 insertions(+) create mode 100644 envshare/pages/api/v1/load.ts create mode 100644 envshare/pages/api/v1/og.tsx create mode 100644 envshare/pages/api/v1/secret/[id].ts create mode 100644 envshare/pages/api/v1/secret/index.ts create mode 100644 envshare/pages/api/v1/store.ts (limited to 'envshare/pages') diff --git a/envshare/pages/api/v1/load.ts b/envshare/pages/api/v1/load.ts new file mode 100644 index 0000000..ddbfac0 --- /dev/null +++ b/envshare/pages/api/v1/load.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from "next/server"; +import { Redis } from "@upstash/redis"; + +const redis = Redis.fromEnv(); +export default async function handler(req: NextRequest) { + const url = new URL(req.url); + const id = url.searchParams.get("id"); + if (!id) { + return new NextResponse("id param is missing", { status: 400 }); + } + const key = ["envshare", id].join(":"); + + const [data, _] = await Promise.all([ + await redis.hgetall<{ encrypted: string; remainingReads: number | null; iv: string }>(key), + await redis.incr("envshare:metrics:reads"), + ]); + if (!data) { + return new NextResponse("Not Found", { status: 404 }); + } + if (data.remainingReads !== null && data.remainingReads < 1) { + await redis.del(key); + return new NextResponse("Not Found", { status: 404 }); + } + + let remainingReads: number | null = null; + if (data.remainingReads !== null) { + // Decrement the number of reads and return the remaining reads + remainingReads = await redis.hincrby(key, "remainingReads", -1); + } + + return NextResponse.json({ iv: data.iv, encrypted: data.encrypted, remainingReads }); +} + +export const config = { + runtime: "edge", +}; diff --git a/envshare/pages/api/v1/og.tsx b/envshare/pages/api/v1/og.tsx new file mode 100644 index 0000000..dad6531 --- /dev/null +++ b/envshare/pages/api/v1/og.tsx @@ -0,0 +1,64 @@ +import { ImageResponse } from "@vercel/og"; +import { NextRequest } from "next/server"; + +export const config = { + runtime: "edge", +}; + +export default async function handler(req: NextRequest) { + try { + const { searchParams } = new URL(req.url); + // Redundant fallback alternate tagline + const title = searchParams.get("title") ?? "Share Environment Variables Securely"; + const subtitle = searchParams.get("subtitle") ?? "EnvShare"; + + const inter = await fetch(new URL("../../../public/fonts/Inter-SemiBold.ttf", import.meta.url)).then((res) => + res.arrayBuffer(), + ); + + // TODO: Fix tailwind classes on this route + return new ImageResponse( +
+ {/* backgroundImage: bg-gradient-to-tr from-zinc-900/50 to-zinc-700/30 */} +
+
+ {/* font-semibold bg-gradient-to-t bg-clip-text from-zinc-100/50 to-white whitespace-pre */} +

+ {title} +

+

{subtitle}

+
+
+
, + { + height: 630, + width: 1200, + emoji: "twemoji", + fonts: [ + { + name: "Inter", + data: inter, + style: "normal", + }, + ], + }, + ); + } catch (e) { + console.log(`${(e as Error).message}`); + return new Response("Failed to generate the image", { + status: 500, + }); + } +} diff --git a/envshare/pages/api/v1/secret/[id].ts b/envshare/pages/api/v1/secret/[id].ts new file mode 100644 index 0000000..8b5f082 --- /dev/null +++ b/envshare/pages/api/v1/secret/[id].ts @@ -0,0 +1,58 @@ +import { NextRequest, NextResponse } from "next/server"; +import { Redis } from "@upstash/redis"; +import { z } from "zod"; + +const responseValidation = z.union([ + z.object({ + data: z.object({ + remainingReads: z.number().int().optional(), + secret: z.string(), + }), + }), + z.object({ + error: z.string(), + }), +]); + +const redis = Redis.fromEnv(); +export default async function handler(req: NextRequest): Promise { + try { + if (req.method !== "GET") { + return NextResponse.json({ error: "Method Not Allowed" }, { status: 405 }); + } + const id = new URL(req.url).searchParams.get("id"); + if (!id) { + return NextResponse.json({ error: "Missing `id` parameter" }, { status: 400 }); + } + + const redisKey = ["envshare", id].join(":"); + + const [data, _] = await Promise.all([ + await redis.hgetall<{ secret: string; remainingReads: number | null }>(redisKey), + await redis.incr("envshare:metrics:reads"), + ]); + + if (!data) { + return NextResponse.json({ error: "Not Found" }, { status: 404 }); + } + if (data.remainingReads !== null && data.remainingReads < 1) { + await redis.del(redisKey); + return NextResponse.json({ error: "Not Found" }, { status: 404 }); + } + + let remainingReads: number | null = null; + if (data.remainingReads !== null) { + // Decrement the number of reads and return the remaining reads + remainingReads = await redis.hincrby(redisKey, "remainingReads", -1); + } + + return NextResponse.json({ data: { secret: data.secret, remainingReads: remainingReads ?? undefined } }); + } catch (e) { + console.error(e); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); + } +} + +export const config = { + runtime: "edge", +}; diff --git a/envshare/pages/api/v1/secret/index.ts b/envshare/pages/api/v1/secret/index.ts new file mode 100644 index 0000000..423e7a0 --- /dev/null +++ b/envshare/pages/api/v1/secret/index.ts @@ -0,0 +1,96 @@ +import { NextRequest, NextResponse } from "next/server"; +import { Redis } from "@upstash/redis"; +import { generateId } from "pkg/id"; +import { z } from "zod"; + +export const requestValidation = z.object({ + // ttl in seconds + // defaults to 30 days + // not more than 1 year + // 0 means no expiration + ttl: z + .string() + .nullable() + .transform((v) => (v ? parseInt(v, 10) : 43260)) + .refine((v) => v >= 0 && v <= 30758400, "ttl must be between 0 and 30758400 seconds"), + + // number of reads before deletion + // defaults to null (no limit) + reads: z + .string() + .nullable() + .transform((v) => (v ? parseInt(v, 10) : null)) + .refine((v) => v === null || v > 0, "reads must be greater than 0"), + secret: z.string().min(1), +}); +export const responseValidation = z.union([ + z.object({ + data: z.object({ + id: z.string(), + ttl: z.number().optional(), + reads: z.number().optional(), + expiresAt: z.string(), + url: z.string().url(), + }), + }), + z.object({ + error: z.string(), + }), +]); + +const redis = Redis.fromEnv(); + +export default async function handler(req: NextRequest): Promise { + try { + if (req.method !== "POST") { + return NextResponse.json({ error: "Method Not Allowed" }, { status: 405 }); + } + + const parsed = requestValidation.safeParse({ + ttl: req.headers.get("envshare-ttl"), + reads: req.headers.get("envshare-reads"), + secret: await req.text(), + }); + if (!parsed.success) { + return NextResponse.json({ error: JSON.parse(parsed.error.message) }, { status: 400 }); + } + const { ttl, reads, secret } = parsed.data; + + const id = generateId(); + const rediskey = ["envshare", id].join(":"); + + const tx = redis.multi(); + + tx.hset(rediskey, { + remainingReads: reads ?? null, + secret, + }); + tx.incr("envshare:metrics:writes"); + if (ttl > 0) { + tx.expire(rediskey, ttl); + } + + await tx.exec(); + const url = new URL(req.url); + url.pathname = `/api/v1/secret/${id}`; + + return NextResponse.json( + responseValidation.parse({ + data: { + id, + ttl: ttl > 0 ? ttl : undefined, + reads: reads ?? undefined, + expiresAt: ttl > 0 ? new Date(Date.now() + ttl * 1000).toISOString() : undefined, + url: url.toString(), + }, + }), + ); + } catch (e) { + console.error(e); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); + } +} + +export const config = { + runtime: "edge", +}; diff --git a/envshare/pages/api/v1/store.ts b/envshare/pages/api/v1/store.ts new file mode 100644 index 0000000..c35e9b4 --- /dev/null +++ b/envshare/pages/api/v1/store.ts @@ -0,0 +1,38 @@ +import { NextRequest, NextResponse } from "next/server"; +import { Redis } from "@upstash/redis"; +import { generateId } from "pkg/id"; + +type Request = { + encrypted: string; + ttl?: number; + reads: number; + iv: string; +}; + +const redis = Redis.fromEnv(); +export default async function handler(req: NextRequest) { + const { encrypted, ttl, reads, iv } = (await req.json()) as Request; + + const id = generateId(); + const key = ["envshare", id].join(":"); + + const tx = redis.multi(); + + tx.hset(key, { + remainingReads: reads > 0 ? reads : null, + encrypted, + iv, + }); + if (ttl) { + tx.expire(key, ttl); + } + tx.incr("envshare:metrics:writes"); + + await tx.exec(); + + return NextResponse.json({ id }); +} + +export const config = { + runtime: "edge", +}; -- cgit v1.2.3-70-g09d2