import { FastifyPluginAsync } from "fastify"; import { AuthService } from "../../services/auth.service"; import { AuditService } from "../../services/audit.service"; import { AuthResponse, AuditAction, AuditCategory } from "@library/shared-types"; const authRoutes: FastifyPluginAsync = async (app) => { const authService = new AuthService(app); /** * Discord OAuth callback. */ app.get("/callback", async (request, reply) => { try { const tokenResult = await app.oauth2Discord.getAccessTokenFromAuthorizationCodeFlow( request ); // Get user data from Discord API const discordResponse = await fetch("https://discord.com/api/users/@me", { headers: { Authorization: `Bearer ${tokenResult.token.access_token}`, }, }); if (!discordResponse.ok) { throw new Error("Failed to fetch Discord user data"); } const userData = await discordResponse.json(); // Create or update user in database const user = await authService.createOrUpdateUserFromDiscord(userData); // Generate JWT const jwt = await authService.generateToken(user); // Log successful login await AuditService.log({ action: AuditAction.LOGIN, category: AuditCategory.AUTH, userId: user.id, details: `User ${user.username} logged in via Discord`, success: true, }, request); // Set signed cookie and redirect to frontend reply .setCookie("auth-token", jwt, { path: "/", httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", maxAge: 7 * 24 * 60 * 60, // 7 days signed: true, }) .redirect("/"); // Redirect to root since API serves frontend } catch (error) { // Log failed login attempt await AuditService.log({ action: AuditAction.LOGIN_FAILED, category: AuditCategory.SECURITY, details: error instanceof Error ? error.message : String(error), success: false, }, request); app.log.error({ err: error }, "Auth callback error"); reply .code(401) .send({ error: "Authentication failed" }); } }); /** * Get current user. */ app.get<{ Reply: AuthResponse | { error: string } }>( "/me", { preValidation: [app.authenticate], }, async (request, reply) => { const jwtUser = request.user as { id: string }; const user = await authService.getUserById(jwtUser.id); if (!user) { return reply.code(404).send({ error: "User not found" }); } const token = await authService.generateToken(user); return { user, accessToken: token, }; } ); /** * Logout. */ app.post("/logout", async (request, reply) => { // Try to get user ID from JWT if available try { await request.jwtVerify(); const user = request.user as { id?: string; username?: string }; if (user?.id) { await AuditService.log({ action: AuditAction.LOGOUT, category: AuditCategory.AUTH, userId: user.id, details: `User ${user.username ?? "unknown"} logged out`, success: true, }, request); } } catch { // User wasn't authenticated, just proceed with logout } reply .clearCookie("auth-token", { path: "/", signed: true, }) .send({ message: "Logged out successfully" }); }); /** * Get CSRF token for state-changing requests. */ app.get("/csrf-token", async (request, reply) => { const token = reply.generateCsrf(); return { csrfToken: token }; }); }; export default authRoutes;