82 lines
3.5 KiB
JavaScript
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 });
|
|
}
|
|
}
|