- Phase 25.4: N shortcut for new exploration on dashboard (react-hotkeys-hook) - Phase 25.5: overflow-x-auto on tables, responsive padding (p-4 md:p-6) - Phase 26: SAML/OIDC/LDAP providers (build-fixed), TOTP/MFA service - Phase 26: KyselySSOConfigRepository + KyselyTOTPRepository - Phase 26: SSO HTTP controller (config CRUD + MFA setup/verify/disable) - Phase 26: Audit module index.ts + SSO module index.ts - Phase 26: Session management endpoints (findByUserId, deleteById, list/revoke) - Phase 26: SSO and audit routes feature-gated (auth:sso, audit:logs) - Phase 26: Frontend SSOSection (SAML/OIDC/LDAP config + TOTP setup) - Phase 26: Frontend SessionsSection (list/revoke active sessions) - Phase 26: Settings navigation updated with SSO & Sessions sections Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
193 lines
7.5 KiB
JavaScript
193 lines
7.5 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.createAuthController = createAuthController;
|
|
const express_1 = require("express");
|
|
const AuthMiddleware_1 = require("../../application/middleware/AuthMiddleware");
|
|
function createAuthController(registerCommand, loginCommand, createOrgCommand, inviteMemberCommand, createApiKeyCommand, getUserQuery, listOrgMembersQuery, sessionRepository, apiKeyRepository, userRepository) {
|
|
const router = (0, express_1.Router)();
|
|
const authMiddleware = (0, AuthMiddleware_1.createAuthMiddleware)(userRepository, sessionRepository, apiKeyRepository);
|
|
// POST /api/auth/register
|
|
router.post('/register', async (req, res) => {
|
|
const result = await registerCommand.execute({
|
|
email: req.body.email,
|
|
password: req.body.password,
|
|
name: req.body.name,
|
|
role: req.body.role,
|
|
});
|
|
if (!result.ok) {
|
|
res.status(400).json({ error: result.error });
|
|
return;
|
|
}
|
|
res.status(201).json(result.value);
|
|
});
|
|
// POST /api/auth/login
|
|
router.post('/login', async (req, res) => {
|
|
const result = await loginCommand.execute({
|
|
email: req.body.email,
|
|
password: req.body.password,
|
|
});
|
|
if (!result.ok) {
|
|
res.status(401).json({ error: result.error });
|
|
return;
|
|
}
|
|
const { sessionToken, expiresAt, ...userData } = result.value;
|
|
res.cookie('abe_session', sessionToken, {
|
|
httpOnly: true,
|
|
secure: process.env['NODE_ENV'] === 'production',
|
|
sameSite: 'lax',
|
|
expires: expiresAt,
|
|
});
|
|
res.json({ ...userData, sessionToken });
|
|
});
|
|
// POST /api/auth/logout
|
|
router.post('/logout', authMiddleware, async (req, res) => {
|
|
const token = req.cookies?.['abe_session'] ?? req.headers.authorization?.substring(7);
|
|
if (token) {
|
|
await sessionRepository.deleteByToken(token);
|
|
}
|
|
res.clearCookie('abe_session');
|
|
res.json({ success: true });
|
|
});
|
|
// GET /api/auth/me
|
|
router.get('/me', authMiddleware, async (req, res) => {
|
|
const result = await getUserQuery.execute({ userId: req.user.id });
|
|
if (!result.ok) {
|
|
res.status(404).json({ error: result.error });
|
|
return;
|
|
}
|
|
res.json(result.value);
|
|
});
|
|
// GET /api/auth/setup-required
|
|
router.get('/setup-required', async (_req, res) => {
|
|
const count = await userRepository.count();
|
|
res.json({ required: count === 0 });
|
|
});
|
|
// POST /api/auth/setup — first-run setup
|
|
router.post('/setup', async (req, res) => {
|
|
const count = await userRepository.count();
|
|
if (count > 0) {
|
|
res.status(400).json({ error: 'Setup already completed' });
|
|
return;
|
|
}
|
|
const registerResult = await registerCommand.execute({
|
|
email: req.body.email,
|
|
password: req.body.password,
|
|
name: req.body.name,
|
|
role: 'owner',
|
|
});
|
|
if (!registerResult.ok) {
|
|
res.status(400).json({ error: registerResult.error });
|
|
return;
|
|
}
|
|
const createOrgResult = await createOrgCommand.execute({
|
|
name: req.body.orgName ?? 'My Organization',
|
|
ownerId: registerResult.value.userId,
|
|
});
|
|
if (!createOrgResult.ok) {
|
|
res.status(400).json({ error: createOrgResult.error });
|
|
return;
|
|
}
|
|
res.status(201).json({
|
|
user: registerResult.value,
|
|
organization: createOrgResult.value,
|
|
});
|
|
});
|
|
// POST /api/auth/organizations — create org
|
|
router.post('/organizations', authMiddleware, async (req, res) => {
|
|
const result = await createOrgCommand.execute({
|
|
name: req.body.name,
|
|
ownerId: req.user.id,
|
|
});
|
|
if (!result.ok) {
|
|
res.status(400).json({ error: result.error });
|
|
return;
|
|
}
|
|
res.status(201).json(result.value);
|
|
});
|
|
// POST /api/auth/organizations/:orgId/members — invite member
|
|
router.post('/organizations/:orgId/members', authMiddleware, async (req, res) => {
|
|
const result = await inviteMemberCommand.execute({
|
|
orgId: String(req.params['orgId']),
|
|
inviterUserId: req.user.id,
|
|
email: req.body.email,
|
|
role: req.body.role ?? 'member',
|
|
});
|
|
if (!result.ok) {
|
|
res.status(400).json({ error: result.error });
|
|
return;
|
|
}
|
|
res.status(201).json(result.value);
|
|
});
|
|
// GET /api/auth/organizations/:orgId/members
|
|
router.get('/organizations/:orgId/members', authMiddleware, async (req, res) => {
|
|
const result = await listOrgMembersQuery.execute({ orgId: String(req.params['orgId']) });
|
|
if (!result.ok) {
|
|
res.status(404).json({ error: result.error });
|
|
return;
|
|
}
|
|
res.json(result.value);
|
|
});
|
|
// POST /api/auth/api-keys — create API key
|
|
router.post('/api-keys', authMiddleware, async (req, res) => {
|
|
const result = await createApiKeyCommand.execute({
|
|
userId: req.user.id,
|
|
orgId: req.user.orgId ?? 'default',
|
|
name: req.body.name,
|
|
permissions: req.body.permissions,
|
|
expiresAt: req.body.expiresAt ? new Date(req.body.expiresAt) : undefined,
|
|
});
|
|
if (!result.ok) {
|
|
res.status(400).json({ error: result.error });
|
|
return;
|
|
}
|
|
res.status(201).json(result.value);
|
|
});
|
|
// GET /api/auth/api-keys — list API keys
|
|
router.get('/api-keys', authMiddleware, async (req, res) => {
|
|
const keys = await apiKeyRepository.listByUser(req.user.id);
|
|
res.json(keys.map((k) => ({
|
|
id: k.id.toString(),
|
|
name: k.name,
|
|
keyPrefix: k.keyPrefix,
|
|
permissions: k.permissions,
|
|
expiresAt: k.expiresAt,
|
|
lastUsedAt: k.lastUsedAt,
|
|
createdAt: k.createdAt,
|
|
})));
|
|
});
|
|
// DELETE /api/auth/api-keys/:id — revoke API key
|
|
router.delete('/api-keys/:id', authMiddleware, async (req, res) => {
|
|
const keyId = String(req.params['id']);
|
|
const key = await apiKeyRepository.findById(keyId);
|
|
if (!key || key.userId !== req.user.id) {
|
|
res.status(404).json({ error: 'API key not found' });
|
|
return;
|
|
}
|
|
await apiKeyRepository.delete(keyId);
|
|
res.json({ success: true });
|
|
});
|
|
// GET /api/auth/sessions — list active sessions (session management dashboard)
|
|
router.get('/sessions', authMiddleware, async (req, res) => {
|
|
const sessions = await sessionRepository.findByUserId(req.user.id);
|
|
res.json(sessions.map((s) => ({
|
|
id: s.id,
|
|
createdAt: new Date(s.createdAt).toISOString(),
|
|
expiresAt: new Date(s.expiresAt).toISOString(),
|
|
})));
|
|
});
|
|
// DELETE /api/auth/sessions/:id — revoke a specific session
|
|
router.delete('/sessions/:id', authMiddleware, async (req, res) => {
|
|
const sessionId = String(req.params['id']);
|
|
// Only allow revoking own sessions
|
|
const userSessions = await sessionRepository.findByUserId(req.user.id);
|
|
const owns = userSessions.some((s) => s.id === sessionId);
|
|
if (!owns) {
|
|
res.status(404).json({ error: 'Session not found' });
|
|
return;
|
|
}
|
|
await sessionRepository.deleteById(sessionId);
|
|
res.json({ success: true });
|
|
});
|
|
return router;
|
|
}
|