Files
poshmanager/server/controllers/authController.js
2026-06-25 12:05:53 -05:00

82 lines
3.5 KiB
JavaScript

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 });
}
}