@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,377 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import {
|
|
3
|
+
readUsers,
|
|
4
|
+
readUsersRaw,
|
|
5
|
+
writeUsers,
|
|
6
|
+
reloadAuthelia,
|
|
7
|
+
hashPassword,
|
|
8
|
+
generateTotpSecret,
|
|
9
|
+
writeTotpToDatabase,
|
|
10
|
+
} from '../../lib/authelia.js';
|
|
11
|
+
import { removeGrantsForUser } from '../../lib/user-access.js';
|
|
12
|
+
import { bumpUserEpoch } from '../../lib/config.js';
|
|
13
|
+
import { gatekeeperRequest } from '../../lib/gatekeeper-client.js';
|
|
14
|
+
|
|
15
|
+
const CreateUserSchema = z.object({
|
|
16
|
+
username: z
|
|
17
|
+
.string()
|
|
18
|
+
.min(2)
|
|
19
|
+
.max(32)
|
|
20
|
+
.regex(
|
|
21
|
+
/^[a-z0-9][a-z0-9_-]*$/,
|
|
22
|
+
'Username must start with a lowercase letter or digit and may contain underscores or hyphens',
|
|
23
|
+
),
|
|
24
|
+
displayname: z.string().min(1).max(100),
|
|
25
|
+
email: z.string().email(),
|
|
26
|
+
password: z.string().min(8).max(128),
|
|
27
|
+
groups: z.array(z.string()).optional().default([]),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const UsernameParamSchema = z.object({
|
|
31
|
+
username: z
|
|
32
|
+
.string()
|
|
33
|
+
.min(2)
|
|
34
|
+
.max(32)
|
|
35
|
+
.regex(/^[a-z0-9][a-z0-9_-]*$/, 'Invalid username format'),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const UpdateUserSchema = z
|
|
39
|
+
.object({
|
|
40
|
+
displayname: z.string().min(1).max(100).optional(),
|
|
41
|
+
email: z.string().email().optional(),
|
|
42
|
+
password: z.string().min(8).max(128).optional(),
|
|
43
|
+
groups: z.array(z.string()).optional(),
|
|
44
|
+
})
|
|
45
|
+
.refine((data) => Object.keys(data).length > 0, {
|
|
46
|
+
message: 'At least one field must be provided',
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export default async function usersRoutes(fastify, _opts) {
|
|
50
|
+
// GET /api/users — list all users (no sensitive fields)
|
|
51
|
+
fastify.get(
|
|
52
|
+
'/users',
|
|
53
|
+
{
|
|
54
|
+
preHandler: fastify.requireRole(['admin']),
|
|
55
|
+
},
|
|
56
|
+
async (request, reply) => {
|
|
57
|
+
try {
|
|
58
|
+
const users = await readUsers();
|
|
59
|
+
const sorted = users.sort((a, b) => a.username.localeCompare(b.username));
|
|
60
|
+
return { users: sorted };
|
|
61
|
+
} catch (err) {
|
|
62
|
+
request.log.error(err, 'Failed to read user database');
|
|
63
|
+
return reply.code(500).send({ error: 'Failed to read user database' });
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// POST /api/users — create a new user
|
|
69
|
+
fastify.post(
|
|
70
|
+
'/users',
|
|
71
|
+
{
|
|
72
|
+
preHandler: fastify.requireRole(['admin']),
|
|
73
|
+
// Strict tier — user creation hashes a fresh password and provisions
|
|
74
|
+
// an identity. Brute-forcing this surface is cheap if uncapped.
|
|
75
|
+
config: { rateLimit: { max: 5, timeWindow: '1 minute' } },
|
|
76
|
+
},
|
|
77
|
+
async (request, reply) => {
|
|
78
|
+
const body = CreateUserSchema.parse(request.body);
|
|
79
|
+
|
|
80
|
+
let usersData;
|
|
81
|
+
try {
|
|
82
|
+
usersData = await readUsersRaw();
|
|
83
|
+
} catch (err) {
|
|
84
|
+
request.log.error(err, 'Failed to read user database');
|
|
85
|
+
return reply.code(500).send({ error: 'Failed to read user database' });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (usersData.users[body.username]) {
|
|
89
|
+
return reply.code(409).send({ error: 'Username already exists' });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let hash;
|
|
93
|
+
try {
|
|
94
|
+
hash = await hashPassword(body.password);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
request.log.error(err, 'Failed to hash password');
|
|
97
|
+
return reply.code(500).send({ error: 'Failed to hash password' });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
usersData.users[body.username] = {
|
|
101
|
+
displayname: body.displayname,
|
|
102
|
+
email: body.email,
|
|
103
|
+
password: hash,
|
|
104
|
+
groups: body.groups,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
await writeUsers(usersData);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
request.log.error(err, 'Failed to update user database');
|
|
111
|
+
return reply.code(500).send({ error: 'Failed to update user database' });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await reloadAuthelia();
|
|
116
|
+
} catch (err) {
|
|
117
|
+
request.log.warn(err, 'Failed to reload Authelia after user creation');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return reply.code(201).send({
|
|
121
|
+
ok: true,
|
|
122
|
+
user: {
|
|
123
|
+
username: body.username,
|
|
124
|
+
displayname: body.displayname,
|
|
125
|
+
email: body.email,
|
|
126
|
+
groups: body.groups,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// PUT /api/users/:username — update a user
|
|
133
|
+
fastify.put(
|
|
134
|
+
'/users/:username',
|
|
135
|
+
{
|
|
136
|
+
preHandler: fastify.requireRole(['admin']),
|
|
137
|
+
// Strict tier — body may include a fresh password that we re-hash;
|
|
138
|
+
// treat as auth-adjacent and bound to the same 5/min ceiling as
|
|
139
|
+
// creation.
|
|
140
|
+
config: { rateLimit: { max: 5, timeWindow: '1 minute' } },
|
|
141
|
+
},
|
|
142
|
+
async (request, reply) => {
|
|
143
|
+
const { username } = UsernameParamSchema.parse(request.params);
|
|
144
|
+
const body = UpdateUserSchema.parse(request.body);
|
|
145
|
+
|
|
146
|
+
let usersData;
|
|
147
|
+
try {
|
|
148
|
+
usersData = await readUsersRaw();
|
|
149
|
+
} catch (err) {
|
|
150
|
+
request.log.error(err, 'Failed to read user database');
|
|
151
|
+
return reply.code(500).send({ error: 'Failed to read user database' });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!usersData.users[username]) {
|
|
155
|
+
return reply.code(404).send({ error: 'User not found' });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const user = usersData.users[username];
|
|
159
|
+
|
|
160
|
+
if (body.displayname !== undefined) {
|
|
161
|
+
user.displayname = body.displayname;
|
|
162
|
+
}
|
|
163
|
+
if (body.email !== undefined) {
|
|
164
|
+
user.email = body.email;
|
|
165
|
+
}
|
|
166
|
+
if (body.groups !== undefined) {
|
|
167
|
+
user.groups = body.groups;
|
|
168
|
+
}
|
|
169
|
+
if (body.password !== undefined) {
|
|
170
|
+
try {
|
|
171
|
+
user.password = await hashPassword(body.password);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
request.log.error(err, 'Failed to hash password');
|
|
174
|
+
return reply.code(500).send({ error: 'Failed to hash password' });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
await writeUsers(usersData);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
request.log.error(err, 'Failed to update user database');
|
|
182
|
+
return reply.code(500).send({ error: 'Failed to update user database' });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
await reloadAuthelia();
|
|
187
|
+
} catch (err) {
|
|
188
|
+
request.log.warn(err, 'Failed to reload Authelia after user update');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
ok: true,
|
|
193
|
+
user: {
|
|
194
|
+
username,
|
|
195
|
+
displayname: user.displayname,
|
|
196
|
+
email: user.email,
|
|
197
|
+
groups: user.groups || [],
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// DELETE /api/users/:username — delete a user
|
|
204
|
+
fastify.delete(
|
|
205
|
+
'/users/:username',
|
|
206
|
+
{
|
|
207
|
+
preHandler: fastify.requireRole(['admin']),
|
|
208
|
+
// Moderate tier — destructive management write; the cascade also
|
|
209
|
+
// fires off gatekeeper/cleanup work, so we cap concurrency to avoid
|
|
210
|
+
// hammering downstream services.
|
|
211
|
+
config: { rateLimit: { max: 30, timeWindow: '1 minute' } },
|
|
212
|
+
},
|
|
213
|
+
async (request, reply) => {
|
|
214
|
+
const { username } = UsernameParamSchema.parse(request.params);
|
|
215
|
+
|
|
216
|
+
let usersData;
|
|
217
|
+
try {
|
|
218
|
+
usersData = await readUsersRaw();
|
|
219
|
+
} catch (err) {
|
|
220
|
+
request.log.error(err, 'Failed to read user database');
|
|
221
|
+
return reply.code(500).send({ error: 'Failed to read user database' });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!usersData.users[username]) {
|
|
225
|
+
return reply.code(404).send({ error: 'User not found' });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const userCount = Object.keys(usersData.users).length;
|
|
229
|
+
if (userCount <= 1) {
|
|
230
|
+
return reply.code(400).send({ error: 'Cannot delete the last user' });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
delete usersData.users[username];
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
await writeUsers(usersData);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
request.log.error(err, 'Failed to update user database');
|
|
239
|
+
return reply.code(500).send({ error: 'Failed to update user database' });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
await reloadAuthelia();
|
|
244
|
+
} catch (err) {
|
|
245
|
+
request.log.warn(err, 'Failed to reload Authelia after user deletion');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---------------------------------------------------------------
|
|
249
|
+
// Cascade cleanup. The user is already gone from users.yml; from
|
|
250
|
+
// here on we never roll back. Failures collect into `warnings` so
|
|
251
|
+
// the admin sees a partial-success response and can investigate.
|
|
252
|
+
// ---------------------------------------------------------------
|
|
253
|
+
const warnings = [];
|
|
254
|
+
|
|
255
|
+
// 1. Legacy user-plugin-access.json grants (pre-gatekeeper migration)
|
|
256
|
+
try {
|
|
257
|
+
const removed = await removeGrantsForUser(username, request.log);
|
|
258
|
+
if (removed > 0) {
|
|
259
|
+
request.log.info(
|
|
260
|
+
{ username, removed },
|
|
261
|
+
'Cascade: removed legacy user-plugin-access grants',
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
} catch (err) {
|
|
265
|
+
request.log.warn(
|
|
266
|
+
{ err, username },
|
|
267
|
+
'Cascade: failed to remove legacy user-plugin-access grants',
|
|
268
|
+
);
|
|
269
|
+
warnings.push({
|
|
270
|
+
step: 'user-plugin-access',
|
|
271
|
+
message: `Failed to remove legacy grants: ${err.message}`,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 2. Gatekeeper grants — single bulk-delete keyed by principal
|
|
276
|
+
try {
|
|
277
|
+
const result = await gatekeeperRequest(
|
|
278
|
+
'DELETE',
|
|
279
|
+
`/api/grants?principalType=user&principalId=${encodeURIComponent(username)}`,
|
|
280
|
+
);
|
|
281
|
+
if (result.ok) {
|
|
282
|
+
const removed =
|
|
283
|
+
result.data && typeof result.data === 'object' && 'removed' in result.data
|
|
284
|
+
? result.data.removed
|
|
285
|
+
: 0;
|
|
286
|
+
if (removed > 0) {
|
|
287
|
+
request.log.info(
|
|
288
|
+
{ username, removed },
|
|
289
|
+
'Cascade: removed gatekeeper grants for deleted user',
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
} else if (result.reason === 'unreachable' || result.reason === 'no-secret') {
|
|
293
|
+
request.log.warn(
|
|
294
|
+
{ username, reason: result.reason },
|
|
295
|
+
'Cascade: gatekeeper unreachable — grants for deleted user remain in access-grants.json',
|
|
296
|
+
);
|
|
297
|
+
warnings.push({
|
|
298
|
+
step: 'gatekeeper',
|
|
299
|
+
message:
|
|
300
|
+
'Gatekeeper service was unreachable. Re-run grant cleanup once it is back online — grants for this user may still exist in access-grants.json.',
|
|
301
|
+
});
|
|
302
|
+
} else {
|
|
303
|
+
request.log.warn(
|
|
304
|
+
{ username, statusCode: result.statusCode, error: result.error },
|
|
305
|
+
'Cascade: gatekeeper returned an error',
|
|
306
|
+
);
|
|
307
|
+
warnings.push({
|
|
308
|
+
step: 'gatekeeper',
|
|
309
|
+
message: `Gatekeeper returned HTTP ${result.statusCode}: ${result.error}`,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
} catch (err) {
|
|
313
|
+
request.log.warn({ err, username }, 'Cascade: unexpected failure calling gatekeeper');
|
|
314
|
+
warnings.push({
|
|
315
|
+
step: 'gatekeeper',
|
|
316
|
+
message: `Unexpected failure: ${err.message}`,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 3. Live user-access sessions — bump per-user epoch in panel.json so
|
|
321
|
+
// middleware rejects every session token with iat < epoch.
|
|
322
|
+
try {
|
|
323
|
+
const epoch = await bumpUserEpoch(username);
|
|
324
|
+
request.log.info({ username, epoch }, 'Cascade: bumped user-access session epoch');
|
|
325
|
+
} catch (err) {
|
|
326
|
+
request.log.warn(
|
|
327
|
+
{ err, username },
|
|
328
|
+
'Cascade: failed to bump user-access session epoch — old session tokens may still validate',
|
|
329
|
+
);
|
|
330
|
+
warnings.push({
|
|
331
|
+
step: 'session-epoch',
|
|
332
|
+
message: `Failed to invalidate live sessions: ${err.message}. Old session tokens for this username may still validate until their natural expiry.`,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const response = { ok: true };
|
|
337
|
+
if (warnings.length > 0) {
|
|
338
|
+
response.warnings = warnings;
|
|
339
|
+
}
|
|
340
|
+
return response;
|
|
341
|
+
},
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
// POST /api/users/:username/reset-totp — generate a new TOTP secret
|
|
345
|
+
fastify.post(
|
|
346
|
+
'/users/:username/reset-totp',
|
|
347
|
+
{
|
|
348
|
+
preHandler: fastify.requireRole(['admin']),
|
|
349
|
+
},
|
|
350
|
+
async (request, reply) => {
|
|
351
|
+
const { username } = UsernameParamSchema.parse(request.params);
|
|
352
|
+
|
|
353
|
+
let usersData;
|
|
354
|
+
try {
|
|
355
|
+
usersData = await readUsersRaw();
|
|
356
|
+
} catch (err) {
|
|
357
|
+
request.log.error(err, 'Failed to read user database');
|
|
358
|
+
return reply.code(500).send({ error: 'Failed to read user database' });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!usersData.users[username]) {
|
|
362
|
+
return reply.code(404).send({ error: 'User not found' });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const { secret, uri } = generateTotpSecret(username);
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
await writeTotpToDatabase(username, secret);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
request.log.error(err, 'Failed to write TOTP to Authelia database');
|
|
371
|
+
return reply.code(500).send({ error: 'Failed to write TOTP configuration' });
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return { ok: true, totpUri: uri };
|
|
375
|
+
},
|
|
376
|
+
);
|
|
377
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { managementOnly } from '../middleware/onboarding-guard.js';
|
|
2
|
+
import systemRoutes from './management/system.js';
|
|
3
|
+
import servicesRoutes from './management/services.js';
|
|
4
|
+
import logsRoutes from './management/logs.js';
|
|
5
|
+
import usersRoutes from './management/users.js';
|
|
6
|
+
import certsRoutes from './management/certs.js';
|
|
7
|
+
import tunnelRoutes from './management/tunnels.js';
|
|
8
|
+
import sitesRoutes from './management/sites.js';
|
|
9
|
+
import invitationRoutes from './management/invitations.js';
|
|
10
|
+
import pluginRoutes from './management/plugins.js';
|
|
11
|
+
import settingsRoutes from './management/settings.js';
|
|
12
|
+
import ticketRoutes from './management/tickets.js';
|
|
13
|
+
import identityRoutes from './management/identity.js';
|
|
14
|
+
import storageRoutes from './management/storage.js';
|
|
15
|
+
import gatekeeperProxyRoutes from './management/gatekeeper-proxy.js';
|
|
16
|
+
import agentsRoutes from './management/agents.js';
|
|
17
|
+
|
|
18
|
+
export default async function managementRoutes(fastify, _opts) {
|
|
19
|
+
fastify.addHook('onRequest', managementOnly());
|
|
20
|
+
|
|
21
|
+
await fastify.register(tunnelRoutes);
|
|
22
|
+
await fastify.register(sitesRoutes);
|
|
23
|
+
await fastify.register(systemRoutes);
|
|
24
|
+
await fastify.register(servicesRoutes);
|
|
25
|
+
await fastify.register(logsRoutes);
|
|
26
|
+
await fastify.register(usersRoutes);
|
|
27
|
+
await fastify.register(certsRoutes);
|
|
28
|
+
await fastify.register(invitationRoutes);
|
|
29
|
+
await fastify.register(pluginRoutes);
|
|
30
|
+
await fastify.register(settingsRoutes);
|
|
31
|
+
await fastify.register(ticketRoutes);
|
|
32
|
+
await fastify.register(identityRoutes);
|
|
33
|
+
await fastify.register(storageRoutes);
|
|
34
|
+
await fastify.register(gatekeeperProxyRoutes);
|
|
35
|
+
await fastify.register(agentsRoutes);
|
|
36
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import dns from 'node:dns/promises';
|
|
2
|
+
import { CORE_SUBDOMAINS } from '@lamalibre/lamaste';
|
|
3
|
+
import { getConfig, updateConfig } from '../../lib/config.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve A records for a hostname, returning an empty array on expected DNS errors.
|
|
7
|
+
*/
|
|
8
|
+
async function resolveA(hostname) {
|
|
9
|
+
try {
|
|
10
|
+
return await dns.resolve4(hostname);
|
|
11
|
+
} catch (err) {
|
|
12
|
+
if (err.code === 'ENOTFOUND' || err.code === 'ENODATA' || err.code === 'ETIMEOUT') {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
throw err;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build a human-readable diagnostic message for the DNS verification result.
|
|
21
|
+
*/
|
|
22
|
+
function buildMessage({ domain, expectedIp, records, wildcardOk, apexOk }) {
|
|
23
|
+
const failing = records.filter((r) => !r.ok);
|
|
24
|
+
if (failing.length > 0) {
|
|
25
|
+
const details = failing
|
|
26
|
+
.map((r) =>
|
|
27
|
+
r.resolvedIps.length > 0
|
|
28
|
+
? `${r.name} resolves to ${r.resolvedIps.join(', ')}`
|
|
29
|
+
: `${r.name} does not resolve`,
|
|
30
|
+
)
|
|
31
|
+
.join('; ');
|
|
32
|
+
return `${details}. Point ${failing.map((r) => r.name).join(', ')} (or a wildcard *.${domain}) at ${expectedIp}. DNS propagation can take up to 48 hours, but usually completes within minutes.`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const notes = [];
|
|
36
|
+
if (!wildcardOk) {
|
|
37
|
+
notes.push(
|
|
38
|
+
'Wildcard DNS is not configured — you will need to add individual subdomain records for each tunnel.',
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (!apexOk) {
|
|
42
|
+
notes.push(
|
|
43
|
+
`${domain} itself does not point to this server — that is only needed to serve a site on it.`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return ['DNS is correctly configured. panel, auth and tunnel resolve to your server.', ...notes]
|
|
47
|
+
.join(' ')
|
|
48
|
+
.trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default async function dnsRoute(fastify, _opts) {
|
|
52
|
+
fastify.post('/verify-dns', async (request, reply) => {
|
|
53
|
+
const config = getConfig();
|
|
54
|
+
const { status } = config.onboarding;
|
|
55
|
+
|
|
56
|
+
if (status !== 'DOMAIN_SET' && status !== 'DNS_READY') {
|
|
57
|
+
return reply.code(409).send({
|
|
58
|
+
error: 'Domain must be set before DNS verification',
|
|
59
|
+
onboardingStatus: status,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { domain, ip: expectedIp } = config;
|
|
64
|
+
|
|
65
|
+
// Onboarding issues certificates for the core subdomains only, so those
|
|
66
|
+
// are the names that must resolve here. The apex is reported but not
|
|
67
|
+
// required: it may still belong to another host until a site is put on it.
|
|
68
|
+
const coreNames = CORE_SUBDOMAINS.map((sub) => `${sub}.${domain}`);
|
|
69
|
+
const [coreResolved, apexResolvedIps, wildcardResolvedIps] = await Promise.all([
|
|
70
|
+
Promise.all(coreNames.map((name) => resolveA(name))),
|
|
71
|
+
resolveA(domain),
|
|
72
|
+
resolveA(`test-lamaste-check.${domain}`),
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
const records = coreNames.map((name, i) => {
|
|
76
|
+
const resolvedIps = coreResolved[i] ?? [];
|
|
77
|
+
return { name, resolvedIps, ok: resolvedIps.includes(expectedIp) };
|
|
78
|
+
});
|
|
79
|
+
const ok = records.every((r) => r.ok);
|
|
80
|
+
const apexOk = apexResolvedIps.includes(expectedIp);
|
|
81
|
+
const wildcardOk = wildcardResolvedIps.includes(expectedIp);
|
|
82
|
+
|
|
83
|
+
if (ok) {
|
|
84
|
+
await updateConfig({ onboarding: { status: 'DNS_READY' } });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const message = buildMessage({ domain, expectedIp, records, wildcardOk, apexOk });
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
ok,
|
|
91
|
+
domain,
|
|
92
|
+
expectedIp,
|
|
93
|
+
records,
|
|
94
|
+
apexOk,
|
|
95
|
+
apexResolvedIps,
|
|
96
|
+
wildcardOk,
|
|
97
|
+
wildcardResolvedIps,
|
|
98
|
+
message,
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { LETSENCRYPT_EMAIL_REGEX } from '@lamalibre/lamaste';
|
|
3
|
+
import { getConfig, updateConfig } from '../../lib/config.js';
|
|
4
|
+
|
|
5
|
+
const DomainSchema = z.object({
|
|
6
|
+
domain: z
|
|
7
|
+
.string()
|
|
8
|
+
.min(1)
|
|
9
|
+
.regex(/^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/)
|
|
10
|
+
.describe('Fully qualified domain name'),
|
|
11
|
+
email: z
|
|
12
|
+
.string()
|
|
13
|
+
.regex(
|
|
14
|
+
LETSENCRYPT_EMAIL_REGEX,
|
|
15
|
+
'Enter an email address that starts with a letter or digit, e.g. admin@example.com',
|
|
16
|
+
)
|
|
17
|
+
.describe("Email for Let's Encrypt registration"),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export default async function domainRoute(fastify, _opts) {
|
|
21
|
+
fastify.post('/domain', async (request, reply) => {
|
|
22
|
+
const body = DomainSchema.parse(request.body);
|
|
23
|
+
|
|
24
|
+
const config = getConfig();
|
|
25
|
+
const { status } = config.onboarding;
|
|
26
|
+
|
|
27
|
+
if (status !== 'FRESH' && status !== 'DOMAIN_SET') {
|
|
28
|
+
return reply.code(409).send({
|
|
29
|
+
error: 'Cannot change domain in current state',
|
|
30
|
+
onboardingStatus: status,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await updateConfig({
|
|
35
|
+
domain: body.domain,
|
|
36
|
+
email: body.email,
|
|
37
|
+
onboarding: { status: 'DOMAIN_SET' },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return { ok: true, domain: body.domain, email: body.email };
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { onboardingOnly } from '../../middleware/onboarding-guard.js';
|
|
2
|
+
import statusRoute from './status.js';
|
|
3
|
+
import domainRoute from './domain.js';
|
|
4
|
+
import dnsRoute from './dns.js';
|
|
5
|
+
import provisionRoute from './provision.js';
|
|
6
|
+
|
|
7
|
+
export default async function onboardingRoutes(fastify, _opts) {
|
|
8
|
+
// Status endpoint is always accessible — no guard
|
|
9
|
+
await fastify.register(statusRoute);
|
|
10
|
+
|
|
11
|
+
// All other onboarding routes are guarded: return 410 after onboarding completes
|
|
12
|
+
await fastify.register(async function guarded(app) {
|
|
13
|
+
app.addHook('onRequest', onboardingOnly());
|
|
14
|
+
await app.register(domainRoute);
|
|
15
|
+
await app.register(dnsRoute);
|
|
16
|
+
await app.register(provisionRoute);
|
|
17
|
+
});
|
|
18
|
+
}
|