aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/envshare/pages/api/v1/secret/index.ts
blob: 423e7a0a402aac342c76d50f9b610cf4c7f74660 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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",
};