@lamalibre/create-lamaste 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +118 -0
- package/README.md +114 -0
- package/bin/create-lamaste.js +15 -0
- package/package.json +54 -0
- package/scripts/bundle-vendor.js +110 -0
- package/scripts/lamaste-cert-info +62 -0
- package/scripts/lamaste-certbot +96 -0
- package/scripts/lamaste-priv +693 -0
- package/src/index.js +646 -0
- package/src/lib/accounts.js +86 -0
- package/src/lib/binaries.js +137 -0
- package/src/lib/cert-help-page.js +160 -0
- package/src/lib/env.js +132 -0
- package/src/lib/http-redirect.js +38 -0
- package/src/lib/ownership.js +231 -0
- package/src/lib/relay-services.js +103 -0
- package/src/lib/secrets.js +19 -0
- package/src/lib/service-config.js +311 -0
- package/src/lib/summary.js +131 -0
- package/src/lib/wrappers.js +63 -0
- package/src/tasks/gatekeeper.js +233 -0
- package/src/tasks/harden.js +293 -0
- package/src/tasks/mtls.js +193 -0
- package/src/tasks/nginx.js +265 -0
- package/src/tasks/node.js +110 -0
- package/src/tasks/panel.js +408 -0
- package/src/tasks/redeploy.js +438 -0
- package/vendor/docs/00-introduction/how-it-works.md +447 -0
- package/vendor/docs/00-introduction/quickstart.md +472 -0
- package/vendor/docs/00-introduction/what-is-lamaste.md +112 -0
- package/vendor/docs/01-concepts/authentication.md +464 -0
- package/vendor/docs/01-concepts/authorization.md +242 -0
- package/vendor/docs/01-concepts/certificates.md +452 -0
- package/vendor/docs/01-concepts/dns-and-domains.md +343 -0
- package/vendor/docs/01-concepts/mtls.md +490 -0
- package/vendor/docs/01-concepts/nginx-reverse-proxy.md +540 -0
- package/vendor/docs/01-concepts/security-model.md +600 -0
- package/vendor/docs/01-concepts/tickets.md +414 -0
- package/vendor/docs/01-concepts/tunneling.md +367 -0
- package/vendor/docs/02-guides/agent-setup.md +194 -0
- package/vendor/docs/02-guides/certificate-management.md +341 -0
- package/vendor/docs/02-guides/cloud-provisioning.md +406 -0
- package/vendor/docs/02-guides/desktop-app-setup.md +226 -0
- package/vendor/docs/02-guides/disaster-recovery.md +464 -0
- package/vendor/docs/02-guides/first-tunnel.md +230 -0
- package/vendor/docs/02-guides/installation.md +393 -0
- package/vendor/docs/02-guides/managing-users.md +255 -0
- package/vendor/docs/02-guides/onboarding.md +244 -0
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +375 -0
- package/vendor/docs/02-guides/static-sites.md +364 -0
- package/vendor/docs/03-architecture/e2e-three-vm-sequences.md +457 -0
- package/vendor/docs/03-architecture/installer-flow.md +151 -0
- package/vendor/docs/03-architecture/installer.md +445 -0
- package/vendor/docs/03-architecture/management-flow.md +288 -0
- package/vendor/docs/03-architecture/nginx-configuration.md +830 -0
- package/vendor/docs/03-architecture/onboarding-flow.md +164 -0
- package/vendor/docs/03-architecture/overview.md +541 -0
- package/vendor/docs/03-architecture/panel-client.md +416 -0
- package/vendor/docs/03-architecture/panel-server.md +695 -0
- package/vendor/docs/03-architecture/state-management.md +529 -0
- package/vendor/docs/03-architecture/system-overview.md +167 -0
- package/vendor/docs/04-api-reference/certificates.md +1141 -0
- package/vendor/docs/04-api-reference/gatekeeper.md +592 -0
- package/vendor/docs/04-api-reference/onboarding.md +473 -0
- package/vendor/docs/04-api-reference/overview.md +404 -0
- package/vendor/docs/04-api-reference/services.md +267 -0
- package/vendor/docs/04-api-reference/settings.md +343 -0
- package/vendor/docs/04-api-reference/sites.md +716 -0
- package/vendor/docs/04-api-reference/system.md +268 -0
- package/vendor/docs/04-api-reference/tickets.md +618 -0
- package/vendor/docs/04-api-reference/tunnels.md +417 -0
- package/vendor/docs/04-api-reference/users.md +333 -0
- package/vendor/docs/05-operations/backup-and-restore.md +342 -0
- package/vendor/docs/05-operations/monitoring.md +285 -0
- package/vendor/docs/05-operations/uninstalling.md +344 -0
- package/vendor/docs/05-operations/upgrades.md +270 -0
- package/vendor/docs/06-reference/config-files.md +1080 -0
- package/vendor/docs/06-reference/glossary.md +528 -0
- package/vendor/docs/06-reference/installer-flags.md +260 -0
- package/vendor/docs/06-reference/ports-and-services.md +244 -0
- package/vendor/docs/06-reference/troubleshooting.md +665 -0
- package/vendor/docs/_index.json +233 -0
- package/vendor/docs/gatekeeper-design.md +953 -0
- package/vendor/docs/package.json +15 -0
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +641 -0
- package/vendor/gatekeeper/dist/boot.d.ts +1 -0
- package/vendor/gatekeeper/dist/boot.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/boot.js +17 -0
- package/vendor/gatekeeper/dist/boot.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/access.d.ts +2 -0
- package/vendor/gatekeeper/dist/cli/commands/access.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/access.js +33 -0
- package/vendor/gatekeeper/dist/cli/commands/access.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.d.ts +2 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.js +110 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.d.ts +2 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.js +98 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/index.d.ts +3 -0
- package/vendor/gatekeeper/dist/cli/index.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/index.js +45 -0
- package/vendor/gatekeeper/dist/cli/index.js.map +1 -0
- package/vendor/gatekeeper/dist/index.d.ts +9 -0
- package/vendor/gatekeeper/dist/index.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/index.js +13 -0
- package/vendor/gatekeeper/dist/index.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/authz.d.ts +16 -0
- package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/authz.js +42 -0
- package/vendor/gatekeeper/dist/lib/authz.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.d.ts +15 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.js +19 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/constants.d.ts +82 -0
- package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/constants.js +79 -0
- package/vendor/gatekeeper/dist/lib/constants.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/grants.d.ts +32 -0
- package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/grants.js +314 -0
- package/vendor/gatekeeper/dist/lib/grants.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/groups.d.ts +10 -0
- package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/groups.js +247 -0
- package/vendor/gatekeeper/dist/lib/groups.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/migration.d.ts +17 -0
- package/vendor/gatekeeper/dist/lib/migration.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/migration.js +79 -0
- package/vendor/gatekeeper/dist/lib/migration.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0001_groups.sql +7 -0
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0002_access_grants.sql +14 -0
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0003_access_request_log.sql +9 -0
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0001_groups.sql +7 -0
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0002_access_grants.sql +14 -0
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0003_access_request_log.sql +9 -0
- package/vendor/gatekeeper/dist/lib/state-db.d.ts +16 -0
- package/vendor/gatekeeper/dist/lib/state-db.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/state-db.js +154 -0
- package/vendor/gatekeeper/dist/lib/state-db.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts +21 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/templates.js +151 -0
- package/vendor/gatekeeper/dist/lib/templates.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts +178 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/types.js +2 -0
- package/vendor/gatekeeper/dist/lib/types.js.map +1 -0
- package/vendor/gatekeeper/dist/server/index.d.ts +15 -0
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/index.js +251 -0
- package/vendor/gatekeeper/dist/server/index.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.d.ts +7 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.js +17 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/authz.js +279 -0
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts +13 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js +249 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/grants.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/grants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/grants.js +112 -0
- package/vendor/gatekeeper/dist/server/routes/grants.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/groups.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/groups.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/groups.js +139 -0
- package/vendor/gatekeeper/dist/server/routes/groups.js.map +1 -0
- package/vendor/gatekeeper/package.json +49 -0
- package/vendor/server/bin/lamaste-server.js +204 -0
- package/vendor/server/package.json +31 -0
- package/vendor/server/src/commands/certs.js +224 -0
- package/vendor/server/src/commands/chisel.js +132 -0
- package/vendor/server/src/commands/logs.js +49 -0
- package/vendor/server/src/commands/plugins.js +321 -0
- package/vendor/server/src/commands/reset-admin.js +478 -0
- package/vendor/server/src/commands/restart.js +94 -0
- package/vendor/server/src/commands/sites.js +257 -0
- package/vendor/server/src/commands/status.js +158 -0
- package/vendor/server/src/commands/tunnels.js +365 -0
- package/vendor/server/src/commands/uninstall.js +326 -0
- package/vendor/server/src/config.js +38 -0
- package/vendor/server/src/exec.js +26 -0
- package/vendor/server/src/logger.js +39 -0
- package/vendor/server/src/ndjson.js +56 -0
- package/vendor/server/src/panel-api.js +85 -0
- package/vendor/server/src/state.js +45 -0
- package/vendor/server-ui/dist/assets/index-B0nWuUyL.js +257 -0
- package/vendor/server-ui/dist/assets/index-BqWWFlTU.css +1 -0
- package/vendor/server-ui/dist/index.html +13 -0
- package/vendor/serverd/package.json +44 -0
- package/vendor/serverd/src/boot.js +13 -0
- package/vendor/serverd/src/index.js +191 -0
- package/vendor/serverd/src/lib/app-error.js +14 -0
- package/vendor/serverd/src/lib/authelia.js +85 -0
- package/vendor/serverd/src/lib/certbot.js +74 -0
- package/vendor/serverd/src/lib/chisel-args.js +6 -0
- package/vendor/serverd/src/lib/chisel-reconcile.js +232 -0
- package/vendor/serverd/src/lib/chisel-runtime.js +46 -0
- package/vendor/serverd/src/lib/chisel-users.js +109 -0
- package/vendor/serverd/src/lib/chisel.js +81 -0
- package/vendor/serverd/src/lib/config.js +190 -0
- package/vendor/serverd/src/lib/constants.js +8 -0
- package/vendor/serverd/src/lib/csr-signing.js +331 -0
- package/vendor/serverd/src/lib/enrollment.js +302 -0
- package/vendor/serverd/src/lib/files.js +19 -0
- package/vendor/serverd/src/lib/gatekeeper-client.js +135 -0
- package/vendor/serverd/src/lib/invite-page.js +281 -0
- package/vendor/serverd/src/lib/ip-utils.js +79 -0
- package/vendor/serverd/src/lib/migrations/state/0001_enrollment_tokens.sql +17 -0
- package/vendor/serverd/src/lib/migrations/state/0002_tickets.sql +70 -0
- package/vendor/serverd/src/lib/migrations/state/0003_plugins.sql +15 -0
- package/vendor/serverd/src/lib/migrations/state/0004_agents.sql +19 -0
- package/vendor/serverd/src/lib/mtls.js +795 -0
- package/vendor/serverd/src/lib/nginx.js +1090 -0
- package/vendor/serverd/src/lib/pki-paths.js +41 -0
- package/vendor/serverd/src/lib/pki-sign.js +103 -0
- package/vendor/serverd/src/lib/plugins.js +538 -0
- package/vendor/serverd/src/lib/push-install.js +390 -0
- package/vendor/serverd/src/lib/revocation.js +112 -0
- package/vendor/serverd/src/lib/services.js +131 -0
- package/vendor/serverd/src/lib/session.js +151 -0
- package/vendor/serverd/src/lib/state-db.js +130 -0
- package/vendor/serverd/src/lib/state.js +140 -0
- package/vendor/serverd/src/lib/storage.js +63 -0
- package/vendor/serverd/src/lib/system-stats.js +58 -0
- package/vendor/serverd/src/lib/tickets.js +1556 -0
- package/vendor/serverd/src/lib/totp.js +76 -0
- package/vendor/serverd/src/lib/tunnel-deps.js +41 -0
- package/vendor/serverd/src/lib/user-access-session.js +142 -0
- package/vendor/serverd/src/lib/user-access.js +52 -0
- package/vendor/serverd/src/middleware/errors.js +63 -0
- package/vendor/serverd/src/middleware/mtls.js +120 -0
- package/vendor/serverd/src/middleware/onboarding-guard.js +30 -0
- package/vendor/serverd/src/middleware/role-guard.js +58 -0
- package/vendor/serverd/src/middleware/twofa-session.js +65 -0
- package/vendor/serverd/src/middleware/user-access-session.js +52 -0
- package/vendor/serverd/src/routes/enrollment.js +120 -0
- package/vendor/serverd/src/routes/health.js +22 -0
- package/vendor/serverd/src/routes/invite.js +163 -0
- package/vendor/serverd/src/routes/management/agents.js +182 -0
- package/vendor/serverd/src/routes/management/certs.js +911 -0
- package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +160 -0
- package/vendor/serverd/src/routes/management/identity.js +107 -0
- package/vendor/serverd/src/routes/management/invitations.js +166 -0
- package/vendor/serverd/src/routes/management/logs.js +218 -0
- package/vendor/serverd/src/routes/management/plugins.js +560 -0
- package/vendor/serverd/src/routes/management/services.js +63 -0
- package/vendor/serverd/src/routes/management/settings.js +325 -0
- package/vendor/serverd/src/routes/management/sites.js +522 -0
- package/vendor/serverd/src/routes/management/storage.js +192 -0
- package/vendor/serverd/src/routes/management/system.js +201 -0
- package/vendor/serverd/src/routes/management/tickets.js +457 -0
- package/vendor/serverd/src/routes/management/tunnels.js +709 -0
- package/vendor/serverd/src/routes/management/users.js +377 -0
- package/vendor/serverd/src/routes/management.js +36 -0
- package/vendor/serverd/src/routes/onboarding/dns.js +101 -0
- package/vendor/serverd/src/routes/onboarding/domain.js +42 -0
- package/vendor/serverd/src/routes/onboarding/index.js +18 -0
- package/vendor/serverd/src/routes/onboarding/provision.js +327 -0
- package/vendor/serverd/src/routes/onboarding/status.js +12 -0
- package/vendor/serverd/src/routes/plugin-router.js +79 -0
- package/vendor/serverd/src/routes/user-access.js +389 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getConfig, updateConfig } from '../../lib/config.js';
|
|
3
|
+
import { generateAdminTotpSecret, verifyTotp } from '../../lib/totp.js';
|
|
4
|
+
import {
|
|
5
|
+
createSessionCookie,
|
|
6
|
+
ensureSessionSecret,
|
|
7
|
+
COOKIE_NAME,
|
|
8
|
+
COOKIE_OPTIONS,
|
|
9
|
+
} from '../../lib/session.js';
|
|
10
|
+
import { disableIpVhost, enableIpVhost } from '../../lib/nginx.js';
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Rate limiting — in-memory, keyed by IP
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
const MAX_ATTEMPTS = 5;
|
|
17
|
+
const WINDOW_MS = 2 * 60 * 1000; // 2 minutes
|
|
18
|
+
const BAN_MS = 5 * 60 * 1000; // 5 minutes
|
|
19
|
+
const MAX_ENTRIES = 1000;
|
|
20
|
+
|
|
21
|
+
/** @type {Map<string, { attempts: number[], bannedUntil: number | null }>} */
|
|
22
|
+
const rateLimitMap = new Map();
|
|
23
|
+
|
|
24
|
+
function checkRateLimit(ip) {
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
const entry = rateLimitMap.get(ip);
|
|
27
|
+
|
|
28
|
+
if (!entry) {
|
|
29
|
+
// Don't allocate until a failed attempt is recorded
|
|
30
|
+
return { allowed: true };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check ban
|
|
34
|
+
if (entry.bannedUntil && now < entry.bannedUntil) {
|
|
35
|
+
const retryAfter = Math.ceil((entry.bannedUntil - now) / 1000);
|
|
36
|
+
return { allowed: false, retryAfter };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Clear expired ban
|
|
40
|
+
if (entry.bannedUntil && now >= entry.bannedUntil) {
|
|
41
|
+
rateLimitMap.delete(ip);
|
|
42
|
+
return { allowed: true };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Prune old attempts outside window
|
|
46
|
+
entry.attempts = entry.attempts.filter((t) => now - t < WINDOW_MS);
|
|
47
|
+
|
|
48
|
+
if (entry.attempts.length === 0) {
|
|
49
|
+
rateLimitMap.delete(ip);
|
|
50
|
+
return { allowed: true };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (entry.attempts.length >= MAX_ATTEMPTS) {
|
|
54
|
+
entry.bannedUntil = now + BAN_MS;
|
|
55
|
+
const retryAfter = Math.ceil(BAN_MS / 1000);
|
|
56
|
+
return { allowed: false, retryAfter };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { allowed: true };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function recordAttempt(ip) {
|
|
63
|
+
let entry = rateLimitMap.get(ip);
|
|
64
|
+
if (!entry) {
|
|
65
|
+
// Sweep stale entries if map grows too large
|
|
66
|
+
if (rateLimitMap.size >= MAX_ENTRIES) {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
for (const [key, val] of rateLimitMap) {
|
|
69
|
+
val.attempts = val.attempts.filter((t) => now - t < WINDOW_MS);
|
|
70
|
+
if (val.attempts.length === 0 && (!val.bannedUntil || now >= val.bannedUntil)) {
|
|
71
|
+
rateLimitMap.delete(key);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// If still at capacity after sweep, evict the oldest entry
|
|
75
|
+
if (rateLimitMap.size >= MAX_ENTRIES) {
|
|
76
|
+
const oldest = rateLimitMap.keys().next().value;
|
|
77
|
+
rateLimitMap.delete(oldest);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
entry = { attempts: [], bannedUntil: null };
|
|
81
|
+
rateLimitMap.set(ip, entry);
|
|
82
|
+
}
|
|
83
|
+
entry.attempts.push(Date.now());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Helper to set the session cookie on a reply
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
function setSessionCookie(request, reply, sessionSecret) {
|
|
91
|
+
const certSerial = request.headers['x-ssl-client-serial'] || undefined;
|
|
92
|
+
const session = createSessionCookie(sessionSecret, certSerial);
|
|
93
|
+
reply.setCookie(COOKIE_NAME, session.value, {
|
|
94
|
+
...COOKIE_OPTIONS,
|
|
95
|
+
maxAge: session.maxAge,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Shared TOTP validation: rate limit + parse + verify
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
const CodeSchema = z.object({
|
|
104
|
+
code: z.string().regex(/^\d{6}$/, 'Code must be exactly 6 digits'),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Common rate-limit + code-parse + TOTP-verify sequence.
|
|
109
|
+
* Returns the parsed code on success, or sends an error reply and returns null.
|
|
110
|
+
*/
|
|
111
|
+
async function validateTotpCode(request, reply) {
|
|
112
|
+
const ip = request.ip;
|
|
113
|
+
const limit = checkRateLimit(ip);
|
|
114
|
+
if (!limit.allowed) {
|
|
115
|
+
reply.code(429).send({
|
|
116
|
+
error: 'Too many attempts. Try again later.',
|
|
117
|
+
retryAfter: limit.retryAfter,
|
|
118
|
+
});
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let body;
|
|
123
|
+
try {
|
|
124
|
+
body = CodeSchema.parse(request.body);
|
|
125
|
+
} catch {
|
|
126
|
+
reply.code(400).send({ error: 'Code must be exactly 6 digits' });
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const config = getConfig();
|
|
130
|
+
|
|
131
|
+
if (!config.panel2fa?.secret) {
|
|
132
|
+
reply.code(400).send({ error: '2FA secret not configured' });
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!verifyTotp(config.panel2fa.secret, body.code)) {
|
|
137
|
+
recordAttempt(ip);
|
|
138
|
+
reply.code(401).send({ error: 'Invalid TOTP code' });
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return body.code;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Routes
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
export default async function settingsRoutes(fastify, _opts) {
|
|
150
|
+
// GET /settings/2fa — return current 2FA status (exempt from 2FA session)
|
|
151
|
+
fastify.get(
|
|
152
|
+
'/settings/2fa',
|
|
153
|
+
{ preHandler: fastify.requireRole(['admin']) },
|
|
154
|
+
async (_request, _reply) => {
|
|
155
|
+
const config = getConfig();
|
|
156
|
+
const panel2fa = config.panel2fa || { enabled: false, setupComplete: false };
|
|
157
|
+
return {
|
|
158
|
+
enabled: panel2fa.enabled,
|
|
159
|
+
setupComplete: panel2fa.setupComplete,
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// POST /settings/2fa/setup — generate TOTP secret
|
|
165
|
+
fastify.post(
|
|
166
|
+
'/settings/2fa/setup',
|
|
167
|
+
{ preHandler: fastify.requireRole(['admin']) },
|
|
168
|
+
async (_request, reply) => {
|
|
169
|
+
const config = getConfig();
|
|
170
|
+
|
|
171
|
+
if (!config.domain) {
|
|
172
|
+
return reply.code(400).send({
|
|
173
|
+
error: 'Domain must be configured before enabling 2FA',
|
|
174
|
+
details: {
|
|
175
|
+
hint: 'Enabling 2FA disables IP:9292 access. A domain is required.',
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (config.panel2fa?.enabled && config.panel2fa?.setupComplete) {
|
|
181
|
+
return reply.code(409).send({
|
|
182
|
+
error: '2FA is already enabled',
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const { secret, uri } = generateAdminTotpSecret();
|
|
187
|
+
|
|
188
|
+
await updateConfig({
|
|
189
|
+
panel2fa: {
|
|
190
|
+
enabled: false,
|
|
191
|
+
secret,
|
|
192
|
+
setupComplete: false,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return { uri, manualKey: secret };
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// POST /settings/2fa/confirm — verify initial code and enable 2FA
|
|
201
|
+
fastify.post(
|
|
202
|
+
'/settings/2fa/confirm',
|
|
203
|
+
{
|
|
204
|
+
preHandler: fastify.requireRole(['admin']),
|
|
205
|
+
// Strict tier — TOTP confirmation is auth-adjacent. The in-file
|
|
206
|
+
// checkRateLimit/recordAttempt path also throttles per-IP failed
|
|
207
|
+
// attempts, but the global Fastify limiter caps total request
|
|
208
|
+
// volume regardless of TOTP outcome.
|
|
209
|
+
config: { rateLimit: { max: 5, timeWindow: '1 minute' } },
|
|
210
|
+
},
|
|
211
|
+
async (request, reply) => {
|
|
212
|
+
const config = getConfig();
|
|
213
|
+
|
|
214
|
+
if (!config.panel2fa?.secret) {
|
|
215
|
+
return reply.code(400).send({
|
|
216
|
+
error: 'No 2FA setup in progress. Call POST /settings/2fa/setup first.',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (config.panel2fa.enabled && config.panel2fa.setupComplete) {
|
|
221
|
+
return reply.code(409).send({ error: '2FA is already enabled' });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const code = await validateTotpCode(request, reply);
|
|
225
|
+
if (code === null) return;
|
|
226
|
+
|
|
227
|
+
// Enable 2FA
|
|
228
|
+
const sessionSecret = await ensureSessionSecret();
|
|
229
|
+
|
|
230
|
+
await updateConfig({
|
|
231
|
+
panel2fa: {
|
|
232
|
+
enabled: true,
|
|
233
|
+
secret: config.panel2fa.secret,
|
|
234
|
+
setupComplete: true,
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Disable IP vhost
|
|
239
|
+
try {
|
|
240
|
+
await disableIpVhost();
|
|
241
|
+
} catch (err) {
|
|
242
|
+
// Rollback 2FA on nginx failure
|
|
243
|
+
await updateConfig({
|
|
244
|
+
panel2fa: { enabled: false, secret: null, setupComplete: false },
|
|
245
|
+
});
|
|
246
|
+
request.log.error({ err }, 'Failed to disable IP vhost during 2FA enable');
|
|
247
|
+
return reply.code(500).send({
|
|
248
|
+
error: 'Failed to disable IP vhost. 2FA was not enabled.',
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Issue session cookie
|
|
253
|
+
setSessionCookie(request, reply, sessionSecret);
|
|
254
|
+
|
|
255
|
+
return { enabled: true };
|
|
256
|
+
},
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// POST /settings/2fa/verify — verify code and issue session (exempt from 2FA session)
|
|
260
|
+
fastify.post(
|
|
261
|
+
'/settings/2fa/verify',
|
|
262
|
+
{
|
|
263
|
+
preHandler: fastify.requireRole(['admin']),
|
|
264
|
+
// Strict tier — direct TOTP brute-force surface. The TOTP-specific
|
|
265
|
+
// ban after 5 invalid codes still applies; this cap bounds the
|
|
266
|
+
// total request volume per IP.
|
|
267
|
+
config: { rateLimit: { max: 5, timeWindow: '1 minute' } },
|
|
268
|
+
},
|
|
269
|
+
async (request, reply) => {
|
|
270
|
+
const config = getConfig();
|
|
271
|
+
|
|
272
|
+
if (!config.panel2fa?.enabled || !config.panel2fa?.secret) {
|
|
273
|
+
return reply.code(400).send({ error: '2FA is not enabled' });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const code = await validateTotpCode(request, reply);
|
|
277
|
+
if (code === null) return;
|
|
278
|
+
|
|
279
|
+
const sessionSecret = await ensureSessionSecret();
|
|
280
|
+
setSessionCookie(request, reply, sessionSecret);
|
|
281
|
+
|
|
282
|
+
return { verified: true };
|
|
283
|
+
},
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// POST /settings/2fa/disable — disable 2FA (requires valid session + code)
|
|
287
|
+
fastify.post(
|
|
288
|
+
'/settings/2fa/disable',
|
|
289
|
+
{
|
|
290
|
+
preHandler: fastify.requireRole(['admin']),
|
|
291
|
+
// Strict tier — disabling 2FA weakens auth posture; treat with the
|
|
292
|
+
// same ceiling as the other 2FA endpoints.
|
|
293
|
+
config: { rateLimit: { max: 5, timeWindow: '1 minute' } },
|
|
294
|
+
},
|
|
295
|
+
async (request, reply) => {
|
|
296
|
+
const config = getConfig();
|
|
297
|
+
|
|
298
|
+
if (!config.panel2fa?.enabled || !config.panel2fa?.secret) {
|
|
299
|
+
return reply.code(400).send({ error: '2FA is not enabled' });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const code = await validateTotpCode(request, reply);
|
|
303
|
+
if (code === null) return;
|
|
304
|
+
|
|
305
|
+
// Disable 2FA
|
|
306
|
+
await updateConfig({
|
|
307
|
+
panel2fa: { enabled: false, secret: null, setupComplete: false },
|
|
308
|
+
sessionSecret: null,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Re-enable IP vhost
|
|
312
|
+
try {
|
|
313
|
+
await enableIpVhost();
|
|
314
|
+
} catch (err) {
|
|
315
|
+
request.log.error({ err }, 'Failed to re-enable IP vhost during 2FA disable');
|
|
316
|
+
// 2FA is already disabled in config — warn but don't fail
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Clear session cookie
|
|
320
|
+
reply.clearCookie(COOKIE_NAME, COOKIE_OPTIONS);
|
|
321
|
+
|
|
322
|
+
return { enabled: false };
|
|
323
|
+
},
|
|
324
|
+
);
|
|
325
|
+
}
|