const express = require('express'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const passport = require('passport'); const OAuth2Strategy = require('passport-oauth2').Strategy; const pool = require('../config/database'); const { validateRequest } = require('../utils/validation'); const { registerSchema, loginSchema, changePasswordSchema } = require('../utils/validation'); const { AppError } = require('../middleware/errorHandler'); const { authenticateToken } = require('../middleware/auth'); const router = express.Router(); // Configure Authentik OAuth2 strategy if (process.env.AUTHENTIK_CLIENT_ID && process.env.AUTHENTIK_CLIENT_SECRET && process.env.AUTHENTIK_BASE_URL) { passport.use('authentik', new OAuth2Strategy({ authorizationURL: `${process.env.AUTHENTIK_BASE_URL}/application/o/authorize/`, tokenURL: `${process.env.AUTHENTIK_BASE_URL}/application/o/token/`, clientID: process.env.AUTHENTIK_CLIENT_ID, clientSecret: process.env.AUTHENTIK_CLIENT_SECRET, callbackURL: process.env.AUTHENTIK_CALLBACK_URL || '/api/auth/authentik/callback' }, async (accessToken, refreshToken, profile, done) => { try { // Get user info from Authentik const axios = require('axios'); const userInfoResponse = await axios.get(`${process.env.AUTHENTIK_BASE_URL}/application/o/userinfo/`, { headers: { 'Authorization': `Bearer ${accessToken}` } }); const userInfo = userInfoResponse.data; // Check if user already exists const existingUser = await pool.query( 'SELECT * FROM users WHERE oauth_provider = $1 AND oauth_id = $2', ['authentik', userInfo.sub] ); if (existingUser.rows.length > 0) { return done(null, existingUser.rows[0]); } // Check if user exists with same email const emailUser = await pool.query( 'SELECT * FROM users WHERE email = $1', [userInfo.email] ); if (emailUser.rows.length > 0) { // Link Authentik account to existing user await pool.query( 'UPDATE users SET oauth_provider = $1, oauth_id = $2 WHERE id = $3', ['authentik', userInfo.sub, emailUser.rows[0].id] ); return done(null, emailUser.rows[0]); } // Create new user const newUser = await pool.query( `INSERT INTO users (email, first_name, last_name, oauth_provider, oauth_id) VALUES ($1, $2, $3, $4, $5) RETURNING *`, [ userInfo.email, userInfo.given_name || userInfo.name?.split(' ')[0] || 'User', userInfo.family_name || userInfo.name?.split(' ')[1] || '', 'authentik', userInfo.sub ] ); return done(null, newUser.rows[0]); } catch (error) { console.error('Authentik OAuth error:', error); return done(error, null); } })); } passport.serializeUser((user, done) => { done(null, user.id); }); passport.deserializeUser(async (id, done) => { try { const user = await pool.query('SELECT * FROM users WHERE id = $1', [id]); done(null, user.rows[0]); } catch (error) { done(error, null); } }); // Generate JWT token const generateToken = (userId) => { return jwt.sign( { userId }, process.env.JWT_SECRET, { expiresIn: '7d' } ); }; // @route POST /api/auth/register // @desc Register a new user // @access Public router.post('/register', validateRequest(registerSchema), async (req, res, next) => { try { const { email, password, firstName, lastName } = req.body; // Check if user already exists const existingUser = await pool.query( 'SELECT id FROM users WHERE email = $1', [email.toLowerCase()] ); if (existingUser.rows.length > 0) { throw new AppError('User with this email already exists', 409); } // Hash password const saltRounds = 12; const passwordHash = await bcrypt.hash(password, saltRounds); // Create user const newUser = await pool.query( `INSERT INTO users (email, password_hash, first_name, last_name) VALUES ($1, $2, $3, $4) RETURNING id, email, first_name, last_name, role`, [email.toLowerCase(), passwordHash, firstName, lastName] ); const user = newUser.rows[0]; const token = generateToken(user.id); res.status(201).json({ success: true, message: 'User registered successfully', data: { user: { id: user.id, email: user.email, firstName: user.first_name, lastName: user.last_name, role: user.role }, token } }); } catch (error) { next(error); } }); // @route POST /api/auth/login // @desc Login user // @access Public router.post('/login', validateRequest(loginSchema), async (req, res, next) => { try { const { email, password } = req.body; // Find user const userResult = await pool.query( 'SELECT id, email, password_hash, first_name, last_name, role FROM users WHERE email = $1', [email.toLowerCase()] ); if (userResult.rows.length === 0) { throw new AppError('Invalid email or password', 401); } const user = userResult.rows[0]; // Check password if (!user.password_hash) { throw new AppError('Please use Google sign-in for this account', 401); } const isValidPassword = await bcrypt.compare(password, user.password_hash); if (!isValidPassword) { throw new AppError('Invalid email or password', 401); } const token = generateToken(user.id); res.json({ success: true, message: 'Login successful', data: { user: { id: user.id, email: user.email, firstName: user.first_name, lastName: user.last_name, role: user.role }, token } }); } catch (error) { next(error); } }); // @route GET /api/auth/authentik // @desc Start Authentik OAuth2 flow // @access Public router.get('/authentik', (req, res, next) => { if (!process.env.AUTHENTIK_CLIENT_ID) { return res.status(503).json({ success: false, message: 'Authentik OAuth not configured' }); } passport.authenticate('authentik', { scope: 'openid profile email' })(req, res, next); }); // @route GET /api/auth/authentik/callback // @desc Authentik OAuth2 callback // @access Public router.get('/authentik/callback', passport.authenticate('authentik', { session: false }), (req, res) => { const token = generateToken(req.user.id); // Redirect to frontend with token const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000'; res.redirect(`${frontendUrl}/auth/callback?token=${token}`); } ); // @route POST /api/auth/change-password // @desc Change user password // @access Private router.post('/change-password', authenticateToken, validateRequest(changePasswordSchema), async (req, res, next) => { try { const { currentPassword, newPassword } = req.body; const userId = req.user.id; // Get current user const userResult = await pool.query( 'SELECT password_hash FROM users WHERE id = $1', [userId] ); const user = userResult.rows[0]; if (!user.password_hash) { throw new AppError('Cannot change password for OAuth accounts', 400); } // Verify current password const isValidPassword = await bcrypt.compare(currentPassword, user.password_hash); if (!isValidPassword) { throw new AppError('Current password is incorrect', 401); } // Hash new password const saltRounds = 12; const newPasswordHash = await bcrypt.hash(newPassword, saltRounds); // Update password await pool.query( 'UPDATE users SET password_hash = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2', [newPasswordHash, userId] ); res.json({ success: true, message: 'Password changed successfully' }); } catch (error) { next(error); } }); // @route POST /api/auth/forgot-password // @desc Request password reset // @access Public router.post('/forgot-password', async (req, res, next) => { try { const { email } = req.body; // Check if user exists const userResult = await pool.query( 'SELECT id, first_name FROM users WHERE email = $1', [email.toLowerCase()] ); // Always return success for security (don't reveal if email exists) res.json({ success: true, message: 'If an account with that email exists, a password reset link has been sent' }); // TODO: Implement email sending logic // In a real application, you would: // 1. Generate a secure reset token // 2. Store it in database with expiration // 3. Send email with reset link } catch (error) { next(error); } }); // @route GET /api/auth/me // @desc Get current user info // @access Private router.get('/me', authenticateToken, async (req, res, next) => { try { const userResult = await pool.query( 'SELECT id, email, first_name, last_name, role, created_at FROM users WHERE id = $1', [req.user.id] ); const user = userResult.rows[0]; res.json({ success: true, data: { user: { id: user.id, email: user.email, firstName: user.first_name, lastName: user.last_name, role: user.role, createdAt: user.created_at } } }); } catch (error) { next(error); } }); // @route GET /api/auth/registration-status // @desc Get registration status (public endpoint) // @access Public router.get('/registration-status', async (req, res, next) => { try { // Create admin_settings table if it doesn't exist await pool.query(` CREATE TABLE IF NOT EXISTS admin_settings ( id SERIAL PRIMARY KEY, setting_key VARCHAR(255) UNIQUE NOT NULL, setting_value TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); // Insert default registration setting if it doesn't exist await pool.query(` INSERT INTO admin_settings (setting_key, setting_value) VALUES ('registrationEnabled', 'true') ON CONFLICT (setting_key) DO NOTHING `); // Get registration setting const settingResult = await pool.query( 'SELECT setting_value FROM admin_settings WHERE setting_key = $1', ['registrationEnabled'] ); const enabled = settingResult.rows.length > 0 ? settingResult.rows[0].setting_value === 'true' : true; res.json({ success: true, data: { enabled } }); } catch (error) { next(error); } }); module.exports = router;