Import source: backend/src/routes/auth

Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
arsam
2026-09-18 23:09:25 +03:30
parent 2b7d4ecdc2
commit 728a8530b7
+105
View File
@@ -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<FastifyPluginAsync>[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<string, string> = {};
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));
});
};