diff options
| author | JustZvan <justzvan@justzvan.xyz> | 2026-02-06 12:16:40 +0100 |
|---|---|---|
| committer | JustZvan <justzvan@justzvan.xyz> | 2026-02-06 12:16:40 +0100 |
| commit | e904e9634548e47d611bdcbb88d7b180b927fd5f (patch) | |
| tree | 21aa5be08fc5b22585508c0263ee5ea4effcc593 /src/routes/signin.ts | |
feat: initial commit!
Diffstat (limited to 'src/routes/signin.ts')
| -rw-r--r-- | src/routes/signin.ts | 175 |
1 files changed, 175 insertions, 0 deletions
diff --git a/src/routes/signin.ts b/src/routes/signin.ts new file mode 100644 index 0000000..7c17432 --- /dev/null +++ b/src/routes/signin.ts @@ -0,0 +1,175 @@ +import argon2 from "argon2"; +import express from "express"; +import { users, linkedDevices } from "../db/schema"; +import { db } from "../db/db"; +import { Infer, z } from "zod"; +import { signJwt } from "../account/jwt"; +import { eq } from "drizzle-orm"; +import { logger } from "../lib/pino"; + +/** Validates signin request body with email and password */ +const SigninBodySchema = z.object({ + email: z + .string() + .transform((val) => val.trim()) + .pipe( + z + .email({ error: "Invalid email" }) + .nonempty({ error: "Email can't be empty" }), + ), + password: z.string(), +}); + +const router: express.Router = express.Router(); + +router.post("/signin", async (req, res) => { + const body: Infer<typeof SigninBodySchema> = req.body; + logger.info({ email: body.email }, "Signin attempt initiated"); + + const parsed = SigninBodySchema.safeParse(body); + + if (!parsed.success) { + logger.warn( + { email: body.email, error: parsed.error }, + "Signin validation failed", + ); + res.send({ + success: false, + reason: parsed.error, + token: "", + }); + return; + } + + const existingUser = ( + await db.select().from(users).where(eq(users.email, body.email)).limit(1) + )[0]; + + if (!existingUser) { + logger.warn({ email: body.email }, "Signin failed: user not found"); + res.send({ + success: false, + reason: "Invalid email or password", + }); + return; + } + + const validPassword = await argon2.verify( + existingUser.password, + body.password, + ); + + if (!validPassword) { + logger.warn( + { email: body.email, userId: existingUser.id }, + "Signin failed: invalid password", + ); + res.send({ + success: false, + reason: "Invalid email or password", + }); + return; + } + + const jwt = await signJwt( + { id: existingUser.id, type: "parent" }, + "urn:buddy:users", + ); + + logger.info( + { userId: existingUser.id, email: body.email }, + "User signin successful", + ); + + res.send({ + success: true, + token: jwt, + reason: "", + }); +}); + +router.post("/kid/link", async (req, res) => { + const body: Infer<typeof SigninBodySchema> = req.body; + + const parsed = SigninBodySchema.safeParse(body); + + if (!parsed.success) { + logger.warn({ error: parsed.error }, "Kid link validation failed"); + res.send({ + success: false, + reason: parsed.error, + token: "", + }); + return; + } + + logger.info({ email: parsed.data.email }, "Kid link request initiated"); + + const existingUser = ( + await db + .select() + .from(users) + .where(eq(users.email, parsed.data.email)) + .limit(1) + )[0]; + + if (!existingUser) { + logger.warn( + { email: parsed.data.email }, + "Kid link failed: user not found", + ); + res.send({ + success: false, + reason: "Invalid email or password", + }); + return; + } + + logger.debug({ email: parsed.data.email }, "User found for kid link"); + + const validPassword = await argon2.verify( + existingUser.password, + parsed.data.password, + ); + + if (!validPassword) { + res.send({ + success: false, + reason: "Invalid email or password", + }); + return; + } + + if (!existingUser.emailVerified) { + res.send({ + success: false, + reason: "You must verify your email in the parent app before using Buddy", + }); + return; + } + + const newDevice = ( + await db + .insert(linkedDevices) + .values({ parentId: existingUser.id }) + .returning({ id: linkedDevices.id }) + )[0]; + + const jwt = await signJwt( + { id: newDevice!.id, type: "child" }, + "urn:buddy:devices", + ); + + logger.info( + { deviceId: newDevice!.id, parentId: existingUser.id }, + "New child device linked successfully", + ); + + res.send({ + success: true, + token: jwt, + reason: "", + }); +}); + +export default router; |