aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/envshare/pages/api
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 /envshare/pages/api
parentb135aac8531c1e1488147ad8c6f98eddbdbe0c99 (diff)
downloadHydroRoll-4919f028c884a041da7ff098abb02389b4eac598.tar.gz
HydroRoll-4919f028c884a041da7ff098abb02389b4eac598.zip
✨add envshare docs
Diffstat (limited to 'envshare/pages/api')
-rw-r--r--envshare/pages/api/v1/load.ts36
-rw-r--r--envshare/pages/api/v1/og.tsx64
-rw-r--r--envshare/pages/api/v1/secret/[id].ts58
-rw-r--r--envshare/pages/api/v1/secret/index.ts96
-rw-r--r--envshare/pages/api/v1/store.ts38
5 files changed, 292 insertions, 0 deletions
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(
+ <div tw='w-[1200px] h-[630px] flex flex-col items-center justify-center text-center'>
+ {/* backgroundImage: bg-gradient-to-tr from-zinc-900/50 to-zinc-700/30 */}
+ <div
+ tw="bg-black w-full h-full flex"
+ style={{ backgroundImage: "linear-gradient(to top right, rgba(24,24,27,.5), rgba(63,63,70,.3))" }}
+ >
+ <div tw="flex flex-col text-3xl tracking-tight text-gray-300 w-full items-center h-full justify-center text-center">
+ {/* font-semibold bg-gradient-to-t bg-clip-text from-zinc-100/50 to-white whitespace-pre */}
+ <h1
+ tw="text-white text-7xl"
+ style={{
+ color: "transparent",
+ paddingLeft: "12rem",
+ paddingRight: "12rem",
+ backgroundImage: "linear-gradient(to top, rgba(244, 244, 245, .5), rgba(255,255,255,1))",
+ backgroundClip: "text",
+ }}
+ >
+ {title}
+ </h1>
+ <p tw="mt-4 font-bold">{subtitle}</p>
+ </div>
+ </div>
+ </div>,
+ {
+ 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<NextResponse> {
+ 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<NextResponse> {
+ 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",
+};