generated from nhcarrigan/template
136 lines
3.7 KiB
TypeScript
136 lines
3.7 KiB
TypeScript
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; |