+
+ Protect your account:{" "}
+ Your role has elevated permissions. We recommend enabling multi-factor authentication (MFA).
+
+
+
+ Set up MFA
+
+
+
+
+ );
+}
diff --git a/apps/web/src/server/auth.ts b/apps/web/src/server/auth.ts
index 68b95e7..bcf8cb6 100644
--- a/apps/web/src/server/auth.ts
+++ b/apps/web/src/server/auth.ts
@@ -118,6 +118,19 @@ const authConfig = {
}
}
+ // MFA enforcement: if the user's role is in requireMfaForRoles but they
+ // haven't set up TOTP yet, block login and signal setup requirement.
+ if (!user.totpEnabled) {
+ const settings = await prisma.systemSettings.findUnique({
+ where: { id: "singleton" },
+ select: { requireMfaForRoles: true },
+ });
+ const requireMfaForRoles = (settings?.requireMfaForRoles as string[] | null) ?? [];
+ if (requireMfaForRoles.includes(user.systemRole)) {
+ throw new Error("MFA_REQUIRED_SETUP:" + user.id);
+ }
+ }
+
// Track last login time
await prisma.user.update({
where: { id: user.id },
diff --git a/docker-compose.yml b/docker-compose.yml
index c4efebd..38e8e80 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -39,8 +39,8 @@ services:
ports:
- "5050:80"
environment:
- PGADMIN_DEFAULT_EMAIL: admin@capakraken.dev
- PGADMIN_DEFAULT_PASSWORD: admin
+ PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL:-admin@capakraken.dev}
+ PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:?PGADMIN_PASSWORD must be set}
depends_on:
postgres:
condition: service_healthy
diff --git a/packages/api/src/__tests__/mfa-enforcement.test.ts b/packages/api/src/__tests__/mfa-enforcement.test.ts
new file mode 100644
index 0000000..25f2c3e
--- /dev/null
+++ b/packages/api/src/__tests__/mfa-enforcement.test.ts
@@ -0,0 +1,101 @@
+/**
+ * Unit tests for MFA enforcement via SystemSettings.requireMfaForRoles.
+ *
+ * Tests cover:
+ * - requireMfaForRoles is returned by buildSystemSettingsViewModel
+ * - buildSettingsUpdatePayload includes requireMfaForRoles in the DB payload
+ * - buildSettingsUpdatePayload handles null (clear enforcement)
+ * - Schema validation: valid roles accepted, invalid roles rejected
+ */
+
+import { describe, expect, it } from "vitest";
+import {
+ buildSettingsUpdatePayload,
+ buildSystemSettingsViewModel,
+ settingsUpdateInputSchema,
+} from "../router/settings-support.js";
+import type { RuntimeSecretField, RuntimeSecretStatus } from "../lib/system-settings-runtime.js";
+import { RUNTIME_SECRET_FIELDS } from "../lib/system-settings-runtime.js";
+
+const emptyRuntimeSecrets = Object.fromEntries(
+ RUNTIME_SECRET_FIELDS.map((field) => [
+ field,
+ { configured: false, activeSource: "none", hasStoredValue: false, envVarNames: [] } satisfies RuntimeSecretStatus,
+ ]),
+) as Record