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:
@@ -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));
|
||||||
|
});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user