69 lines
2.4 KiB
TypeScript
69 lines
2.4 KiB
TypeScript
import { Router } from 'express';
|
|
import bcrypt from 'bcryptjs';
|
|
import { z } from 'zod';
|
|
import { pool } from '../db.js';
|
|
import { config } from '../config.js';
|
|
import { requireAuth, signUser } from '../middleware/auth.js';
|
|
import { audit } from '../services/audit.js';
|
|
|
|
export const authRouter = Router();
|
|
|
|
const loginSchema = z.object({ email: z.string().email(), password: z.string().min(1) });
|
|
|
|
authRouter.post('/login', async (req, res) => {
|
|
const body = loginSchema.parse(req.body);
|
|
const result = await pool.query(
|
|
`SELECT id, email, password_hash, role, allowed_domains FROM admin_users WHERE email=$1 AND active=true`,
|
|
[body.email.toLowerCase()],
|
|
);
|
|
const user = result.rows[0];
|
|
if (!user || !(await bcrypt.compare(body.password, user.password_hash))) {
|
|
res.status(401).json({ error: 'Invalid email or password' });
|
|
return;
|
|
}
|
|
const token = signUser({ id: user.id, email: user.email, role: user.role, allowed_domains: user.allowed_domains ?? [] });
|
|
res.cookie('mailadmin_token', token, {
|
|
httpOnly: true,
|
|
sameSite: 'lax',
|
|
secure: config.cookieSecure,
|
|
maxAge: 12 * 60 * 60 * 1000,
|
|
});
|
|
res.json({ email: user.email, role: user.role, allowed_domains: user.allowed_domains ?? [] });
|
|
});
|
|
|
|
authRouter.post('/logout', (_req, res) => {
|
|
res.clearCookie('mailadmin_token');
|
|
res.json({ ok: true });
|
|
});
|
|
|
|
authRouter.get('/me', requireAuth, (req, res) => {
|
|
res.json(req.user);
|
|
});
|
|
|
|
// Self-service password change. Requires the current password to prevent
|
|
// session hijacking from changing the password silently.
|
|
const changePwSchema = z.object({
|
|
current_password: z.string().min(1),
|
|
new_password: z.string().min(8),
|
|
});
|
|
|
|
authRouter.post('/change-password', requireAuth, async (req, res) => {
|
|
const body = changePwSchema.parse(req.body);
|
|
const result = await pool.query(
|
|
`SELECT id, password_hash FROM admin_users WHERE email=$1 AND active=true`,
|
|
[req.user!.email.toLowerCase()],
|
|
);
|
|
const row = result.rows[0];
|
|
if (!row || !(await bcrypt.compare(body.current_password, row.password_hash))) {
|
|
res.status(401).json({ error: 'Current password is incorrect' });
|
|
return;
|
|
}
|
|
const newHash = await bcrypt.hash(body.new_password, 12);
|
|
await pool.query(
|
|
`UPDATE admin_users SET password_hash=$1, updated_at=now() WHERE id=$2`,
|
|
[newHash, row.id],
|
|
);
|
|
await audit(req.user!.email, 'admin.self_password_change', 'admin', req.user!.email, {}, req.ip);
|
|
res.json({ ok: true });
|
|
});
|