@lamalibre/create-lamaste 4.5.0 → 4.6.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/lamaste-priv +34 -2
- 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/certificates.md +3 -3
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/security-model.md +2 -2
- 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/onboarding.md +2 -2
- 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 +65 -7
- 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 +2 -2
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +112 -37
- package/vendor/docs/04-api-reference/app-host.md +3 -3
- package/vendor/docs/04-api-reference/gatekeeper.md +19 -1
- 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/sites.md +4 -0
- package/vendor/docs/04-api-reference/tunnels.md +21 -19
- 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/gatekeeper-design.md +8 -8
- package/vendor/docs/identity-directory-design.md +3 -3
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- package/vendor/gatekeeper/dist/cli/index.js +0 -0
- 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/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 +2 -1
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +29 -10
- 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 +103 -22
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
- package/vendor/gatekeeper/package.json +2 -2
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/plugin-web-surface.js +269 -0
- package/vendor/server/src/commands/plugins.js +63 -6
- package/vendor/server/src/commands/sites.js +15 -1
- 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-BZwQ_aQc.css +1 -0
- package/vendor/server-ui/dist/assets/index-BvEfCZYS.js +84 -0
- package/vendor/server-ui/dist/index.html +2 -2
- package/vendor/serverd/package.json +2 -2
- package/vendor/serverd/src/index.js +9 -4
- package/vendor/serverd/src/lib/chisel-runtime.js +18 -6
- package/vendor/serverd/src/lib/chisel.js +10 -2
- package/vendor/serverd/src/lib/gatekeeper-client.js +18 -7
- package/vendor/serverd/src/lib/hook-pipelines.js +8 -3
- package/vendor/serverd/src/lib/migrations/state/0015_plugin_web_surface.sql +14 -0
- package/vendor/serverd/src/lib/nginx.js +269 -2
- package/vendor/serverd/src/lib/plugin-hooks.js +40 -11
- package/vendor/serverd/src/lib/plugin-lifecycle.js +128 -5
- package/vendor/serverd/src/lib/plugin-packages.js +74 -2
- package/vendor/serverd/src/lib/plugins.js +39 -6
- package/vendor/serverd/src/lib/state.js +18 -0
- package/vendor/serverd/src/lib/user-surface.js +48 -27
- package/vendor/serverd/src/lib/vhost-reconcile.js +5 -3
- package/vendor/serverd/src/lib/web-surface.js +766 -0
- package/vendor/serverd/src/routes/management/plugin-web-surface.js +151 -0
- package/vendor/serverd/src/routes/management/plugins.js +2 -0
- package/vendor/serverd/src/routes/management/sites.js +143 -65
- package/vendor/serverd/src/routes/management/tunnels.js +11 -20
- package/vendor/serverd/src/routes/management.js +2 -0
- package/vendor/server-ui/dist/assets/index-D-fer5ef.css +0 -1
- package/vendor/server-ui/dist/assets/index-DSaDPJaw.js +0 -84
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A plugin's web surface (slice 7): the site it owns, where the relay
|
|
3
|
+
* serves the shell its server package ships beside its user surface's API.
|
|
4
|
+
* Admin only. Business logic lives in `lib/web-surface.js`.
|
|
5
|
+
*
|
|
6
|
+
* GET /plugins/:name/web-surface what the installed version declares, the
|
|
7
|
+
* site, whether it is served, the trees, the offer
|
|
8
|
+
* POST /plugins/:name/web-surface { subdomain?, accessMode? } — create the site
|
|
9
|
+
* POST /plugins/:name/web-surface/move { tunnelId } — take over the bound tunnel's host
|
|
10
|
+
* DELETE /plugins/:name/web-surface delete the site and its trees
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { WEB_SURFACE_ACCESS_MODES, derivePluginRoute } from '@lamalibre/lamaste';
|
|
15
|
+
import { actorFromRequest } from '../../lib/actor.js';
|
|
16
|
+
import { findPlugin } from '../../lib/plugins.js';
|
|
17
|
+
import {
|
|
18
|
+
createWebSurface,
|
|
19
|
+
deleteWebSurface,
|
|
20
|
+
moveWebSurfaceFromTunnel,
|
|
21
|
+
webSurfaceStatus,
|
|
22
|
+
} from '../../lib/web-surface.js';
|
|
23
|
+
|
|
24
|
+
const PluginNameParamSchema = z.object({
|
|
25
|
+
name: z
|
|
26
|
+
.string()
|
|
27
|
+
.min(1)
|
|
28
|
+
.max(100)
|
|
29
|
+
.regex(/^[a-z0-9-]+$/, 'Plugin name must contain only lowercase letters, numbers, and hyphens'),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const CreateBodySchema = z
|
|
33
|
+
.object({
|
|
34
|
+
subdomain: z
|
|
35
|
+
.string()
|
|
36
|
+
.min(1)
|
|
37
|
+
.max(63)
|
|
38
|
+
.regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, 'subdomain is a DNS label')
|
|
39
|
+
.optional(),
|
|
40
|
+
accessMode: z.enum(WEB_SURFACE_ACCESS_MODES).optional(),
|
|
41
|
+
})
|
|
42
|
+
.strict();
|
|
43
|
+
|
|
44
|
+
const MoveBodySchema = z.object({ tunnelId: z.string().min(1).max(64) }).strict();
|
|
45
|
+
|
|
46
|
+
/** The refusal a library threw (its `statusCode`, message and `code`), or a 500. */
|
|
47
|
+
function sendFailure(request, reply, err, fallback) {
|
|
48
|
+
if (Number.isInteger(err.statusCode) && err.statusCode < 500) {
|
|
49
|
+
return reply
|
|
50
|
+
.code(err.statusCode)
|
|
51
|
+
.send({ error: err.message, ...(err.code ? { code: err.code } : {}) });
|
|
52
|
+
}
|
|
53
|
+
request.log.error(err, fallback);
|
|
54
|
+
return reply
|
|
55
|
+
.code(Number.isInteger(err.statusCode) ? err.statusCode : 500)
|
|
56
|
+
.send({ error: fallback, details: err.message });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The route plugin `:name` holds: the installed plugin's, else the one its
|
|
61
|
+
* name derives — a site outlives its plugin, and its status and deletion
|
|
62
|
+
* stay reachable (creating or moving refuses without an installed plugin).
|
|
63
|
+
*/
|
|
64
|
+
async function routeOf(request) {
|
|
65
|
+
const { name } = PluginNameParamSchema.parse(request.params);
|
|
66
|
+
const plugin = await findPlugin(name);
|
|
67
|
+
return plugin?.route ?? derivePluginRoute(name);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default async function pluginWebSurfaceRoutes(fastify, _opts) {
|
|
71
|
+
fastify.get(
|
|
72
|
+
'/plugins/:name/web-surface',
|
|
73
|
+
{ preHandler: fastify.requireRole(['admin']) },
|
|
74
|
+
async (request, reply) => {
|
|
75
|
+
const route = await routeOf(request);
|
|
76
|
+
try {
|
|
77
|
+
return { webSurface: await webSurfaceStatus(route) };
|
|
78
|
+
} catch (err) {
|
|
79
|
+
return sendFailure(request, reply, err, 'Failed to read the web surface');
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
fastify.post(
|
|
85
|
+
'/plugins/:name/web-surface',
|
|
86
|
+
{
|
|
87
|
+
preHandler: fastify.requireRole(['admin']),
|
|
88
|
+
// Issues a certificate and reloads nginx.
|
|
89
|
+
config: { rateLimit: { max: 10, timeWindow: '1 minute' } },
|
|
90
|
+
},
|
|
91
|
+
async (request, reply) => {
|
|
92
|
+
const route = await routeOf(request);
|
|
93
|
+
const body = CreateBodySchema.parse(request.body ?? {});
|
|
94
|
+
try {
|
|
95
|
+
const webSurface = await createWebSurface(route, body, {
|
|
96
|
+
actor: actorFromRequest(request),
|
|
97
|
+
logger: request.log,
|
|
98
|
+
});
|
|
99
|
+
return reply.code(201).send({ webSurface });
|
|
100
|
+
} catch (err) {
|
|
101
|
+
return sendFailure(request, reply, err, 'Failed to create the web surface');
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
fastify.post(
|
|
107
|
+
'/plugins/:name/web-surface/move',
|
|
108
|
+
{
|
|
109
|
+
preHandler: fastify.requireRole(['admin']),
|
|
110
|
+
config: { rateLimit: { max: 10, timeWindow: '1 minute' } },
|
|
111
|
+
},
|
|
112
|
+
async (request, reply) => {
|
|
113
|
+
const route = await routeOf(request);
|
|
114
|
+
const { tunnelId } = MoveBodySchema.parse(request.body);
|
|
115
|
+
try {
|
|
116
|
+
return {
|
|
117
|
+
webSurface: await moveWebSurfaceFromTunnel(route, tunnelId, {
|
|
118
|
+
actor: actorFromRequest(request),
|
|
119
|
+
logger: request.log,
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
} catch (err) {
|
|
123
|
+
return sendFailure(
|
|
124
|
+
request,
|
|
125
|
+
reply,
|
|
126
|
+
err,
|
|
127
|
+
'Failed to move the user surface to the web surface',
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
fastify.delete(
|
|
134
|
+
'/plugins/:name/web-surface',
|
|
135
|
+
{
|
|
136
|
+
preHandler: fastify.requireRole(['admin']),
|
|
137
|
+
config: { rateLimit: { max: 10, timeWindow: '1 minute' } },
|
|
138
|
+
},
|
|
139
|
+
async (request, reply) => {
|
|
140
|
+
const route = await routeOf(request);
|
|
141
|
+
try {
|
|
142
|
+
return await deleteWebSurface(route, {
|
|
143
|
+
actor: actorFromRequest(request),
|
|
144
|
+
logger: request.log,
|
|
145
|
+
});
|
|
146
|
+
} catch (err) {
|
|
147
|
+
return sendFailure(request, reply, err, 'Failed to delete the web surface');
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -301,6 +301,8 @@ async function withHooks(plugin) {
|
|
|
301
301
|
agent: status.agent !== null,
|
|
302
302
|
// Lamaste installs the agent half (agent.package); false: none, or installed by hand.
|
|
303
303
|
agentHalf: status.agentHalf !== null,
|
|
304
|
+
// The installed version declares a web surface (its tree is in the approval).
|
|
305
|
+
web: status.web.declared,
|
|
304
306
|
approval: status.approval.state,
|
|
305
307
|
autoApprove: status.autoApprove,
|
|
306
308
|
failOpenCount: status.relay?.failOpenCount ?? 0,
|
|
@@ -11,9 +11,12 @@ import {
|
|
|
11
11
|
updateSite,
|
|
12
12
|
verifyDns,
|
|
13
13
|
SiteError,
|
|
14
|
+
withTunnelLock,
|
|
14
15
|
} from '@lamalibre/lamaste/server';
|
|
16
|
+
import { AppError } from '../../lib/app-error.js';
|
|
17
|
+
import { webSurfaceServing } from '../../lib/web-surface.js';
|
|
15
18
|
import { getConfig } from '../../lib/config.js';
|
|
16
|
-
import { readSites, writeSites, readTunnels } from '../../lib/state.js';
|
|
19
|
+
import { readSites, writeSites, readTunnels, withSitesLock } from '../../lib/state.js';
|
|
17
20
|
import { writeStaticSiteVhost, removeStaticSiteVhost } from '../../lib/nginx.js';
|
|
18
21
|
import { updateAccessControl } from '../../lib/authelia.js';
|
|
19
22
|
import { issueTunnelCert, getCertPath, issueSiteCert } from '../../lib/certbot.js';
|
|
@@ -134,6 +137,79 @@ function siteErrorStatus(code) {
|
|
|
134
137
|
}
|
|
135
138
|
}
|
|
136
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Run a site workflow that may give a site a hostname: under core's tunnel
|
|
142
|
+
* lock, then the sites lock — the order every such workflow takes them in
|
|
143
|
+
* (`withSitesLock`) — so no tunnel takes the name meanwhile.
|
|
144
|
+
*
|
|
145
|
+
* @template T
|
|
146
|
+
* @param {() => Promise<T>} fn
|
|
147
|
+
* @returns {Promise<T>}
|
|
148
|
+
*/
|
|
149
|
+
function withHostnameLocks(fn) {
|
|
150
|
+
return withTunnelLock(() => withSitesLock(fn));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Refuse (409) a site a plugin owns: its files are the plugin's approved
|
|
155
|
+
* web tree and its settings follow the plugin — it is managed through the
|
|
156
|
+
* plugin's web surface, never here (and never by `lamaste-agent deploy`).
|
|
157
|
+
*/
|
|
158
|
+
function assertNotPluginOwned(site) {
|
|
159
|
+
if (site?.owner) {
|
|
160
|
+
throw new AppError(
|
|
161
|
+
`Site '${site.name}' is the web surface of plugin '${site.owner.plugin}' — manage it through the plugin`,
|
|
162
|
+
409,
|
|
163
|
+
{ code: 'plugin_owned_site' },
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The site `id`, refused when a plugin owns it; null when there is none. */
|
|
169
|
+
async function siteForManagement(id) {
|
|
170
|
+
const site = (await readSites()).find((s) => s.id === id) ?? null;
|
|
171
|
+
assertNotPluginOwned(site);
|
|
172
|
+
return site;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Record the size of site `id` on disk in sites.json, re-reading the file
|
|
177
|
+
* under the sites lock — never a copy read before an upload, which would
|
|
178
|
+
* undo every change made meanwhile.
|
|
179
|
+
*
|
|
180
|
+
* @returns {Promise<number>} the size
|
|
181
|
+
*/
|
|
182
|
+
async function recordSiteSize(id) {
|
|
183
|
+
const totalSize = await getSiteSize(id);
|
|
184
|
+
await withSitesLock(async () => {
|
|
185
|
+
const sites = await readSites();
|
|
186
|
+
const site = sites.find((s) => s.id === id);
|
|
187
|
+
if (!site) return;
|
|
188
|
+
site.totalSize = totalSize;
|
|
189
|
+
await writeSites(sites);
|
|
190
|
+
});
|
|
191
|
+
return totalSize;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Sites as listed: a plugin-owned one with `webSurface: { served, reason }`
|
|
196
|
+
* — whether it serves its plugin's shell and API now, or answers 503 and why.
|
|
197
|
+
*/
|
|
198
|
+
async function withWebSurfaceState(sites) {
|
|
199
|
+
if (!sites.some((site) => site.owner)) return sites;
|
|
200
|
+
const tunnels = await readTunnels();
|
|
201
|
+
return Promise.all(
|
|
202
|
+
sites.map(async (site) => {
|
|
203
|
+
if (!site.owner) return site;
|
|
204
|
+
const serving = await webSurfaceServing(site.owner.plugin, tunnels);
|
|
205
|
+
return {
|
|
206
|
+
...site,
|
|
207
|
+
webSurface: { served: serving.served, reason: serving.served ? null : serving.reason },
|
|
208
|
+
};
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
137
213
|
function assertAgentSiteAccess(request, site) {
|
|
138
214
|
if (request.certRole === 'agent') {
|
|
139
215
|
const allowed = request.certAllowedSites || [];
|
|
@@ -191,7 +267,7 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
191
267
|
}
|
|
192
268
|
|
|
193
269
|
const total = sites.length;
|
|
194
|
-
const windowed = sites.slice(offset, offset + limit);
|
|
270
|
+
const windowed = await withWebSurfaceState(sites.slice(offset, offset + limit));
|
|
195
271
|
|
|
196
272
|
return { sites: windowed, total, limit, offset };
|
|
197
273
|
},
|
|
@@ -214,22 +290,24 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
214
290
|
}
|
|
215
291
|
|
|
216
292
|
try {
|
|
217
|
-
const result = await
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
293
|
+
const result = await withHostnameLocks(() =>
|
|
294
|
+
createSite({
|
|
295
|
+
name: body.name,
|
|
296
|
+
type: body.type,
|
|
297
|
+
customDomain: body.customDomain,
|
|
298
|
+
aliases: body.aliases,
|
|
299
|
+
spaMode: body.spaMode,
|
|
300
|
+
autheliaProtected: body.autheliaProtected,
|
|
301
|
+
domain: config.domain,
|
|
302
|
+
email: config.email,
|
|
303
|
+
nginx: nginxDeps,
|
|
304
|
+
certbot: certbotDeps,
|
|
305
|
+
files: filesDeps,
|
|
306
|
+
siteState: siteStateDeps,
|
|
307
|
+
tunnelState: tunnelReadDeps,
|
|
308
|
+
logger: request.log,
|
|
309
|
+
}),
|
|
310
|
+
);
|
|
233
311
|
|
|
234
312
|
const response = { ok: true, site: result.site };
|
|
235
313
|
if (result.message) response.message = result.message;
|
|
@@ -261,12 +339,15 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
261
339
|
const { id } = IdParamSchema.parse(request.params);
|
|
262
340
|
|
|
263
341
|
try {
|
|
264
|
-
await
|
|
265
|
-
id
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
342
|
+
await withSitesLock(async () => {
|
|
343
|
+
await siteForManagement(id);
|
|
344
|
+
await deleteSite({
|
|
345
|
+
id,
|
|
346
|
+
nginx: nginxDeps,
|
|
347
|
+
files: filesDeps,
|
|
348
|
+
siteState: siteStateDeps,
|
|
349
|
+
logger: request.log,
|
|
350
|
+
});
|
|
270
351
|
});
|
|
271
352
|
return { ok: true };
|
|
272
353
|
} catch (err) {
|
|
@@ -277,6 +358,7 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
277
358
|
details: err.message,
|
|
278
359
|
});
|
|
279
360
|
}
|
|
361
|
+
if (err instanceof AppError) throw err;
|
|
280
362
|
request.log.error(err, 'Failed to delete site');
|
|
281
363
|
return reply.code(500).send({
|
|
282
364
|
error: 'Failed to delete site',
|
|
@@ -298,21 +380,24 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
298
380
|
const config = getConfig();
|
|
299
381
|
|
|
300
382
|
try {
|
|
301
|
-
const result = await
|
|
302
|
-
id
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
383
|
+
const result = await withHostnameLocks(async () => {
|
|
384
|
+
await siteForManagement(id);
|
|
385
|
+
return updateSite({
|
|
386
|
+
id,
|
|
387
|
+
spaMode: body.spaMode,
|
|
388
|
+
autheliaProtected: body.autheliaProtected,
|
|
389
|
+
allowedUsers: body.allowedUsers,
|
|
390
|
+
aliases: body.aliases,
|
|
391
|
+
domain: config.domain,
|
|
392
|
+
email: config.email,
|
|
393
|
+
serverIp: config.ip,
|
|
394
|
+
nginx: nginxDeps,
|
|
395
|
+
certbot: certbotDeps,
|
|
396
|
+
siteState: siteStateDeps,
|
|
397
|
+
tunnelState: tunnelReadDeps,
|
|
398
|
+
authelia: autheliaDeps,
|
|
399
|
+
logger: request.log,
|
|
400
|
+
});
|
|
316
401
|
});
|
|
317
402
|
return result;
|
|
318
403
|
} catch (err) {
|
|
@@ -330,6 +415,7 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
330
415
|
details: err.message,
|
|
331
416
|
});
|
|
332
417
|
}
|
|
418
|
+
if (err instanceof AppError) throw err;
|
|
333
419
|
request.log.error(err, 'Failed to update site');
|
|
334
420
|
return reply.code(500).send({
|
|
335
421
|
error: 'Failed to update site configuration',
|
|
@@ -353,15 +439,18 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
353
439
|
const config = getConfig();
|
|
354
440
|
|
|
355
441
|
try {
|
|
356
|
-
const result = await
|
|
357
|
-
id
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
442
|
+
const result = await withHostnameLocks(async () => {
|
|
443
|
+
await siteForManagement(id);
|
|
444
|
+
return verifyDns({
|
|
445
|
+
id,
|
|
446
|
+
serverIp: config.ip,
|
|
447
|
+
domain: config.domain,
|
|
448
|
+
email: config.email,
|
|
449
|
+
nginx: nginxDeps,
|
|
450
|
+
certbot: certbotDeps,
|
|
451
|
+
siteState: siteStateDeps,
|
|
452
|
+
logger: request.log,
|
|
453
|
+
});
|
|
365
454
|
});
|
|
366
455
|
return result;
|
|
367
456
|
} catch (err) {
|
|
@@ -371,6 +460,7 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
371
460
|
error: err.message,
|
|
372
461
|
});
|
|
373
462
|
}
|
|
463
|
+
if (err instanceof AppError) throw err;
|
|
374
464
|
request.log.error(err, 'Failed to verify DNS');
|
|
375
465
|
return reply.code(500).send({
|
|
376
466
|
error: 'DNS verification failed',
|
|
@@ -390,8 +480,7 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
390
480
|
const { id } = IdParamSchema.parse(request.params);
|
|
391
481
|
const { path: relativePath } = PathQuerySchema.parse(request.query);
|
|
392
482
|
|
|
393
|
-
const
|
|
394
|
-
const site = sites.find((s) => s.id === id);
|
|
483
|
+
const site = await siteForManagement(id);
|
|
395
484
|
if (!site) {
|
|
396
485
|
return reply.code(404).send({ error: 'Site not found' });
|
|
397
486
|
}
|
|
@@ -417,8 +506,7 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
417
506
|
const { id } = IdParamSchema.parse(request.params);
|
|
418
507
|
const config = getConfig();
|
|
419
508
|
|
|
420
|
-
const
|
|
421
|
-
const site = sites.find((s) => s.id === id);
|
|
509
|
+
const site = await siteForManagement(id);
|
|
422
510
|
if (!site) {
|
|
423
511
|
return reply.code(404).send({ error: 'Site not found' });
|
|
424
512
|
}
|
|
@@ -450,13 +538,9 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
450
538
|
|
|
451
539
|
// Update site size
|
|
452
540
|
try {
|
|
453
|
-
const totalSize = await
|
|
454
|
-
const siteIndex = sites.findIndex((s) => s.id === id);
|
|
455
|
-
sites[siteIndex].totalSize = totalSize;
|
|
456
|
-
|
|
541
|
+
const totalSize = await recordSiteSize(id);
|
|
457
542
|
if (totalSize > config.maxSiteSize) {
|
|
458
543
|
// Don't block but warn
|
|
459
|
-
await writeSites(sites);
|
|
460
544
|
return reply.code(200).send({
|
|
461
545
|
ok: true,
|
|
462
546
|
files: uploadedFiles,
|
|
@@ -464,8 +548,6 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
464
548
|
totalSize,
|
|
465
549
|
});
|
|
466
550
|
}
|
|
467
|
-
|
|
468
|
-
await writeSites(sites);
|
|
469
551
|
} catch {
|
|
470
552
|
// Non-critical — don't fail the upload
|
|
471
553
|
}
|
|
@@ -483,8 +565,7 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
483
565
|
async (request, reply) => {
|
|
484
566
|
const { id } = IdParamSchema.parse(request.params);
|
|
485
567
|
|
|
486
|
-
const
|
|
487
|
-
const site = sites.find((s) => s.id === id);
|
|
568
|
+
const site = await siteForManagement(id);
|
|
488
569
|
if (!site) {
|
|
489
570
|
return reply.code(404).send({ error: 'Site not found' });
|
|
490
571
|
}
|
|
@@ -501,10 +582,7 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
501
582
|
|
|
502
583
|
// Update site size
|
|
503
584
|
try {
|
|
504
|
-
|
|
505
|
-
const siteIndex = sites.findIndex((s) => s.id === id);
|
|
506
|
-
sites[siteIndex].totalSize = totalSize;
|
|
507
|
-
await writeSites(sites);
|
|
585
|
+
await recordSiteSize(id);
|
|
508
586
|
} catch {
|
|
509
587
|
// Non-critical
|
|
510
588
|
}
|
|
@@ -220,10 +220,16 @@ function maxBodyCeiling(request) {
|
|
|
220
220
|
return request.certRole === 'admin' ? TUNNEL_MAX_BODY_MB.max : TUNNEL_MAX_BODY_MB.agentMax;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
/**
|
|
223
|
+
/**
|
|
224
|
+
* Core `createTunnel`'s `hostTaken`: the static site (or one of its
|
|
225
|
+
* aliases) that already answers on `fqdn` — two vhosts with one
|
|
226
|
+
* server_name make nginx pick silently. Asked under the tunnel lock, which
|
|
227
|
+
* every workflow giving a site a hostname holds too.
|
|
228
|
+
*/
|
|
224
229
|
async function siteServing(fqdn) {
|
|
225
230
|
const sites = await readSites();
|
|
226
|
-
|
|
231
|
+
const site = sites.find((s) => s.fqdn === fqdn || (s.aliases ?? []).includes(fqdn));
|
|
232
|
+
return site ? `by a static site ('${site.name}')` : null;
|
|
227
233
|
}
|
|
228
234
|
|
|
229
235
|
/**
|
|
@@ -496,16 +502,6 @@ export default async function tunnelRoutes(fastify, _opts) {
|
|
|
496
502
|
});
|
|
497
503
|
}
|
|
498
504
|
|
|
499
|
-
// A static site (or one of its aliases) may already answer on this
|
|
500
|
-
// hostname; two vhosts with one server_name make nginx pick silently.
|
|
501
|
-
const fqdn = `${subdomain}.${config.domain}`;
|
|
502
|
-
if (await siteServing(fqdn)) {
|
|
503
|
-
return reply.code(400).send({
|
|
504
|
-
error: 'Failed to create tunnel',
|
|
505
|
-
details: `Domain '${fqdn}' is already served by a static site`,
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
|
|
509
505
|
try {
|
|
510
506
|
const tunnel = await createTunnel({
|
|
511
507
|
subdomain,
|
|
@@ -523,6 +519,7 @@ export default async function tunnelRoutes(fastify, _opts) {
|
|
|
523
519
|
chisel: chiselDeps,
|
|
524
520
|
state: stateDeps,
|
|
525
521
|
agents: agentDeps,
|
|
522
|
+
hostTaken: siteServing,
|
|
526
523
|
logger: request.log,
|
|
527
524
|
});
|
|
528
525
|
return reply.code(201).send({ ok: true, tunnel });
|
|
@@ -600,6 +597,7 @@ export default async function tunnelRoutes(fastify, _opts) {
|
|
|
600
597
|
state: stateDeps,
|
|
601
598
|
agents: agentDeps,
|
|
602
599
|
userSurface: userSurfaceDeps,
|
|
600
|
+
hostTaken: siteServing,
|
|
603
601
|
logger: request.log,
|
|
604
602
|
authorize: authorizeFor(request, 'change'),
|
|
605
603
|
});
|
|
@@ -748,14 +746,6 @@ export default async function tunnelRoutes(fastify, _opts) {
|
|
|
748
746
|
});
|
|
749
747
|
}
|
|
750
748
|
|
|
751
|
-
const fqdn = `${subdomain}.${config.domain}`;
|
|
752
|
-
if (await siteServing(fqdn)) {
|
|
753
|
-
return reply.code(400).send({
|
|
754
|
-
error: 'Failed to expose agent panel',
|
|
755
|
-
details: `Domain '${fqdn}' is already served by a static site`,
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
|
|
759
749
|
// Check if a panel tunnel already exists for this agent
|
|
760
750
|
const existing = await readTunnels();
|
|
761
751
|
const existingPanel = existing.find((t) => t.type === 'panel' && t.subdomain === subdomain);
|
|
@@ -780,6 +770,7 @@ export default async function tunnelRoutes(fastify, _opts) {
|
|
|
780
770
|
chisel: chiselDeps,
|
|
781
771
|
state: stateDeps,
|
|
782
772
|
agents: agentDeps,
|
|
773
|
+
hostTaken: siteServing,
|
|
783
774
|
logger: request.log,
|
|
784
775
|
});
|
|
785
776
|
return reply.code(201).send({ ok: true, tunnel });
|
|
@@ -13,6 +13,7 @@ import ticketRoutes from './management/tickets.js';
|
|
|
13
13
|
import identityRoutes from './management/identity.js';
|
|
14
14
|
import identityDirectoryRoutes from './management/identity-directory.js';
|
|
15
15
|
import pluginIdentityRoutes from './management/plugin-identity.js';
|
|
16
|
+
import pluginWebSurfaceRoutes from './management/plugin-web-surface.js';
|
|
16
17
|
import storageRoutes from './management/storage.js';
|
|
17
18
|
import gatekeeperProxyRoutes from './management/gatekeeper-proxy.js';
|
|
18
19
|
import agentsRoutes from './management/agents.js';
|
|
@@ -37,6 +38,7 @@ export default async function managementRoutes(fastify, _opts) {
|
|
|
37
38
|
await fastify.register(identityRoutes);
|
|
38
39
|
await fastify.register(identityDirectoryRoutes);
|
|
39
40
|
await fastify.register(pluginIdentityRoutes);
|
|
41
|
+
await fastify.register(pluginWebSurfaceRoutes);
|
|
40
42
|
await fastify.register(storageRoutes);
|
|
41
43
|
await fastify.register(gatekeeperProxyRoutes);
|
|
42
44
|
await fastify.register(agentsRoutes);
|