@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,522 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static site management routes.
|
|
3
|
+
*
|
|
4
|
+
* Create/delete/update/verifyDns delegate to @lamalibre/lamaste/server core functions.
|
|
5
|
+
* File management endpoints (list, upload, delete) and HTTP-specific auth checks remain here.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import {
|
|
9
|
+
createSite,
|
|
10
|
+
deleteSite,
|
|
11
|
+
updateSite,
|
|
12
|
+
verifyDns,
|
|
13
|
+
SiteError,
|
|
14
|
+
} from '@lamalibre/lamaste/server';
|
|
15
|
+
import { getConfig } from '../../lib/config.js';
|
|
16
|
+
import { readSites, writeSites, readTunnels } from '../../lib/state.js';
|
|
17
|
+
import { writeStaticSiteVhost, removeStaticSiteVhost } from '../../lib/nginx.js';
|
|
18
|
+
import { updateAccessControl } from '../../lib/authelia.js';
|
|
19
|
+
import { issueTunnelCert, getCertPath, issueSiteCert } from '../../lib/certbot.js';
|
|
20
|
+
import {
|
|
21
|
+
createSiteDirectory,
|
|
22
|
+
removeSiteDirectory,
|
|
23
|
+
listFiles,
|
|
24
|
+
saveUploadedFile,
|
|
25
|
+
deleteFile,
|
|
26
|
+
getSiteSize,
|
|
27
|
+
validatePath,
|
|
28
|
+
validateFileExtension,
|
|
29
|
+
getSiteRoot,
|
|
30
|
+
} from '../../lib/files.js';
|
|
31
|
+
|
|
32
|
+
const IdParamSchema = z.object({ id: z.string().uuid() });
|
|
33
|
+
|
|
34
|
+
const AliasSchema = z
|
|
35
|
+
.string()
|
|
36
|
+
.max(253, 'Alias must be at most 253 characters')
|
|
37
|
+
.regex(/^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/, 'Invalid alias hostname');
|
|
38
|
+
|
|
39
|
+
const CreateSiteSchema = z.object({
|
|
40
|
+
name: z
|
|
41
|
+
.string()
|
|
42
|
+
.min(1, 'Name is required')
|
|
43
|
+
.max(100, 'Name must be at most 100 characters')
|
|
44
|
+
.regex(
|
|
45
|
+
/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/,
|
|
46
|
+
'Name must be lowercase alphanumeric with optional hyphens, cannot start or end with a hyphen',
|
|
47
|
+
),
|
|
48
|
+
type: z.enum(['managed', 'custom']),
|
|
49
|
+
customDomain: z
|
|
50
|
+
.string()
|
|
51
|
+
.max(253, 'Domain must be at most 253 characters')
|
|
52
|
+
.regex(/^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/, 'Invalid domain format')
|
|
53
|
+
.optional(),
|
|
54
|
+
aliases: z.array(AliasSchema).max(10, 'At most 10 aliases').optional(),
|
|
55
|
+
spaMode: z.boolean().optional().default(false),
|
|
56
|
+
autheliaProtected: z.boolean().optional().default(false),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const UpdateSiteSchema = z.object({
|
|
60
|
+
spaMode: z.boolean().optional(),
|
|
61
|
+
autheliaProtected: z.boolean().optional(),
|
|
62
|
+
allowedUsers: z.array(z.string().min(1)).optional(),
|
|
63
|
+
aliases: z.array(AliasSchema).max(10, 'At most 10 aliases').optional(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const DeleteFileSchema = z.object({
|
|
67
|
+
path: z.string().min(1, 'Path is required'),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const PathQuerySchema = z.object({
|
|
71
|
+
path: z.string().max(500).optional().default('.'),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// `z.coerce.number()` lets browser query strings pass `?limit=50` without
|
|
75
|
+
// requiring callers to JSON-encode their numeric params.
|
|
76
|
+
const ListSitesQuerySchema = z.object({
|
|
77
|
+
limit: z.coerce.number().int().min(1).max(500).optional().default(100),
|
|
78
|
+
offset: z.coerce.number().int().min(0).optional().default(0),
|
|
79
|
+
sort: z.enum(['createdAt', 'name']).optional().default('createdAt'),
|
|
80
|
+
order: z.enum(['asc', 'desc']).optional().default('desc'),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Dependency adapters for core functions
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
function buildNginxDeps() {
|
|
88
|
+
return { writeStaticSiteVhost, removeStaticSiteVhost };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function buildCertbotDeps() {
|
|
92
|
+
return { issueTunnelCert, getCertPath, issueSiteCert };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildFilesDeps() {
|
|
96
|
+
return { createSiteDirectory, removeSiteDirectory, getSiteRoot };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildSiteStateDeps() {
|
|
100
|
+
return { readSites, writeSites };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function buildTunnelReadDeps() {
|
|
104
|
+
return { readTunnels };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function buildAutheliaDeps() {
|
|
108
|
+
return { updateAccessControl };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Map a SiteError code to an HTTP status code.
|
|
113
|
+
*/
|
|
114
|
+
function siteErrorStatus(code) {
|
|
115
|
+
switch (code) {
|
|
116
|
+
case 'CUSTOM_DOMAIN_REQUIRED':
|
|
117
|
+
case 'NAME_IN_USE':
|
|
118
|
+
case 'RESERVED_NAME':
|
|
119
|
+
case 'NAME_TUNNEL_COLLISION':
|
|
120
|
+
case 'FQDN_IN_USE':
|
|
121
|
+
case 'FQDN_TUNNEL_COLLISION':
|
|
122
|
+
case 'NOT_CUSTOM':
|
|
123
|
+
case 'DOMAIN_NOT_CONFIGURED':
|
|
124
|
+
case 'INVALID_ALIAS':
|
|
125
|
+
case 'ALIAS_IN_USE':
|
|
126
|
+
case 'DNS_MISMATCH':
|
|
127
|
+
return 400;
|
|
128
|
+
case 'NOT_FOUND':
|
|
129
|
+
return 404;
|
|
130
|
+
case 'ALREADY_VERIFIED':
|
|
131
|
+
return 200;
|
|
132
|
+
default:
|
|
133
|
+
return 500;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function assertAgentSiteAccess(request, site) {
|
|
138
|
+
if (request.certRole === 'agent') {
|
|
139
|
+
const allowed = request.certAllowedSites || [];
|
|
140
|
+
if (!allowed.includes(site.name)) {
|
|
141
|
+
const err = new Error('You do not have access to this site');
|
|
142
|
+
err.statusCode = 403;
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export default async function sitesRoutes(fastify, _opts) {
|
|
149
|
+
const nginxDeps = buildNginxDeps();
|
|
150
|
+
const certbotDeps = buildCertbotDeps();
|
|
151
|
+
const filesDeps = buildFilesDeps();
|
|
152
|
+
const siteStateDeps = buildSiteStateDeps();
|
|
153
|
+
const tunnelReadDeps = buildTunnelReadDeps();
|
|
154
|
+
const autheliaDeps = buildAutheliaDeps();
|
|
155
|
+
|
|
156
|
+
// GET /api/sites
|
|
157
|
+
//
|
|
158
|
+
// Paginated. Defaults (limit=100, offset=0, sort=createdAt desc) preserve
|
|
159
|
+
// the prior unpaginated UX for installations under 100 sites. The
|
|
160
|
+
// `sites` array stays the response envelope's primary field; older
|
|
161
|
+
// clients that ignore `total`/`limit`/`offset` continue to function and
|
|
162
|
+
// simply see the first window.
|
|
163
|
+
//
|
|
164
|
+
// Agent-side filtering happens BEFORE windowing so the `total` an agent
|
|
165
|
+
// sees reflects the agent's visible set, not the global count.
|
|
166
|
+
fastify.get(
|
|
167
|
+
'/sites',
|
|
168
|
+
{
|
|
169
|
+
preHandler: fastify.requireRole(['admin', 'agent'], { capability: 'sites:read' }),
|
|
170
|
+
},
|
|
171
|
+
async (request, _reply) => {
|
|
172
|
+
const { limit, offset, sort, order } = ListSitesQuerySchema.parse(request.query);
|
|
173
|
+
let sites = await readSites();
|
|
174
|
+
|
|
175
|
+
const direction = order === 'asc' ? 1 : -1;
|
|
176
|
+
sites.sort((a, b) => {
|
|
177
|
+
if (sort === 'name') {
|
|
178
|
+
const av = String(a.name ?? '');
|
|
179
|
+
const bv = String(b.name ?? '');
|
|
180
|
+
return av < bv ? -direction : av > bv ? direction : 0;
|
|
181
|
+
}
|
|
182
|
+
const at = new Date(a.createdAt ?? 0).getTime();
|
|
183
|
+
const bt = new Date(b.createdAt ?? 0).getTime();
|
|
184
|
+
return (at - bt) * direction;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Agents only see their allowed sites
|
|
188
|
+
if (request.certRole === 'agent') {
|
|
189
|
+
const allowed = request.certAllowedSites || [];
|
|
190
|
+
sites = sites.filter((s) => allowed.includes(s.name));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const total = sites.length;
|
|
194
|
+
const windowed = sites.slice(offset, offset + limit);
|
|
195
|
+
|
|
196
|
+
return { sites: windowed, total, limit, offset };
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// POST /api/sites
|
|
201
|
+
fastify.post(
|
|
202
|
+
'/sites',
|
|
203
|
+
{
|
|
204
|
+
preHandler: fastify.requireRole(['admin']),
|
|
205
|
+
},
|
|
206
|
+
async (request, reply) => {
|
|
207
|
+
const body = CreateSiteSchema.parse(request.body);
|
|
208
|
+
const config = getConfig();
|
|
209
|
+
|
|
210
|
+
if (!config.domain || !config.email) {
|
|
211
|
+
return reply.code(400).send({
|
|
212
|
+
error: 'Domain and email must be configured before creating sites',
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const result = await createSite({
|
|
218
|
+
name: body.name,
|
|
219
|
+
type: body.type,
|
|
220
|
+
customDomain: body.customDomain,
|
|
221
|
+
aliases: body.aliases,
|
|
222
|
+
spaMode: body.spaMode,
|
|
223
|
+
autheliaProtected: body.autheliaProtected,
|
|
224
|
+
domain: config.domain,
|
|
225
|
+
email: config.email,
|
|
226
|
+
nginx: nginxDeps,
|
|
227
|
+
certbot: certbotDeps,
|
|
228
|
+
files: filesDeps,
|
|
229
|
+
siteState: siteStateDeps,
|
|
230
|
+
tunnelState: tunnelReadDeps,
|
|
231
|
+
logger: request.log,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const response = { ok: true, site: result.site };
|
|
235
|
+
if (result.message) response.message = result.message;
|
|
236
|
+
return reply.code(201).send(response);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
if (err instanceof SiteError) {
|
|
239
|
+
const status = siteErrorStatus(err.code);
|
|
240
|
+
return reply.code(status).send({
|
|
241
|
+
error: 'Failed to create site',
|
|
242
|
+
details: err.message,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
request.log.error(err, 'Failed to create site');
|
|
246
|
+
return reply.code(500).send({
|
|
247
|
+
error: 'Failed to create site',
|
|
248
|
+
details: err.message,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// DELETE /api/sites/:id
|
|
255
|
+
fastify.delete(
|
|
256
|
+
'/sites/:id',
|
|
257
|
+
{
|
|
258
|
+
preHandler: fastify.requireRole(['admin']),
|
|
259
|
+
},
|
|
260
|
+
async (request, reply) => {
|
|
261
|
+
const { id } = IdParamSchema.parse(request.params);
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
await deleteSite({
|
|
265
|
+
id,
|
|
266
|
+
nginx: nginxDeps,
|
|
267
|
+
files: filesDeps,
|
|
268
|
+
siteState: siteStateDeps,
|
|
269
|
+
logger: request.log,
|
|
270
|
+
});
|
|
271
|
+
return { ok: true };
|
|
272
|
+
} catch (err) {
|
|
273
|
+
if (err instanceof SiteError) {
|
|
274
|
+
const status = siteErrorStatus(err.code);
|
|
275
|
+
return reply.code(status).send({
|
|
276
|
+
error: 'Failed to delete site',
|
|
277
|
+
details: err.message,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
request.log.error(err, 'Failed to delete site');
|
|
281
|
+
return reply.code(500).send({
|
|
282
|
+
error: 'Failed to delete site',
|
|
283
|
+
details: err.message,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
// PATCH /api/sites/:id — update site settings (spaMode, autheliaProtected, allowedUsers, aliases)
|
|
290
|
+
fastify.patch(
|
|
291
|
+
'/sites/:id',
|
|
292
|
+
{
|
|
293
|
+
preHandler: fastify.requireRole(['admin']),
|
|
294
|
+
},
|
|
295
|
+
async (request, reply) => {
|
|
296
|
+
const { id } = IdParamSchema.parse(request.params);
|
|
297
|
+
const body = UpdateSiteSchema.parse(request.body);
|
|
298
|
+
const config = getConfig();
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const result = await updateSite({
|
|
302
|
+
id,
|
|
303
|
+
spaMode: body.spaMode,
|
|
304
|
+
autheliaProtected: body.autheliaProtected,
|
|
305
|
+
allowedUsers: body.allowedUsers,
|
|
306
|
+
aliases: body.aliases,
|
|
307
|
+
domain: config.domain,
|
|
308
|
+
email: config.email,
|
|
309
|
+
serverIp: config.ip,
|
|
310
|
+
nginx: nginxDeps,
|
|
311
|
+
certbot: certbotDeps,
|
|
312
|
+
siteState: siteStateDeps,
|
|
313
|
+
tunnelState: tunnelReadDeps,
|
|
314
|
+
authelia: autheliaDeps,
|
|
315
|
+
logger: request.log,
|
|
316
|
+
});
|
|
317
|
+
return result;
|
|
318
|
+
} catch (err) {
|
|
319
|
+
if (err instanceof SiteError) {
|
|
320
|
+
const status = siteErrorStatus(err.code);
|
|
321
|
+
// Special case: AUTHELIA_FAILED means state was saved but Authelia sync failed
|
|
322
|
+
if (err.code === 'AUTHELIA_FAILED') {
|
|
323
|
+
return reply.code(500).send({
|
|
324
|
+
error: 'Site saved but Authelia configuration failed',
|
|
325
|
+
details: err.message,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
return reply.code(status).send({
|
|
329
|
+
error: 'Failed to update site configuration',
|
|
330
|
+
details: err.message,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
request.log.error(err, 'Failed to update site');
|
|
334
|
+
return reply.code(500).send({
|
|
335
|
+
error: 'Failed to update site configuration',
|
|
336
|
+
details: err.message,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
// POST /api/sites/:id/verify-dns
|
|
343
|
+
fastify.post(
|
|
344
|
+
'/sites/:id/verify-dns',
|
|
345
|
+
{
|
|
346
|
+
preHandler: fastify.requireRole(['admin']),
|
|
347
|
+
// Moderate tier — triggers DNS lookups and potentially certbot;
|
|
348
|
+
// the work is amplified per request and must be bounded.
|
|
349
|
+
config: { rateLimit: { max: 30, timeWindow: '1 minute' } },
|
|
350
|
+
},
|
|
351
|
+
async (request, reply) => {
|
|
352
|
+
const { id } = IdParamSchema.parse(request.params);
|
|
353
|
+
const config = getConfig();
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const result = await verifyDns({
|
|
357
|
+
id,
|
|
358
|
+
serverIp: config.ip,
|
|
359
|
+
domain: config.domain,
|
|
360
|
+
email: config.email,
|
|
361
|
+
nginx: nginxDeps,
|
|
362
|
+
certbot: certbotDeps,
|
|
363
|
+
siteState: siteStateDeps,
|
|
364
|
+
logger: request.log,
|
|
365
|
+
});
|
|
366
|
+
return result;
|
|
367
|
+
} catch (err) {
|
|
368
|
+
if (err instanceof SiteError) {
|
|
369
|
+
const status = siteErrorStatus(err.code);
|
|
370
|
+
return reply.code(status).send({
|
|
371
|
+
error: err.message,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
request.log.error(err, 'Failed to verify DNS');
|
|
375
|
+
return reply.code(500).send({
|
|
376
|
+
error: 'DNS verification failed',
|
|
377
|
+
details: err.message,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
// GET /api/sites/:id/files
|
|
384
|
+
fastify.get(
|
|
385
|
+
'/sites/:id/files',
|
|
386
|
+
{
|
|
387
|
+
preHandler: fastify.requireRole(['admin', 'agent'], { capability: 'sites:read' }),
|
|
388
|
+
},
|
|
389
|
+
async (request, reply) => {
|
|
390
|
+
const { id } = IdParamSchema.parse(request.params);
|
|
391
|
+
const { path: relativePath } = PathQuerySchema.parse(request.query);
|
|
392
|
+
|
|
393
|
+
const sites = await readSites();
|
|
394
|
+
const site = sites.find((s) => s.id === id);
|
|
395
|
+
if (!site) {
|
|
396
|
+
return reply.code(404).send({ error: 'Site not found' });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
assertAgentSiteAccess(request, site);
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
const files = await listFiles(id, relativePath);
|
|
403
|
+
return { files, path: relativePath };
|
|
404
|
+
} catch (err) {
|
|
405
|
+
return reply.code(400).send({ error: err.message });
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
// POST /api/sites/:id/files — multipart file upload
|
|
411
|
+
fastify.post(
|
|
412
|
+
'/sites/:id/files',
|
|
413
|
+
{
|
|
414
|
+
preHandler: fastify.requireRole(['admin', 'agent'], { capability: 'sites:write' }),
|
|
415
|
+
},
|
|
416
|
+
async (request, reply) => {
|
|
417
|
+
const { id } = IdParamSchema.parse(request.params);
|
|
418
|
+
const config = getConfig();
|
|
419
|
+
|
|
420
|
+
const sites = await readSites();
|
|
421
|
+
const site = sites.find((s) => s.id === id);
|
|
422
|
+
if (!site) {
|
|
423
|
+
return reply.code(404).send({ error: 'Site not found' });
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
assertAgentSiteAccess(request, site);
|
|
427
|
+
|
|
428
|
+
const { path: uploadDir } = PathQuerySchema.parse(request.query);
|
|
429
|
+
const uploadedFiles = [];
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
const parts = request.parts();
|
|
433
|
+
for await (const part of parts) {
|
|
434
|
+
if (part.type !== 'file' || !part.filename) {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const relativePath = uploadDir === '.' ? part.filename : `${uploadDir}/${part.filename}`;
|
|
439
|
+
|
|
440
|
+
// Validate path and file extension before saving
|
|
441
|
+
validatePath(relativePath);
|
|
442
|
+
validateFileExtension(part.filename);
|
|
443
|
+
|
|
444
|
+
await saveUploadedFile(id, relativePath, part.file);
|
|
445
|
+
uploadedFiles.push(relativePath);
|
|
446
|
+
}
|
|
447
|
+
} catch (err) {
|
|
448
|
+
return reply.code(400).send({ error: `Upload failed: ${err.message}` });
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Update site size
|
|
452
|
+
try {
|
|
453
|
+
const totalSize = await getSiteSize(id);
|
|
454
|
+
const siteIndex = sites.findIndex((s) => s.id === id);
|
|
455
|
+
sites[siteIndex].totalSize = totalSize;
|
|
456
|
+
|
|
457
|
+
if (totalSize > config.maxSiteSize) {
|
|
458
|
+
// Don't block but warn
|
|
459
|
+
await writeSites(sites);
|
|
460
|
+
return reply.code(200).send({
|
|
461
|
+
ok: true,
|
|
462
|
+
files: uploadedFiles,
|
|
463
|
+
warning: `Site size (${formatBytes(totalSize)}) exceeds the ${formatBytes(config.maxSiteSize)} limit.`,
|
|
464
|
+
totalSize,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
await writeSites(sites);
|
|
469
|
+
} catch {
|
|
470
|
+
// Non-critical — don't fail the upload
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return { ok: true, files: uploadedFiles };
|
|
474
|
+
},
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
// DELETE /api/sites/:id/files
|
|
478
|
+
fastify.delete(
|
|
479
|
+
'/sites/:id/files',
|
|
480
|
+
{
|
|
481
|
+
preHandler: fastify.requireRole(['admin', 'agent'], { capability: 'sites:write' }),
|
|
482
|
+
},
|
|
483
|
+
async (request, reply) => {
|
|
484
|
+
const { id } = IdParamSchema.parse(request.params);
|
|
485
|
+
|
|
486
|
+
const sites = await readSites();
|
|
487
|
+
const site = sites.find((s) => s.id === id);
|
|
488
|
+
if (!site) {
|
|
489
|
+
return reply.code(404).send({ error: 'Site not found' });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
assertAgentSiteAccess(request, site);
|
|
493
|
+
|
|
494
|
+
const body = DeleteFileSchema.parse(request.body);
|
|
495
|
+
|
|
496
|
+
try {
|
|
497
|
+
await deleteFile(id, body.path);
|
|
498
|
+
} catch (err) {
|
|
499
|
+
return reply.code(400).send({ error: err.message });
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Update site size
|
|
503
|
+
try {
|
|
504
|
+
const totalSize = await getSiteSize(id);
|
|
505
|
+
const siteIndex = sites.findIndex((s) => s.id === id);
|
|
506
|
+
sites[siteIndex].totalSize = totalSize;
|
|
507
|
+
await writeSites(sites);
|
|
508
|
+
} catch {
|
|
509
|
+
// Non-critical
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return { ok: true };
|
|
513
|
+
},
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function formatBytes(bytes) {
|
|
518
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
519
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
520
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
521
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
522
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import {
|
|
3
|
+
registerStorageServer,
|
|
4
|
+
removeStorageServer,
|
|
5
|
+
listStorageServers,
|
|
6
|
+
bindPluginStorage,
|
|
7
|
+
unbindPluginStorage,
|
|
8
|
+
listBindings,
|
|
9
|
+
getBinding,
|
|
10
|
+
} from '../../lib/storage.js';
|
|
11
|
+
import { readPlugins } from '../../lib/plugins.js';
|
|
12
|
+
|
|
13
|
+
// --- Zod schemas ---
|
|
14
|
+
|
|
15
|
+
const RegisterServerSchema = z.object({
|
|
16
|
+
id: z.string().uuid(),
|
|
17
|
+
label: z.string().min(1).max(100),
|
|
18
|
+
provider: z.string().min(1).max(50),
|
|
19
|
+
region: z.string().min(1).max(50),
|
|
20
|
+
bucket: z.string().min(1).max(200),
|
|
21
|
+
endpoint: z.string().url(),
|
|
22
|
+
accessKey: z.string().min(1).max(500),
|
|
23
|
+
secretKey: z.string().min(1).max(500),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const ServerIdParamSchema = z.object({
|
|
27
|
+
id: z.string().uuid(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const BindingBodySchema = z.object({
|
|
31
|
+
pluginName: z
|
|
32
|
+
.string()
|
|
33
|
+
.min(1)
|
|
34
|
+
.max(100)
|
|
35
|
+
.regex(/^[a-z0-9-]+$/, 'Plugin name must contain only lowercase letters, numbers, and hyphens'),
|
|
36
|
+
storageServerId: z.string().uuid(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const PluginNameParamSchema = z.object({
|
|
40
|
+
pluginName: z
|
|
41
|
+
.string()
|
|
42
|
+
.min(1)
|
|
43
|
+
.max(100)
|
|
44
|
+
.regex(/^[a-z0-9-]+$/, 'Plugin name must contain only lowercase letters, numbers, and hyphens'),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export default async function storageRoutes(fastify, _opts) {
|
|
48
|
+
// ===========================================================================
|
|
49
|
+
// Storage server routes
|
|
50
|
+
// ===========================================================================
|
|
51
|
+
|
|
52
|
+
// POST /storage/servers — register a storage server
|
|
53
|
+
fastify.post(
|
|
54
|
+
'/storage/servers',
|
|
55
|
+
{
|
|
56
|
+
preHandler: fastify.requireRole(['admin']),
|
|
57
|
+
},
|
|
58
|
+
async (request, reply) => {
|
|
59
|
+
const body = RegisterServerSchema.parse(request.body);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const entry = await registerStorageServer(body);
|
|
63
|
+
return reply.code(201).send(entry);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
const statusCode = err.statusCode || 500;
|
|
66
|
+
return reply.code(statusCode).send({
|
|
67
|
+
error: err.message || 'Failed to register storage server',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// GET /storage/servers — list registered storage servers
|
|
74
|
+
fastify.get(
|
|
75
|
+
'/storage/servers',
|
|
76
|
+
{
|
|
77
|
+
preHandler: fastify.requireRole(['admin']),
|
|
78
|
+
},
|
|
79
|
+
async (_request, _reply) => {
|
|
80
|
+
const servers = await listStorageServers();
|
|
81
|
+
return { servers };
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// DELETE /storage/servers/:id — remove storage server and bindings
|
|
86
|
+
fastify.delete(
|
|
87
|
+
'/storage/servers/:id',
|
|
88
|
+
{
|
|
89
|
+
preHandler: fastify.requireRole(['admin']),
|
|
90
|
+
},
|
|
91
|
+
async (request, reply) => {
|
|
92
|
+
const params = ServerIdParamSchema.parse(request.params);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const result = await removeStorageServer(params.id);
|
|
96
|
+
return result;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
const statusCode = err.statusCode || 500;
|
|
99
|
+
return reply.code(statusCode).send({
|
|
100
|
+
error: err.message || 'Failed to remove storage server',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// ===========================================================================
|
|
107
|
+
// Storage binding routes
|
|
108
|
+
// ===========================================================================
|
|
109
|
+
|
|
110
|
+
// POST /storage/bindings — bind storage to plugin
|
|
111
|
+
fastify.post(
|
|
112
|
+
'/storage/bindings',
|
|
113
|
+
{
|
|
114
|
+
preHandler: fastify.requireRole(['admin']),
|
|
115
|
+
},
|
|
116
|
+
async (request, reply) => {
|
|
117
|
+
const body = BindingBodySchema.parse(request.body);
|
|
118
|
+
|
|
119
|
+
// Validate plugin exists in registry
|
|
120
|
+
const registry = await readPlugins();
|
|
121
|
+
const pluginExists = registry.plugins.some((p) => p.name === body.pluginName);
|
|
122
|
+
if (!pluginExists) {
|
|
123
|
+
return reply.code(404).send({
|
|
124
|
+
error: `Plugin "${body.pluginName}" not found in registry`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const binding = await bindPluginStorage(body.pluginName, body.storageServerId);
|
|
130
|
+
return reply.code(201).send(binding);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
const statusCode = err.statusCode || 500;
|
|
133
|
+
return reply.code(statusCode).send({
|
|
134
|
+
error: err.message || 'Failed to bind storage to plugin',
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// GET /storage/bindings — list all bindings
|
|
141
|
+
fastify.get(
|
|
142
|
+
'/storage/bindings',
|
|
143
|
+
{
|
|
144
|
+
preHandler: fastify.requireRole(['admin']),
|
|
145
|
+
},
|
|
146
|
+
async (_request, _reply) => {
|
|
147
|
+
const bindings = await listBindings();
|
|
148
|
+
return { bindings };
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// GET /storage/bindings/:pluginName — get binding for a specific plugin
|
|
153
|
+
fastify.get(
|
|
154
|
+
'/storage/bindings/:pluginName',
|
|
155
|
+
{
|
|
156
|
+
preHandler: fastify.requireRole(['admin']),
|
|
157
|
+
},
|
|
158
|
+
async (request, reply) => {
|
|
159
|
+
const params = PluginNameParamSchema.parse(request.params);
|
|
160
|
+
|
|
161
|
+
const binding = await getBinding(params.pluginName);
|
|
162
|
+
if (!binding) {
|
|
163
|
+
return reply.code(404).send({
|
|
164
|
+
error: `No storage binding for plugin "${params.pluginName}"`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return binding;
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// DELETE /storage/bindings/:pluginName — unbind storage from plugin
|
|
173
|
+
fastify.delete(
|
|
174
|
+
'/storage/bindings/:pluginName',
|
|
175
|
+
{
|
|
176
|
+
preHandler: fastify.requireRole(['admin']),
|
|
177
|
+
},
|
|
178
|
+
async (request, reply) => {
|
|
179
|
+
const params = PluginNameParamSchema.parse(request.params);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const result = await unbindPluginStorage(params.pluginName);
|
|
183
|
+
return result;
|
|
184
|
+
} catch (err) {
|
|
185
|
+
const statusCode = err.statusCode || 500;
|
|
186
|
+
return reply.code(statusCode).send({
|
|
187
|
+
error: err.message || 'Failed to unbind storage from plugin',
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
}
|