import { authenticateLocal, publicUser, signUser } from '../auth.js'; import { loginSchema, passwordChangeSchema, profileSchema, themeSchema } from '../forms/schemas.js'; import { changePassword, updateProfile, updateTheme } from '../models/profileModel.js'; import { findOrProvisionEntraUser } from '../models/userModel.js'; import { buildAuthorizeUrl, exchangeCodeForToken, fetchEntraProfile, isEntraLoginConfigured, signState, verifyState } from '../services/entraService.js'; import { getSetting } from '../models/settingsModel.js'; import { config } from '../config.js'; import { logger } from '../services/logger.js'; export function login(req, res) { const parsed = loginSchema.safeParse(req.body); if (!parsed.success) return res.status(400).json({ error: 'Valid email and password are required' }); const user = authenticateLocal(parsed.data.email, parsed.data.password); if (!user) return res.status(401).json({ error: 'Invalid credentials' }); res.json({ token: signUser(user), user: publicUser(user) }); } export function me(req, res) { res.json({ user: req.user }); } // Step 1: redirect the browser to the Microsoft sign-in page. 404 when Entra // login is not fully configured so the client cleanly falls back to local auth. export function entraLogin(req, res) { if (!isEntraLoginConfigured()) return res.status(404).json({ error: 'Entra login is not enabled' }); const state = signState({ nonce: Date.now() }); return res.redirect(buildAuthorizeUrl(state)); } // Step 2: Microsoft redirects back here with an authorization code. We validate // the signed state, exchange the code, provision the user, and hand the SPA a // POSHManager JWT via the app URL. export async function entraCallback(req, res) { const appBase = (getSetting('server_fqdn') || config.serverFqdn || '').replace(/\/+$/, ''); if (!isEntraLoginConfigured()) return res.status(404).json({ error: 'Entra login is not enabled' }); try { if (req.query.error) throw new Error(String(req.query.error_description || req.query.error)); verifyState(String(req.query.state || '')); const tokens = await exchangeCodeForToken(String(req.query.code || '')); const profile = await fetchEntraProfile(tokens.access_token); const user = findOrProvisionEntraUser(profile); const token = signUser(user); return res.redirect(`${appBase}/?entraToken=${encodeURIComponent(token)}`); } catch (error) { logger.error('entra callback failed', { error: error.message }); return res.redirect(`${appBase}/?entraError=${encodeURIComponent(error.message)}`); } } export function profile(req, res) { const parsed = profileSchema.safeParse(req.body); if (!parsed.success) return res.status(400).json({ error: 'Valid profile information is required' }); try { res.json({ user: updateProfile(req.user.id, parsed.data) }); } catch (error) { res.status(400).json({ error: error.message }); } } export function theme(req, res) { const parsed = themeSchema.safeParse(req.body); if (!parsed.success) return res.status(400).json({ error: 'Valid theme id is required' }); res.json({ user: updateTheme(req.user.id, parsed.data.theme) }); } export function password(req, res) { const parsed = passwordChangeSchema.safeParse(req.body); if (!parsed.success) return res.status(400).json({ error: 'New password must be at least 8 characters' }); try { res.json(changePassword(req.user.id, parsed.data)); } catch (error) { res.status(400).json({ error: error.message }); } }