summaryrefslogtreecommitdiff
path: root/website/src/actions/otp/verify-otp.ts
diff options
context:
space:
mode:
authorJoe Carstairs <me@joeac.net>2025-12-18 11:03:32 +0000
committerJoe Carstairs <me@joeac.net>2025-12-18 11:03:32 +0000
commit40d6c7f248c3fe506690ee3ccb3894f952e164ed (patch)
treefe135c14cb58385d109a3bfcadde46dc5ffcafba /website/src/actions/otp/verify-otp.ts
parenta81d1de1e58ee2c314dbbea941dcb4e4a776ed84 (diff)
otp actions
Diffstat (limited to 'website/src/actions/otp/verify-otp.ts')
-rw-r--r--website/src/actions/otp/verify-otp.ts48
1 files changed, 48 insertions, 0 deletions
diff --git a/website/src/actions/otp/verify-otp.ts b/website/src/actions/otp/verify-otp.ts
new file mode 100644
index 0000000..726786e
--- /dev/null
+++ b/website/src/actions/otp/verify-otp.ts
@@ -0,0 +1,48 @@
+import { randomBytes } from "node:crypto";
+import { z } from "astro/zod";
+import { defineAction } from "astro:actions";
+import { and, db, eq, gte, Otp, SendmailToken } from "astro:db";
+
+export default defineAction({
+ input: z.object({
+ guess: z.string().length(6),
+ lenient: z.boolean().default(false),
+ userId: z.string().nonempty(),
+ }),
+ handler: verifyOtp,
+});
+
+async function verifyOtp({ guess, lenient, userId }: VerifyOtpParams) {
+ const leniency = lenient ? 1000 * 60 : 0;
+ const isOtpCorrect =
+ (await db.$count(
+ Otp,
+ and(
+ eq(Otp.userId, userId),
+ eq(Otp.value, guess),
+ gte(Otp.validUntil, Date.now() - leniency),
+ ),
+ )) > 0;
+
+ if (!isOtpCorrect) {
+ return false;
+ }
+
+ await db.delete(Otp).where(and(eq(Otp.userId, userId), eq(Otp.value, guess)));
+
+ const token = randomBytes(256).toString("hex");
+ await db.insert(SendmailToken).values({
+ userId,
+ value: token,
+ createdAt: Date.now(),
+ validUntil: Date.now() + 60_000,
+ });
+
+ return token;
+}
+
+type VerifyOtpParams = {
+ guess: string;
+ lenient: boolean;
+ userId: string;
+};