aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/envshare/pages/api/v1/secret
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/v1/secret
parentb135aac8531c1e1488147ad8c6f98eddbdbe0c99 (diff)
downloadHydroRoll-4919f028c884a041da7ff098abb02389b4eac598.tar.gz
HydroRoll-4919f028c884a041da7ff098abb02389b4eac598.zip
✨add envshare docs
Diffstat (limited to 'envshare/pages/api/v1/secret')
-rw-r--r--envshare/pages/api/v1/secret/[id].ts58
-rw-r--r--envshare/pages/api/v1/secret/index.ts96
2 files changed, 154 insertions, 0 deletions
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",
+};