@lamalibre/create-lamaste 4.5.0 → 4.7.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/package.json +2 -2
- package/scripts/bundle-vendor.js +20 -3
- package/scripts/lamaste-priv +56 -5
- package/scripts/pin-release.js +98 -0
- package/src/index.js +8 -1
- package/src/lib/env.js +19 -0
- package/src/lib/service-config.js +7 -0
- package/src/tasks/redeploy.js +8 -0
- package/vendor/docs/00-introduction/how-it-works.md +2 -2
- package/vendor/docs/00-introduction/quickstart.md +5 -5
- package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
- package/vendor/docs/01-concepts/authentication.md +6 -2
- package/vendor/docs/01-concepts/authorization.md +4 -0
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/dns-and-domains.md +9 -0
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/security-model.md +28 -3
- package/vendor/docs/01-concepts/tickets.md +2 -2
- package/vendor/docs/01-concepts/tunneling.md +4 -4
- package/vendor/docs/02-guides/agent-setup.md +5 -5
- package/vendor/docs/02-guides/certificate-management.md +2 -2
- package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
- package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
- package/vendor/docs/02-guides/installation.md +3 -3
- package/vendor/docs/02-guides/managing-users.md +5 -11
- package/vendor/docs/02-guides/onboarding.md +2 -2
- package/vendor/docs/02-guides/second-domain.md +82 -0
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
- package/vendor/docs/02-guides/static-sites.md +4 -0
- package/vendor/docs/03-architecture/installer-flow.md +2 -2
- package/vendor/docs/03-architecture/installer.md +4 -4
- package/vendor/docs/03-architecture/management-flow.md +1 -1
- package/vendor/docs/03-architecture/nginx-configuration.md +80 -12
- package/vendor/docs/03-architecture/overview.md +5 -5
- package/vendor/docs/03-architecture/panel-client.md +6 -6
- package/vendor/docs/03-architecture/panel-server.md +2 -2
- package/vendor/docs/03-architecture/state-management.md +3 -3
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +116 -37
- package/vendor/docs/04-api-reference/app-host.md +3 -3
- package/vendor/docs/04-api-reference/domains.md +192 -0
- package/vendor/docs/04-api-reference/gatekeeper.md +54 -8
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +4 -4
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/overview.md +9 -0
- package/vendor/docs/04-api-reference/registry-and-oidc.md +26 -13
- package/vendor/docs/04-api-reference/sites.md +9 -0
- package/vendor/docs/04-api-reference/tunnels.md +35 -28
- package/vendor/docs/04-api-reference/users.md +41 -29
- package/vendor/docs/05-operations/backup-and-restore.md +1 -1
- package/vendor/docs/05-operations/uninstalling.md +1 -1
- package/vendor/docs/05-operations/upgrades.md +15 -6
- package/vendor/docs/06-reference/config-files.md +19 -15
- package/vendor/docs/06-reference/glossary.md +3 -3
- package/vendor/docs/06-reference/installer-flags.md +13 -13
- package/vendor/docs/06-reference/ports-and-services.md +1 -1
- package/vendor/docs/06-reference/troubleshooting.md +8 -8
- package/vendor/docs/_index.json +105 -19
- package/vendor/docs/external-members-design.md +816 -0
- package/vendor/docs/gatekeeper-design.md +8 -8
- package/vendor/docs/identity-directory-design.md +3 -3
- package/vendor/docs/multi-domain-relay-design.md +836 -0
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- package/vendor/gatekeeper/dist/index.d.ts +3 -3
- package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/index.js +2 -2
- package/vendor/gatekeeper/dist/index.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/authz.d.ts +9 -0
- package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/authz.js +31 -0
- package/vendor/gatekeeper/dist/lib/authz.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/constants.d.ts +5 -2
- package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/constants.js +5 -2
- package/vendor/gatekeeper/dist/lib/constants.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/grants.js +20 -13
- package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0004_unconsume_access_grants.sql +8 -0
- package/vendor/gatekeeper/dist/lib/secret.d.ts +6 -0
- package/vendor/gatekeeper/dist/lib/secret.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/secret.js +12 -0
- package/vendor/gatekeeper/dist/lib/secret.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts +20 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/templates.js +80 -14
- package/vendor/gatekeeper/dist/lib/templates.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts +3 -6
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/tunnels.js +4 -16
- package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/types.d.ts +0 -19
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.d.ts +6 -1
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +56 -11
- package/vendor/gatekeeper/dist/server/index.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.js +325 -55
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js +5 -2
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
- package/vendor/gatekeeper/package.json +2 -2
- package/vendor/server/bin/lamaste-server.js +7 -0
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/domains.js +214 -0
- package/vendor/server/src/commands/plugin-identity.js +2 -16
- package/vendor/server/src/commands/plugin-web-surface.js +269 -0
- package/vendor/server/src/commands/plugins.js +175 -14
- package/vendor/server/src/commands/registry-credential.js +10 -3
- package/vendor/server/src/commands/sites.js +18 -1
- package/vendor/server/src/commands/status.js +19 -0
- package/vendor/server/src/commands/tunnels.js +4 -1
- package/vendor/server/src/commands/users.js +8 -19
- package/vendor/server/src/domains.js +92 -0
- package/vendor/server/src/fail.js +21 -0
- package/vendor/server/src/oidc-client-grants.js +63 -0
- package/vendor/server/src/panel-api.js +15 -0
- package/vendor/server/src/plugin-approval.js +80 -1
- package/vendor/server/src/plugin-web-surface.js +307 -0
- package/vendor/server-ui/dist/assets/index-B8Tv_ocF.js +84 -0
- package/vendor/server-ui/dist/assets/index-Cb-ADsbB.css +1 -0
- package/vendor/server-ui/dist/index.html +2 -2
- package/vendor/serverd/package.json +2 -2
- package/vendor/serverd/src/index.js +50 -19
- package/vendor/serverd/src/lib/app-error.js +24 -0
- package/vendor/serverd/src/lib/audience.js +12 -3
- package/vendor/serverd/src/lib/authelia.js +341 -35
- package/vendor/serverd/src/lib/chisel-runtime.js +18 -6
- package/vendor/serverd/src/lib/chisel.js +10 -2
- package/vendor/serverd/src/lib/directory.js +67 -52
- package/vendor/serverd/src/lib/domains.js +1792 -0
- package/vendor/serverd/src/lib/gatekeeper-client.js +34 -7
- package/vendor/serverd/src/lib/hook-pipelines.js +8 -3
- package/vendor/serverd/src/lib/identity-sync.js +35 -0
- package/vendor/serverd/src/lib/migrations/state/0015_plugin_web_surface.sql +14 -0
- package/vendor/serverd/src/lib/migrations/state/0016_domains.sql +53 -0
- package/vendor/serverd/src/lib/migrations/state/0017_membership_kind.sql +15 -0
- package/vendor/serverd/src/lib/migrations/state/0018_domain_group_keys.sql +23 -0
- package/vendor/serverd/src/lib/nginx.js +576 -131
- package/vendor/serverd/src/lib/owned-groups.js +326 -0
- package/vendor/serverd/src/lib/plugin-domain.js +86 -0
- package/vendor/serverd/src/lib/plugin-hooks.js +40 -11
- package/vendor/serverd/src/lib/plugin-lifecycle.js +129 -5
- package/vendor/serverd/src/lib/plugin-oidc.js +144 -42
- package/vendor/serverd/src/lib/plugin-packages.js +74 -2
- package/vendor/serverd/src/lib/plugins.js +82 -9
- package/vendor/serverd/src/lib/registry-credential.js +39 -6
- package/vendor/serverd/src/lib/services.js +9 -1
- package/vendor/serverd/src/lib/state.js +29 -0
- package/vendor/serverd/src/lib/tunnel-deps.js +2 -4
- package/vendor/serverd/src/lib/user-surface.js +77 -31
- package/vendor/serverd/src/lib/users-lock.js +23 -0
- package/vendor/serverd/src/lib/vhost-reconcile.js +151 -41
- package/vendor/serverd/src/lib/web-surface.js +783 -0
- package/vendor/serverd/src/middleware/user-access-session.js +11 -4
- package/vendor/serverd/src/routes/invite.js +15 -5
- package/vendor/serverd/src/routes/management/domains.js +154 -0
- package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +44 -8
- package/vendor/serverd/src/routes/management/identity.js +21 -7
- package/vendor/serverd/src/routes/management/invitations.js +33 -4
- package/vendor/serverd/src/routes/management/plugin-web-surface.js +151 -0
- package/vendor/serverd/src/routes/management/plugins.js +6 -2
- package/vendor/serverd/src/routes/management/registry-credential.js +86 -57
- package/vendor/serverd/src/routes/management/sites.js +188 -71
- package/vendor/serverd/src/routes/management/tunnels.js +37 -22
- package/vendor/serverd/src/routes/management/users.js +184 -64
- package/vendor/serverd/src/routes/management.js +4 -0
- package/vendor/serverd/src/routes/onboarding/dns.js +3 -17
- package/vendor/serverd/src/routes/onboarding/provision.js +22 -13
- package/vendor/serverd/src/routes/user-access.js +13 -1
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0001_groups.sql +0 -7
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0002_access_grants.sql +0 -14
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0003_access_request_log.sql +0 -9
- package/vendor/server-ui/dist/assets/index-D-fer5ef.css +0 -1
- package/vendor/server-ui/dist/assets/index-DSaDPJaw.js +0 -84
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*
|
|
10
10
|
* `auth.kind` is `token` (a pasted long-lived Feria token) or `oidc` (the
|
|
11
11
|
* panel's own `lamaste-server` client on Authelia: client-credentials grant,
|
|
12
|
-
* Feria login
|
|
12
|
+
* Feria login; `issuerDomain` names the active domain whose issuer,
|
|
13
|
+
* `https://auth.<domain>`, the registry trusts — the primary when absent). For `oidc` the client is written to Authelia before the
|
|
13
14
|
* chain is verified; if verification fails it is removed again, and the
|
|
14
15
|
* error says what Feria's `auth.json` needs.
|
|
15
16
|
*
|
|
@@ -28,9 +29,12 @@ import {
|
|
|
28
29
|
clearRegistryCredentialConfig,
|
|
29
30
|
readRegistryCredentialConfig,
|
|
30
31
|
verifyRegistryCredential,
|
|
31
|
-
|
|
32
|
+
setRegistryCredential,
|
|
32
33
|
} from '../../lib/registry-credential.js';
|
|
33
|
-
import {
|
|
34
|
+
import { DomainNameSchema } from '@lamalibre/lamaste';
|
|
35
|
+
import { sendFailure } from '../../lib/app-error.js';
|
|
36
|
+
import { resolveActiveDomain } from '../../lib/domains.js';
|
|
37
|
+
import { syncOidcClients, withRegistryCandidate } from '../../lib/plugin-oidc.js';
|
|
34
38
|
|
|
35
39
|
const FeriaTokenSchema = z
|
|
36
40
|
.string()
|
|
@@ -40,7 +44,7 @@ const FeriaTokenSchema = z
|
|
|
40
44
|
|
|
41
45
|
const AuthSchema = z.discriminatedUnion('kind', [
|
|
42
46
|
z.object({ kind: z.literal('token'), token: FeriaTokenSchema }),
|
|
43
|
-
z.object({ kind: z.literal('oidc') }),
|
|
47
|
+
z.object({ kind: z.literal('oidc'), issuerDomain: DomainNameSchema.optional() }),
|
|
44
48
|
]);
|
|
45
49
|
|
|
46
50
|
const PutSchema = z
|
|
@@ -64,9 +68,13 @@ const PutSchema = z
|
|
|
64
68
|
}
|
|
65
69
|
});
|
|
66
70
|
|
|
67
|
-
/** The stored `auth` for a request body. */
|
|
68
|
-
function authOf(body) {
|
|
69
|
-
if (body.auth?.kind === 'oidc')
|
|
71
|
+
/** The stored `auth` for a request body (`issuerDomain` resolved against the active domains). */
|
|
72
|
+
async function authOf(body) {
|
|
73
|
+
if (body.auth?.kind === 'oidc') {
|
|
74
|
+
const auth = { kind: 'oidc', clientId: LAMASTE_SERVER_OIDC_CLIENT_ID };
|
|
75
|
+
if (body.auth.issuerDomain === undefined) return auth;
|
|
76
|
+
return { ...auth, issuerDomain: await resolveActiveDomain(body.auth.issuerDomain) };
|
|
77
|
+
}
|
|
70
78
|
if (body.auth?.kind === 'token') return { kind: 'token', token: body.auth.token };
|
|
71
79
|
return { kind: 'token', token: body.token };
|
|
72
80
|
}
|
|
@@ -90,62 +98,83 @@ export default async function registryCredentialRoutes(fastify) {
|
|
|
90
98
|
},
|
|
91
99
|
async (request, reply) => {
|
|
92
100
|
const body = PutSchema.parse(request.body);
|
|
93
|
-
|
|
101
|
+
let auth;
|
|
102
|
+
try {
|
|
103
|
+
auth = await authOf(body);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
return sendFailure(request, reply, err, 'Failed to read the domains');
|
|
106
|
+
}
|
|
94
107
|
const candidate = { registryUrl: body.registryUrl, auth };
|
|
95
108
|
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
109
|
+
// An oidc candidate is what every sync serves until it is stored or
|
|
110
|
+
// dropped — the identity tick's too, which would otherwise remove the
|
|
111
|
+
// panel's client from Authelia under the verification.
|
|
112
|
+
const registryAuth =
|
|
113
|
+
auth.kind === 'oidc'
|
|
114
|
+
? {
|
|
115
|
+
registryUrl: body.registryUrl,
|
|
116
|
+
...(auth.issuerDomain ? { issuerDomain: auth.issuerDomain } : {}),
|
|
117
|
+
}
|
|
118
|
+
: undefined;
|
|
119
|
+
try {
|
|
120
|
+
return await withRegistryCandidate(registryAuth, async () => {
|
|
121
|
+
// The panel's own client must exist in Authelia (with a secret in
|
|
122
|
+
// the store) before the chain can be exercised.
|
|
123
|
+
if (registryAuth) {
|
|
124
|
+
try {
|
|
125
|
+
if (!(await syncOidcClients(request.log))) {
|
|
126
|
+
return reply.code(503).send({ error: 'Complete onboarding (set a domain) first' });
|
|
127
|
+
}
|
|
128
|
+
} catch (err) {
|
|
129
|
+
return reply.code(500).send({
|
|
130
|
+
error: `Could not register the ${LAMASTE_SERVER_OIDC_CLIENT_ID} client in Authelia: ${err.message}`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
105
133
|
}
|
|
106
|
-
} catch (err) {
|
|
107
|
-
return reply.code(500).send({
|
|
108
|
-
error: `Could not register the ${LAMASTE_SERVER_OIDC_CLIENT_ID} client in Authelia: ${err.message}`,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
134
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
135
|
+
let verified;
|
|
136
|
+
try {
|
|
137
|
+
verified = await verifyRegistryCredential(candidate);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
const status = err instanceof RegistryCredentialError ? err.statusCode : 502;
|
|
140
|
+
return reply.code(status).send({ error: err.message });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const config = {
|
|
144
|
+
provider: 'feria',
|
|
145
|
+
registryUrl: body.registryUrl,
|
|
146
|
+
scope: body.scope ?? '@lamalibre',
|
|
147
|
+
auth,
|
|
148
|
+
owner: verified.owner,
|
|
149
|
+
configuredAt: new Date().toISOString(),
|
|
150
|
+
};
|
|
151
|
+
let warnings;
|
|
152
|
+
try {
|
|
153
|
+
warnings = await setRegistryCredential(config, { logger: request.log });
|
|
154
|
+
} catch (err) {
|
|
155
|
+
return sendFailure(request, reply, err, 'Failed to store the registry credential');
|
|
156
|
+
}
|
|
157
|
+
request.log.info(
|
|
158
|
+
{ registryUrl: body.registryUrl, owner: verified.owner, auth: auth.kind },
|
|
159
|
+
'Registry credential configured',
|
|
122
160
|
);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
161
|
+
return {
|
|
162
|
+
configured: true,
|
|
163
|
+
...summariseRegistryCredential(config),
|
|
164
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
} finally {
|
|
168
|
+
// With the candidate dropped, the stored credential decides whether
|
|
169
|
+
// Authelia carries the panel's client: the new one once stored, the
|
|
170
|
+
// previous one after a refusal (the candidate is not kept).
|
|
171
|
+
await syncOidcClients(request.log).catch((err) =>
|
|
172
|
+
request.log.warn(
|
|
173
|
+
{ err: err.message },
|
|
174
|
+
'Failed to sync OIDC clients after the registry credential request',
|
|
175
|
+
),
|
|
176
|
+
);
|
|
126
177
|
}
|
|
127
|
-
|
|
128
|
-
const config = {
|
|
129
|
-
provider: 'feria',
|
|
130
|
-
registryUrl: body.registryUrl,
|
|
131
|
-
scope: body.scope ?? '@lamalibre',
|
|
132
|
-
auth,
|
|
133
|
-
owner: verified.owner,
|
|
134
|
-
configuredAt: new Date().toISOString(),
|
|
135
|
-
};
|
|
136
|
-
await writeRegistryCredentialConfig(config);
|
|
137
|
-
// The stored credential now decides whether Authelia carries the client.
|
|
138
|
-
await syncOidcClients(request.log).catch((err) =>
|
|
139
|
-
request.log.warn(
|
|
140
|
-
{ err: err.message },
|
|
141
|
-
'Failed to sync OIDC clients after credential change',
|
|
142
|
-
),
|
|
143
|
-
);
|
|
144
|
-
request.log.info(
|
|
145
|
-
{ registryUrl: body.registryUrl, owner: verified.owner, auth: auth.kind },
|
|
146
|
-
'Registry credential configured',
|
|
147
|
-
);
|
|
148
|
-
return { configured: true, ...summariseRegistryCredential(config) };
|
|
149
178
|
},
|
|
150
179
|
);
|
|
151
180
|
|
|
@@ -11,11 +11,20 @@ import {
|
|
|
11
11
|
updateSite,
|
|
12
12
|
verifyDns,
|
|
13
13
|
SiteError,
|
|
14
|
+
withTunnelLock,
|
|
14
15
|
} from '@lamalibre/lamaste/server';
|
|
16
|
+
import { DomainNameSchema } from '@lamalibre/lamaste';
|
|
17
|
+
import { AppError } from '../../lib/app-error.js';
|
|
18
|
+
import { webSurfaceServing } from '../../lib/web-surface.js';
|
|
15
19
|
import { getConfig } from '../../lib/config.js';
|
|
16
|
-
import { readSites, writeSites, readTunnels } from '../../lib/state.js';
|
|
20
|
+
import { readSites, writeSites, readTunnels, withSitesLock } from '../../lib/state.js';
|
|
17
21
|
import { writeStaticSiteVhost, removeStaticSiteVhost } from '../../lib/nginx.js';
|
|
18
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
activeDomainNames,
|
|
24
|
+
domainNames,
|
|
25
|
+
resolveActiveDomain,
|
|
26
|
+
syncDomainAccess,
|
|
27
|
+
} from '../../lib/domains.js';
|
|
19
28
|
import { issueTunnelCert, getCertPath, issueSiteCert } from '../../lib/certbot.js';
|
|
20
29
|
import {
|
|
21
30
|
createSiteDirectory,
|
|
@@ -54,6 +63,8 @@ const CreateSiteSchema = z.object({
|
|
|
54
63
|
aliases: z.array(AliasSchema).max(10, 'At most 10 aliases').optional(),
|
|
55
64
|
spaMode: z.boolean().optional().default(false),
|
|
56
65
|
autheliaProtected: z.boolean().optional().default(false),
|
|
66
|
+
/** A managed site's domain (an active one; the primary when absent). */
|
|
67
|
+
domain: DomainNameSchema.optional(),
|
|
57
68
|
});
|
|
58
69
|
|
|
59
70
|
const UpdateSiteSchema = z.object({
|
|
@@ -104,8 +115,31 @@ function buildTunnelReadDeps() {
|
|
|
104
115
|
return { readTunnels };
|
|
105
116
|
}
|
|
106
117
|
|
|
107
|
-
|
|
108
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Authelia renders every site's protection together with the domains'
|
|
120
|
+
* members (lib/domains.js) — given the sites being saved.
|
|
121
|
+
*/
|
|
122
|
+
function buildAutheliaDeps(logger) {
|
|
123
|
+
return {
|
|
124
|
+
renderAccess: async (sites) => {
|
|
125
|
+
await syncDomainAccess({ logger, sites });
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The active domain a managed site is put on (the primary unless named), as
|
|
132
|
+
* a SiteError the create route answers like its other refusals.
|
|
133
|
+
*
|
|
134
|
+
* @param {string | undefined} requested
|
|
135
|
+
*/
|
|
136
|
+
async function siteDomain(requested) {
|
|
137
|
+
try {
|
|
138
|
+
return await resolveActiveDomain(requested);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
if (Number.isInteger(err.statusCode)) throw new SiteError(err.message, 'DOMAIN_NOT_CONFIGURED');
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
109
143
|
}
|
|
110
144
|
|
|
111
145
|
/**
|
|
@@ -116,7 +150,6 @@ function siteErrorStatus(code) {
|
|
|
116
150
|
case 'CUSTOM_DOMAIN_REQUIRED':
|
|
117
151
|
case 'NAME_IN_USE':
|
|
118
152
|
case 'RESERVED_NAME':
|
|
119
|
-
case 'NAME_TUNNEL_COLLISION':
|
|
120
153
|
case 'FQDN_IN_USE':
|
|
121
154
|
case 'FQDN_TUNNEL_COLLISION':
|
|
122
155
|
case 'NOT_CUSTOM':
|
|
@@ -124,6 +157,7 @@ function siteErrorStatus(code) {
|
|
|
124
157
|
case 'INVALID_ALIAS':
|
|
125
158
|
case 'ALIAS_IN_USE':
|
|
126
159
|
case 'DNS_MISMATCH':
|
|
160
|
+
case 'NOT_UNDER_DOMAIN':
|
|
127
161
|
return 400;
|
|
128
162
|
case 'NOT_FOUND':
|
|
129
163
|
return 404;
|
|
@@ -134,6 +168,79 @@ function siteErrorStatus(code) {
|
|
|
134
168
|
}
|
|
135
169
|
}
|
|
136
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Run a site workflow that may give a site a hostname: under core's tunnel
|
|
173
|
+
* lock, then the sites lock — the order every such workflow takes them in
|
|
174
|
+
* (`withSitesLock`) — so no tunnel takes the name meanwhile.
|
|
175
|
+
*
|
|
176
|
+
* @template T
|
|
177
|
+
* @param {() => Promise<T>} fn
|
|
178
|
+
* @returns {Promise<T>}
|
|
179
|
+
*/
|
|
180
|
+
function withHostnameLocks(fn) {
|
|
181
|
+
return withTunnelLock(() => withSitesLock(fn));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Refuse (409) a site a plugin owns: its files are the plugin's approved
|
|
186
|
+
* web tree and its settings follow the plugin — it is managed through the
|
|
187
|
+
* plugin's web surface, never here (and never by `lamaste-agent deploy`).
|
|
188
|
+
*/
|
|
189
|
+
function assertNotPluginOwned(site) {
|
|
190
|
+
if (site?.owner) {
|
|
191
|
+
throw new AppError(
|
|
192
|
+
`Site '${site.name}' is the web surface of plugin '${site.owner.plugin}' — manage it through the plugin`,
|
|
193
|
+
409,
|
|
194
|
+
{ code: 'plugin_owned_site' },
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The site `id`, refused when a plugin owns it; null when there is none. */
|
|
200
|
+
async function siteForManagement(id) {
|
|
201
|
+
const site = (await readSites()).find((s) => s.id === id) ?? null;
|
|
202
|
+
assertNotPluginOwned(site);
|
|
203
|
+
return site;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Record the size of site `id` on disk in sites.json, re-reading the file
|
|
208
|
+
* under the sites lock — never a copy read before an upload, which would
|
|
209
|
+
* undo every change made meanwhile.
|
|
210
|
+
*
|
|
211
|
+
* @returns {Promise<number>} the size
|
|
212
|
+
*/
|
|
213
|
+
async function recordSiteSize(id) {
|
|
214
|
+
const totalSize = await getSiteSize(id);
|
|
215
|
+
await withSitesLock(async () => {
|
|
216
|
+
const sites = await readSites();
|
|
217
|
+
const site = sites.find((s) => s.id === id);
|
|
218
|
+
if (!site) return;
|
|
219
|
+
site.totalSize = totalSize;
|
|
220
|
+
await writeSites(sites);
|
|
221
|
+
});
|
|
222
|
+
return totalSize;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Sites as listed: a plugin-owned one with `webSurface: { served, reason }`
|
|
227
|
+
* — whether it serves its plugin's shell and API now, or answers 503 and why.
|
|
228
|
+
*/
|
|
229
|
+
async function withWebSurfaceState(sites) {
|
|
230
|
+
if (!sites.some((site) => site.owner)) return sites;
|
|
231
|
+
const tunnels = await readTunnels();
|
|
232
|
+
return Promise.all(
|
|
233
|
+
sites.map(async (site) => {
|
|
234
|
+
if (!site.owner) return site;
|
|
235
|
+
const serving = await webSurfaceServing(site.owner.plugin, tunnels);
|
|
236
|
+
return {
|
|
237
|
+
...site,
|
|
238
|
+
webSurface: { served: serving.served, reason: serving.served ? null : serving.reason },
|
|
239
|
+
};
|
|
240
|
+
}),
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
137
244
|
function assertAgentSiteAccess(request, site) {
|
|
138
245
|
if (request.certRole === 'agent') {
|
|
139
246
|
const allowed = request.certAllowedSites || [];
|
|
@@ -151,7 +258,6 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
151
258
|
const filesDeps = buildFilesDeps();
|
|
152
259
|
const siteStateDeps = buildSiteStateDeps();
|
|
153
260
|
const tunnelReadDeps = buildTunnelReadDeps();
|
|
154
|
-
const autheliaDeps = buildAutheliaDeps();
|
|
155
261
|
|
|
156
262
|
// GET /api/sites
|
|
157
263
|
//
|
|
@@ -191,7 +297,7 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
191
297
|
}
|
|
192
298
|
|
|
193
299
|
const total = sites.length;
|
|
194
|
-
const windowed = sites.slice(offset, offset + limit);
|
|
300
|
+
const windowed = await withWebSurfaceState(sites.slice(offset, offset + limit));
|
|
195
301
|
|
|
196
302
|
return { sites: windowed, total, limit, offset };
|
|
197
303
|
},
|
|
@@ -212,24 +318,34 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
212
318
|
error: 'Domain and email must be configured before creating sites',
|
|
213
319
|
});
|
|
214
320
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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,
|
|
321
|
+
if (body.domain !== undefined && body.type !== 'managed') {
|
|
322
|
+
return reply.code(400).send({
|
|
323
|
+
error: "A custom site's hostname names its domain — `domain` is for managed sites",
|
|
232
324
|
});
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
// The domain is resolved under the hostname locks — those a domain's
|
|
328
|
+
// removal takes — so the site never lands on a removed domain.
|
|
329
|
+
const result = await withHostnameLocks(async () =>
|
|
330
|
+
createSite({
|
|
331
|
+
name: body.name,
|
|
332
|
+
type: body.type,
|
|
333
|
+
customDomain: body.customDomain,
|
|
334
|
+
aliases: body.aliases,
|
|
335
|
+
spaMode: body.spaMode,
|
|
336
|
+
autheliaProtected: body.autheliaProtected,
|
|
337
|
+
domain: await siteDomain(body.domain),
|
|
338
|
+
domains: await activeDomainNames(),
|
|
339
|
+
relayDomains: await domainNames(),
|
|
340
|
+
email: config.email,
|
|
341
|
+
nginx: nginxDeps,
|
|
342
|
+
certbot: certbotDeps,
|
|
343
|
+
files: filesDeps,
|
|
344
|
+
siteState: siteStateDeps,
|
|
345
|
+
tunnelState: tunnelReadDeps,
|
|
346
|
+
logger: request.log,
|
|
347
|
+
}),
|
|
348
|
+
);
|
|
233
349
|
|
|
234
350
|
const response = { ok: true, site: result.site };
|
|
235
351
|
if (result.message) response.message = result.message;
|
|
@@ -261,12 +377,15 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
261
377
|
const { id } = IdParamSchema.parse(request.params);
|
|
262
378
|
|
|
263
379
|
try {
|
|
264
|
-
await
|
|
265
|
-
id
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
380
|
+
await withSitesLock(async () => {
|
|
381
|
+
await siteForManagement(id);
|
|
382
|
+
await deleteSite({
|
|
383
|
+
id,
|
|
384
|
+
nginx: nginxDeps,
|
|
385
|
+
files: filesDeps,
|
|
386
|
+
siteState: siteStateDeps,
|
|
387
|
+
logger: request.log,
|
|
388
|
+
});
|
|
270
389
|
});
|
|
271
390
|
return { ok: true };
|
|
272
391
|
} catch (err) {
|
|
@@ -277,6 +396,7 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
277
396
|
details: err.message,
|
|
278
397
|
});
|
|
279
398
|
}
|
|
399
|
+
if (err instanceof AppError) throw err;
|
|
280
400
|
request.log.error(err, 'Failed to delete site');
|
|
281
401
|
return reply.code(500).send({
|
|
282
402
|
error: 'Failed to delete site',
|
|
@@ -298,21 +418,25 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
298
418
|
const config = getConfig();
|
|
299
419
|
|
|
300
420
|
try {
|
|
301
|
-
const result = await
|
|
302
|
-
id
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
421
|
+
const result = await withHostnameLocks(async () => {
|
|
422
|
+
await siteForManagement(id);
|
|
423
|
+
return updateSite({
|
|
424
|
+
id,
|
|
425
|
+
spaMode: body.spaMode,
|
|
426
|
+
autheliaProtected: body.autheliaProtected,
|
|
427
|
+
allowedUsers: body.allowedUsers,
|
|
428
|
+
aliases: body.aliases,
|
|
429
|
+
domains: await activeDomainNames(),
|
|
430
|
+
relayDomains: await domainNames(),
|
|
431
|
+
email: config.email,
|
|
432
|
+
serverIp: config.ip,
|
|
433
|
+
nginx: nginxDeps,
|
|
434
|
+
certbot: certbotDeps,
|
|
435
|
+
siteState: siteStateDeps,
|
|
436
|
+
tunnelState: tunnelReadDeps,
|
|
437
|
+
authelia: buildAutheliaDeps(request.log),
|
|
438
|
+
logger: request.log,
|
|
439
|
+
});
|
|
316
440
|
});
|
|
317
441
|
return result;
|
|
318
442
|
} catch (err) {
|
|
@@ -330,6 +454,7 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
330
454
|
details: err.message,
|
|
331
455
|
});
|
|
332
456
|
}
|
|
457
|
+
if (err instanceof AppError) throw err;
|
|
333
458
|
request.log.error(err, 'Failed to update site');
|
|
334
459
|
return reply.code(500).send({
|
|
335
460
|
error: 'Failed to update site configuration',
|
|
@@ -353,15 +478,18 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
353
478
|
const config = getConfig();
|
|
354
479
|
|
|
355
480
|
try {
|
|
356
|
-
const result = await
|
|
357
|
-
id
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
481
|
+
const result = await withHostnameLocks(async () => {
|
|
482
|
+
await siteForManagement(id);
|
|
483
|
+
return verifyDns({
|
|
484
|
+
id,
|
|
485
|
+
serverIp: config.ip,
|
|
486
|
+
domains: await activeDomainNames(),
|
|
487
|
+
email: config.email,
|
|
488
|
+
nginx: nginxDeps,
|
|
489
|
+
certbot: certbotDeps,
|
|
490
|
+
siteState: siteStateDeps,
|
|
491
|
+
logger: request.log,
|
|
492
|
+
});
|
|
365
493
|
});
|
|
366
494
|
return result;
|
|
367
495
|
} catch (err) {
|
|
@@ -371,6 +499,7 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
371
499
|
error: err.message,
|
|
372
500
|
});
|
|
373
501
|
}
|
|
502
|
+
if (err instanceof AppError) throw err;
|
|
374
503
|
request.log.error(err, 'Failed to verify DNS');
|
|
375
504
|
return reply.code(500).send({
|
|
376
505
|
error: 'DNS verification failed',
|
|
@@ -390,8 +519,7 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
390
519
|
const { id } = IdParamSchema.parse(request.params);
|
|
391
520
|
const { path: relativePath } = PathQuerySchema.parse(request.query);
|
|
392
521
|
|
|
393
|
-
const
|
|
394
|
-
const site = sites.find((s) => s.id === id);
|
|
522
|
+
const site = await siteForManagement(id);
|
|
395
523
|
if (!site) {
|
|
396
524
|
return reply.code(404).send({ error: 'Site not found' });
|
|
397
525
|
}
|
|
@@ -417,8 +545,7 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
417
545
|
const { id } = IdParamSchema.parse(request.params);
|
|
418
546
|
const config = getConfig();
|
|
419
547
|
|
|
420
|
-
const
|
|
421
|
-
const site = sites.find((s) => s.id === id);
|
|
548
|
+
const site = await siteForManagement(id);
|
|
422
549
|
if (!site) {
|
|
423
550
|
return reply.code(404).send({ error: 'Site not found' });
|
|
424
551
|
}
|
|
@@ -450,13 +577,9 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
450
577
|
|
|
451
578
|
// Update site size
|
|
452
579
|
try {
|
|
453
|
-
const totalSize = await
|
|
454
|
-
const siteIndex = sites.findIndex((s) => s.id === id);
|
|
455
|
-
sites[siteIndex].totalSize = totalSize;
|
|
456
|
-
|
|
580
|
+
const totalSize = await recordSiteSize(id);
|
|
457
581
|
if (totalSize > config.maxSiteSize) {
|
|
458
582
|
// Don't block but warn
|
|
459
|
-
await writeSites(sites);
|
|
460
583
|
return reply.code(200).send({
|
|
461
584
|
ok: true,
|
|
462
585
|
files: uploadedFiles,
|
|
@@ -464,8 +587,6 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
464
587
|
totalSize,
|
|
465
588
|
});
|
|
466
589
|
}
|
|
467
|
-
|
|
468
|
-
await writeSites(sites);
|
|
469
590
|
} catch {
|
|
470
591
|
// Non-critical — don't fail the upload
|
|
471
592
|
}
|
|
@@ -483,8 +604,7 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
483
604
|
async (request, reply) => {
|
|
484
605
|
const { id } = IdParamSchema.parse(request.params);
|
|
485
606
|
|
|
486
|
-
const
|
|
487
|
-
const site = sites.find((s) => s.id === id);
|
|
607
|
+
const site = await siteForManagement(id);
|
|
488
608
|
if (!site) {
|
|
489
609
|
return reply.code(404).send({ error: 'Site not found' });
|
|
490
610
|
}
|
|
@@ -501,10 +621,7 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
501
621
|
|
|
502
622
|
// Update site size
|
|
503
623
|
try {
|
|
504
|
-
|
|
505
|
-
const siteIndex = sites.findIndex((s) => s.id === id);
|
|
506
|
-
sites[siteIndex].totalSize = totalSize;
|
|
507
|
-
await writeSites(sites);
|
|
624
|
+
await recordSiteSize(id);
|
|
508
625
|
} catch {
|
|
509
626
|
// Non-critical
|
|
510
627
|
}
|