const passport = require('passport'); const OpenIDConnectStrategy = require('passport-openidconnect'); const express = require('express'); const { param } = require('./applications'); const router = express.Router(); import { Role } from '@app/shared/types/roles'; import pool from '../db'; import { requireLogin } from '../middleware/auth'; import { getUserRoles } from '../services/db/rolesService'; import { getUserState, mapDiscordtoID } from '../services/db/memberService'; import { MemberState } from '@app/shared/types/member'; import { toDateTime } from '@app/shared/utils/time'; import { logger } from '../services/logging/logger'; const querystring = require('querystring'); import { performance } from 'perf_hooks'; function parseJwt(token) { return JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()); } passport.use(new OpenIDConnectStrategy({ issuer: process.env.AUTH_ISSUER, authorizationURL: process.env.AUTH_DOMAIN + '/authorize/', tokenURL: process.env.AUTH_DOMAIN + '/token/', userInfoURL: process.env.AUTH_DOMAIN + '/userinfo/', clientID: process.env.AUTH_CLIENT_ID, clientSecret: process.env.AUTH_CLIENT_SECRET, callbackURL: process.env.AUTH_REDIRECT_URI, scope: ['openid', 'profile', 'discord'] }, async function verify(issuer, sub, profile, jwtClaims, accessToken, refreshToken, params, cb) { // console.log('--- OIDC verify() called ---'); // console.log('issuer:', issuer); // console.log('sub:', sub); // // console.log('discord:', discord); // console.log('profile:', profile); // console.log('jwt: ', parseJwt(jwtClaims)); // console.log('params:', params); let con; try { con = await pool.getConnection(); await con.beginTransaction(); //lookup existing user const existing = await con.query(`SELECT id FROM members WHERE authentik_issuer = ? AND authentik_sub = ? LIMIT 1;`, [issuer, sub]); let memberId: number | null = null; //if member exists if (existing.length > 0) { //login memberId = existing[0].id; logger.info('auth', `Existing member login`, { memberId, issuer, }); } else { //otherwise: create account mode const jwt = parseJwt(jwtClaims); const discordID = jwt.discord?.id as number; //check if account is available to claim if (discordID) memberId = await mapDiscordtoID(discordID); if (discordID && memberId) { // claim account const result = await con.query( `UPDATE members SET authentik_sub = ?, authentik_issuer = ? WHERE id = ?;`, [sub, issuer, memberId] ) logger.info('auth', `Existing member claimed via Discord`, { memberId, discordID, issuer, }); } else { // new account const username = sub.username; const result = await con.query( `INSERT INTO members (name, authentik_sub, authentik_issuer) VALUES (?, ?, ?)`, [username, sub, issuer] ) memberId = Number(result.insertId); logger.info('auth', `New member account created`, { memberId, username, issuer, }); } } await con.query(`UPDATE members SET last_login = ? WHERE id = ?`, [toDateTime(new Date()), memberId]) await con.commit(); return cb(null, { memberId }); } catch (error) { logger.error('auth', `Authentication transaction failed`, { issuer, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); if (con) { try { await con.rollback(); } catch (rollbackError) { logger.error('auth', `Rollback failed`, { error: rollbackError instanceof Error ? rollbackError.message : String(rollbackError), }); } } return cb(error); } finally { if (con) con.release(); } })); router.get('/login', (req, res, next) => { // Store redirect target in session if provided req.session.redirectTo = req.query.redirect; next(); }, passport.authenticate('openidconnect')); router.get('/callback', (req, res, next) => { const redirectURI = req.session.redirectTo; passport.authenticate('openidconnect', (err, user) => { if (err) return next(err); if (!user) return res.redirect(process.env.CLIENT_URL); req.logIn(user, err => { if (err) return next(err); // Use redirect saved from session const redirectTo = redirectURI || process.env.CLIENT_URL; delete req.session.redirectTo; return res.redirect(redirectTo); }); })(req, res, next); }); router.get('/logout', [requireLogin], function (req, res, next) { req.logout(function (err) { if (err) { return next(err); } req.session.destroy((err) => { if (err) { return next(err); } res.clearCookie('connect.sid', { path: '/', domain: process.env.CLIENT_DOMAIN, httpOnly: true, sameSite: 'lax' }); var params = { client_id: process.env.AUTH_CLIENT_ID, returnTo: process.env.CLIENT_URL }; logger.info('auth', `Member logged out`, { user: req.user.id, }); res.redirect(process.env.AUTH_END_SESSION_URI + '?' + querystring.stringify(params)); }) }); }); passport.serializeUser(function (user, cb) { process.nextTick(function () { cb(null, user); }); }); passport.deserializeUser(function (user, cb) { const start = performance.now(); const timings: Record = {}; process.nextTick(async function () { const memberID = user.memberId as number; let con; try { let t; t = performance.now(); con = await pool.getConnection(); timings.getConnection = performance.now() - t; t = performance.now(); const userResults = await con.query( `SELECT id, name FROM members WHERE id = ?;`, [memberID] ); timings.memberQuery = performance.now() - t; const userData: { id: number; name: string; roles: Role[]; state: MemberState; } = userResults[0]; t = performance.now(); const userRoles = await getUserRoles(memberID); timings.roles = performance.now() - t; userData.roles = userRoles || []; t = performance.now(); userData.state = await getUserState(memberID); timings.state = performance.now() - t; // 📊 PROFILING LOG logger.info( 'profiling', 'passport.deserializeUser completed', { memberId: memberID, total_ms: performance.now() - start, breakdown_ms: timings, }, 'profiling' ); return cb(null, userData); } catch (error) { logger.error( 'profiling', 'passport.deserializeUser failed', { memberId: memberID, error: error instanceof Error ? error.message : String(error), } ); return cb(error); } finally { if (con) con.release(); } }); }); declare global { namespace Express { interface Request { user: { id: number; name: string; roles: Role[]; state: MemberState; }; } } } export const authRouter = router;