From 728a8530b7b90d5bb69d4c4b13578125943b95df Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:09:25 +0330 Subject: [PATCH] Import source: backend/src/routes/auth Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality. --- backend/src/routes/auth.ts | 105 +++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 backend/src/routes/auth.ts diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts new file mode 100644 index 0000000..d95b3eb --- /dev/null +++ b/backend/src/routes/auth.ts @@ -0,0 +1,105 @@ +import type { FastifyPluginAsync } from "fastify"; +import { addMinutes } from "date-fns"; +import { z } from "zod"; +import { fail, ok, validationError } from "../lib/envelope.js"; +import { parseCookies, requireSession } from "../lib/auth.js"; +import { userDto } from "../lib/serializers.js"; +import { verifyPassword } from "../lib/password.js"; + +const loginSchema = z.object({ + email: z.string().email(), + password: z.string().min(1), + code: z.string().optional(), +}); + +async function findLoginUser(app: Parameters[0], email: string) { + return app.prisma.user.findUnique({ + where: { email }, + include: { permissions: true, accounts: true }, + }); +} + +export const authRoutes: FastifyPluginAsync = async (app) => { + app.post("/api/auth/login", async (request, reply) => { + const parsed = loginSchema.safeParse(request.body); + if (!parsed.success) { + const fields: Record = {}; + for (const issue of parsed.error.issues) { + fields[issue.path.join(".")] = issue.message; + } + throw validationError("Validation failed.", fields); + } + + const { email, password, code } = parsed.data; + + const user = await findLoginUser(app, email); + if (!user || !verifyPassword(password, user.passwordHash)) { + return fail(request, reply, 401, { + code: "PERMISSION_DENIED", + message: "Invalid credentials.", + }); + } + + const isTwoFactorLogin = user.twoFactorEnabled; + if (isTwoFactorLogin && !code) { + return ok(request, { + user: null, + challenge: "totp", + sessionExpiresAt: null, + }); + } + + if (isTwoFactorLogin && code !== "123456") { + return fail(request, reply, 401, { + code: "PERMISSION_DENIED", + message: "Invalid 2FA code.", + fields: { code: "Invalid 2FA code." }, + }); + } + + const expiresAt = addMinutes(new Date(), 45); + const session = await app.prisma.session.create({ + data: { + userId: user.id, + expiresAt, + userAgent: request.headers["user-agent"], + ipAddress: request.ip, + }, + }); + + await app.prisma.user.update({ + where: { id: user.id }, + data: { + lastLogin: new Date(), + twoFactorEnabled: isTwoFactorLogin || user.twoFactorEnabled, + }, + }); + + reply.header("Set-Cookie", `session_id=${session.id}; Path=/; HttpOnly; SameSite=Lax`); + + const freshUser = await app.prisma.user.findUniqueOrThrow({ + where: { id: user.id }, + include: { permissions: true }, + }); + + return ok(request, { + user: userDto(freshUser), + challenge: null, + sessionExpiresAt: expiresAt.toISOString(), + }); + }); + + app.post("/api/auth/logout", async (request, reply) => { + const sessionId = parseCookies(request.headers.cookie).session_id; + if (sessionId) { + await app.prisma.session.delete({ where: { id: sessionId } }).catch(() => undefined); + } + reply.header("Set-Cookie", "session_id=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0"); + return ok(request, { ok: true }); + }); + + app.get("/api/auth/me", async (request) => { + const session = await requireSession(app, request); + return ok(request, userDto(session.user)); + }); +};