@acms-subir/core 0.3.12 → 0.3.13
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/CHANGELOG.md +6 -0
- package/cli/migrations.generated.mjs +16 -1
- package/integration.mjs +2 -0
- package/package.json +1 -1
- package/src/actions/index.ts +76 -59
- package/src/components/RolesManager.tsx +276 -0
- package/src/layouts/Admin.astro +8 -3
- package/src/lib/auth/accounts.ts +2 -2
- package/src/lib/auth/store.ts +4 -4
- package/src/lib/content-service.ts +14 -0
- package/src/lib/design-service.ts +11 -3
- package/src/lib/editor/http.ts +7 -0
- package/src/lib/editor/me.ts +12 -8
- package/src/lib/editor/roles.ts +14 -6
- package/src/lib/editor/schedule.ts +12 -12
- package/src/lib/editor/sections.ts +11 -4
- package/src/lib/editor/site-data.ts +4 -2
- package/src/lib/editor/translations.ts +2 -1
- package/src/lib/email/accounts.ts +2 -2
- package/src/lib/email/store.ts +3 -3
- package/src/lib/email/types.ts +1 -1
- package/src/lib/permissions/access.ts +73 -0
- package/src/lib/permissions/catalog.ts +147 -0
- package/src/lib/permissions/migration.ts +17 -0
- package/src/lib/permissions/service.ts +59 -0
- package/src/lib/permissions/store.ts +54 -0
- package/src/lib/repo/migrations.ts +2 -0
- package/src/lib/repo/sql.ts +3 -0
- package/src/lib/repo/types.ts +2 -0
- package/src/lib/roles.ts +9 -0
- package/src/pages/admin/c/[collection]/index.astro +6 -0
- package/src/pages/admin/calendar/bookings.astro +2 -0
- package/src/pages/admin/calendar/events/index.astro +11 -0
- package/src/pages/admin/calendar/resources/[id].astro +2 -0
- package/src/pages/admin/calendar/resources/index.astro +2 -0
- package/src/pages/admin/calendar/resources/link.astro +2 -0
- package/src/pages/admin/calendar/settings.astro +2 -0
- package/src/pages/admin/email.astro +2 -2
- package/src/pages/admin/forms/[id]/share.astro +2 -0
- package/src/pages/admin/forms/[id]/submissions.astro +2 -0
- package/src/pages/admin/forms/index.astro +2 -0
- package/src/pages/admin/import.astro +3 -6
- package/src/pages/admin/integrations.astro +10 -12
- package/src/pages/admin/live.astro +3 -3
- package/src/pages/admin/mobile.astro +3 -3
- package/src/pages/admin/pages/index.astro +8 -1
- package/src/pages/admin/roles.astro +80 -0
- package/src/pages/admin/settings.astro +31 -28
- package/src/pages/admin/tags.astro +2 -0
- package/src/pages/admin/users.astro +87 -41
- package/src/pages/api/ai/clean.ts +3 -2
- package/src/pages/api/ai/draft.ts +3 -2
- package/src/pages/api/ai/settings.ts +4 -3
- package/src/pages/api/ai/test.ts +3 -2
- package/src/pages/api/design/export.ts +2 -1
- package/src/pages/api/design/import.ts +2 -1
- package/src/pages/api/design/keys.ts +3 -2
- package/src/pages/api/email/settings.ts +4 -3
- package/src/pages/api/export.ts +3 -2
- package/src/pages/api/import/asset.ts +3 -2
- package/src/pages/api/import/index.ts +4 -3
- package/src/pages/api/import/media.ts +3 -2
- package/src/pages/api/live/oauth/youtube/callback.ts +4 -4
- package/src/pages/api/live/settings.ts +4 -3
- package/src/pages/api/media/upload.ts +4 -0
- package/src/pages/api/mobile/access.ts +4 -3
- package/src/pages/api/monitoring.ts +4 -3
- package/src/pages/api/push/admin.ts +4 -3
- package/src/pages/api/roles.ts +36 -0
- package/src/pages/api/v1/admin/bookings/[id]/[action].ts +2 -1
- package/src/pages/api/v1/admin/bookings/index.ts +3 -2
- package/src/pages/api/v1/admin/documents/[kind]/[id]/[action].ts +3 -2
- package/src/pages/api/v1/admin/documents/[kind]/[id]/index.ts +3 -2
- package/src/pages/api/v1/admin/documents/[kind]/[id]/translations/[locale]/[action].ts +3 -2
- package/src/pages/api/v1/admin/documents/[kind]/[id]/translations/[locale]/index.ts +3 -2
- package/src/pages/api/v1/admin/documents/index.ts +2 -1
- package/src/pages/api/v1/admin/links.ts +2 -1
- package/src/pages/api/v1/admin/media/index.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @acms-subir/core
|
|
2
2
|
|
|
3
|
+
## 0.3.13
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0cea2b7: Team → Roles: owners choose what each role can do, with Save, Discard, Review and Restore defaults. Defaults keep today's access; administrators can now invite people (to roles below their own). Permissions apply on everyone's next request in the web admin and the editor app, and every save is recorded. Adds migration 51.
|
|
8
|
+
|
|
3
9
|
## 0.3.12
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
|
@@ -229,6 +229,20 @@ var PLACES_MIGRATION = { version: 43, statements: [
|
|
|
229
229
|
`ALTER TABLE resources ADD COLUMN place_id TEXT NOT NULL DEFAULT ''`
|
|
230
230
|
] };
|
|
231
231
|
|
|
232
|
+
// src/lib/permissions/migration.ts
|
|
233
|
+
var ROLES_MIGRATION = {
|
|
234
|
+
version: 51,
|
|
235
|
+
statements: [
|
|
236
|
+
`CREATE TABLE role_policy (id INTEGER PRIMARY KEY CHECK (id = 1), overrides TEXT NOT NULL DEFAULT '{}', revision INTEGER NOT NULL DEFAULT 0,
|
|
237
|
+
updated_by TEXT NOT NULL DEFAULT '', updated_at INTEGER NOT NULL DEFAULT 0)`,
|
|
238
|
+
`INSERT INTO role_policy (id) VALUES (1)`,
|
|
239
|
+
`CREATE TABLE role_policy_log (id TEXT PRIMARY KEY, revision INTEGER NOT NULL, actor TEXT NOT NULL, changes TEXT NOT NULL, created_at INTEGER NOT NULL)`,
|
|
240
|
+
`CREATE INDEX role_policy_log_created ON role_policy_log(created_at)`,
|
|
241
|
+
`ALTER TABLE auth_invitations ADD COLUMN invited_by TEXT NOT NULL DEFAULT ''`,
|
|
242
|
+
`ALTER TABLE email_tokens ADD COLUMN invited_by TEXT NOT NULL DEFAULT ''`
|
|
243
|
+
]
|
|
244
|
+
};
|
|
245
|
+
|
|
232
246
|
// src/lib/repo/migrations.ts
|
|
233
247
|
var CONTENT_VERSION_COLUMNS = {
|
|
234
248
|
settings: [
|
|
@@ -968,7 +982,8 @@ var MIGRATIONS = [
|
|
|
968
982
|
`CREATE TABLE backup_state (id INTEGER PRIMARY KEY CHECK (id = 1), last_at INTEGER NOT NULL DEFAULT 0, lease_until INTEGER NOT NULL DEFAULT 0,
|
|
969
983
|
last_key TEXT NOT NULL DEFAULT '', last_error TEXT NOT NULL DEFAULT '', failed_at INTEGER NOT NULL DEFAULT 0)`,
|
|
970
984
|
`INSERT INTO backup_state (id) VALUES (1)`
|
|
971
|
-
] }
|
|
985
|
+
] },
|
|
986
|
+
ROLES_MIGRATION
|
|
972
987
|
];
|
|
973
988
|
var LATEST_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
|
|
974
989
|
|
package/integration.mjs
CHANGED
|
@@ -77,8 +77,10 @@ const ROUTES = [
|
|
|
77
77
|
['/admin/import', 'admin/import.astro'],
|
|
78
78
|
['/admin/tags', 'admin/tags.astro'],
|
|
79
79
|
['/admin/users', 'admin/users.astro'],
|
|
80
|
+
['/admin/roles', 'admin/roles.astro'],
|
|
80
81
|
// API
|
|
81
82
|
['/api/backup', 'api/backup.ts'],
|
|
83
|
+
['/api/roles', 'api/roles.ts'],
|
|
82
84
|
['/api/drafts', 'api/drafts.ts'],
|
|
83
85
|
['/api/ai/settings', 'api/ai/settings.ts'],
|
|
84
86
|
['/api/ai/test', 'api/ai/test.ts'],
|
package/package.json
CHANGED
package/src/actions/index.ts
CHANGED
|
@@ -8,6 +8,8 @@ import type { ActionAPIContext } from 'astro:actions';
|
|
|
8
8
|
import { getRepo } from '../lib/repo';
|
|
9
9
|
import { getMediaStore } from '../lib/media/store';
|
|
10
10
|
import { validateRequestSession } from '../lib/auth/request';
|
|
11
|
+
import { accessFor } from '../lib/permissions/access';
|
|
12
|
+
import { deniedMessage, type Permission } from '../lib/permissions/catalog';
|
|
11
13
|
import { DocTranslations, LinkKind, parseConfig, slugify, widgetTypes, type WidgetType } from '../lib/schema';
|
|
12
14
|
import { env } from '../lib/env';
|
|
13
15
|
import { CONTENT, collectionUrl } from '../lib/content';
|
|
@@ -54,6 +56,13 @@ async function requireAdmin(ctx: ActionAPIContext) {
|
|
|
54
56
|
throw new ActionError({ code: 'UNAUTHORIZED', message: 'Not signed in.' });
|
|
55
57
|
}
|
|
56
58
|
}
|
|
59
|
+
// …and that their role grants the permission (Team → Roles)
|
|
60
|
+
async function requireCan(ctx: ActionAPIContext, permission: Permission) {
|
|
61
|
+
const access = await accessFor(ctx.request, ctx.cookies, await getRepo(ctx.locals));
|
|
62
|
+
if (!access) throw new ActionError({ code: 'UNAUTHORIZED', message: 'Not signed in.' });
|
|
63
|
+
if (!access.can(permission)) throw new ActionError({ code: 'FORBIDDEN', message: deniedMessage(permission) });
|
|
64
|
+
return access;
|
|
65
|
+
}
|
|
57
66
|
|
|
58
67
|
function decodeEntities(s: string): string {
|
|
59
68
|
return s
|
|
@@ -101,6 +110,11 @@ function asActionError(error: unknown): never {
|
|
|
101
110
|
throw error;
|
|
102
111
|
}
|
|
103
112
|
const hooksFor = (ctx: ActionAPIContext): SaveHooks => ({ url: ctx.url, locals: ctx.locals, onRedirect: invalidateRedirectCache });
|
|
113
|
+
// content saves also carry whether this person may change what's live
|
|
114
|
+
async function publishHooks(ctx: ActionAPIContext): Promise<SaveHooks> {
|
|
115
|
+
const access = await accessFor(ctx.request, ctx.cookies, await getRepo(ctx.locals));
|
|
116
|
+
return { ...hooksFor(ctx), canPublish: access?.can('content.publish') ?? false };
|
|
117
|
+
}
|
|
104
118
|
|
|
105
119
|
async function importEvents(
|
|
106
120
|
repo: Awaited<ReturnType<typeof getRepo>>,
|
|
@@ -146,7 +160,7 @@ export const server = {
|
|
|
146
160
|
saveTemplate: defineAction({
|
|
147
161
|
input: z.object({ name: z.string().trim().min(1).max(120), definition: z.any(), purpose: z.enum(['event', 'general']) }),
|
|
148
162
|
handler: async (input, ctx) => {
|
|
149
|
-
await
|
|
163
|
+
await requireCan(ctx, 'forms.manage');
|
|
150
164
|
const parsed = FormDefinition.safeParse(input.definition);
|
|
151
165
|
if (!parsed.success) throw new ActionError({ code: 'BAD_REQUEST', message: parsed.error.issues[0]?.message || 'Invalid form.' });
|
|
152
166
|
const repo = await getRepo(ctx.locals);
|
|
@@ -160,7 +174,7 @@ export const server = {
|
|
|
160
174
|
save: defineAction({
|
|
161
175
|
input: z.object({ id: z.string(), revision: z.number().int(), name: z.string().trim().min(1).max(120), definition: z.any(), op: z.enum(['save', 'publish', 'archive']) }),
|
|
162
176
|
handler: async (input, ctx) => {
|
|
163
|
-
await
|
|
177
|
+
await requireCan(ctx, 'forms.manage');
|
|
164
178
|
const parsed = FormDefinition.safeParse(input.definition);
|
|
165
179
|
if (!parsed.success) throw new ActionError({ code: 'BAD_REQUEST', message: parsed.error.issues[0]?.message || 'Invalid form.' });
|
|
166
180
|
const repo = await getRepo(ctx.locals);
|
|
@@ -179,7 +193,7 @@ export const server = {
|
|
|
179
193
|
useTemplate: defineAction({
|
|
180
194
|
input: z.object({ id: z.string(), name: z.string().trim().min(1).max(120).optional() }),
|
|
181
195
|
handler: async (input, ctx) => {
|
|
182
|
-
await
|
|
196
|
+
await requireCan(ctx, 'forms.manage');
|
|
183
197
|
const repo = await getRepo(ctx.locals);
|
|
184
198
|
const source = await repo.forms.get(input.id);
|
|
185
199
|
if (!source || source.kind !== 'template' || source.purpose !== 'general') throw new ActionError({ code: 'NOT_FOUND', message: 'Template not found.' });
|
|
@@ -215,9 +229,9 @@ export const server = {
|
|
|
215
229
|
scheduledAt: nullableTs.optional(),
|
|
216
230
|
}),
|
|
217
231
|
handler: async ({ id, ...data }, ctx) => {
|
|
218
|
-
await
|
|
232
|
+
await requireCan(ctx, 'content.edit');
|
|
219
233
|
const repo = await getRepo(ctx.locals);
|
|
220
|
-
try { return { post: await savePost(repo, id, data,
|
|
234
|
+
try { return { post: await savePost(repo, id, data, await publishHooks(ctx)) }; } catch (e) { asActionError(e); }
|
|
221
235
|
},
|
|
222
236
|
}),
|
|
223
237
|
// Update button: copy the pending draft onto the live columns. The
|
|
@@ -226,15 +240,15 @@ export const server = {
|
|
|
226
240
|
applyUpdate: defineAction({
|
|
227
241
|
input: z.object({ id: z.string() }),
|
|
228
242
|
handler: async ({ id }, ctx) => {
|
|
229
|
-
await
|
|
243
|
+
await requireCan(ctx, 'content.publish');
|
|
230
244
|
const repo = await getRepo(ctx.locals);
|
|
231
|
-
try { return { post: await applyPostUpdate(repo, id,
|
|
245
|
+
try { return { post: await applyPostUpdate(repo, id, await publishHooks(ctx)) }; } catch (e) { asActionError(e); }
|
|
232
246
|
},
|
|
233
247
|
}),
|
|
234
248
|
discardChanges: defineAction({
|
|
235
249
|
input: z.object({ id: z.string() }),
|
|
236
250
|
handler: async ({ id }, ctx) => {
|
|
237
|
-
await
|
|
251
|
+
await requireCan(ctx, 'content.edit');
|
|
238
252
|
const repo = await getRepo(ctx.locals);
|
|
239
253
|
await repo.discardPostDraft(id);
|
|
240
254
|
const post = await repo.getPost(id);
|
|
@@ -245,7 +259,7 @@ export const server = {
|
|
|
245
259
|
delete: defineAction({
|
|
246
260
|
input: z.object({ id: z.string() }),
|
|
247
261
|
handler: async ({ id }, ctx) => {
|
|
248
|
-
await
|
|
262
|
+
await requireCan(ctx, 'content.delete');
|
|
249
263
|
const repo = await getRepo(ctx.locals);
|
|
250
264
|
await repo.deletePost(id);
|
|
251
265
|
return { ok: true };
|
|
@@ -271,7 +285,7 @@ export const server = {
|
|
|
271
285
|
blocks: z.array(BlockInput).optional(),
|
|
272
286
|
}),
|
|
273
287
|
handler: async ({ id, blocks, ...data }, ctx) => {
|
|
274
|
-
await
|
|
288
|
+
await requireCan(ctx, 'content.edit');
|
|
275
289
|
const repo = await getRepo(ctx.locals);
|
|
276
290
|
const cleanBlocks =
|
|
277
291
|
blocks === undefined
|
|
@@ -279,21 +293,21 @@ export const server = {
|
|
|
279
293
|
: blocks
|
|
280
294
|
.filter((b) => widgetTypes.includes(b.type as WidgetType))
|
|
281
295
|
.map((b) => ({ ...b, type: b.type as WidgetType, config: parseConfig(b.type as WidgetType, b.config) as Record<string, unknown> }));
|
|
282
|
-
try { return { page: await savePage(repo, id, data, cleanBlocks,
|
|
296
|
+
try { return { page: await savePage(repo, id, data, cleanBlocks, await publishHooks(ctx)) }; } catch (e) { asActionError(e); }
|
|
283
297
|
},
|
|
284
298
|
}),
|
|
285
299
|
applyUpdate: defineAction({
|
|
286
300
|
input: z.object({ id: z.string() }),
|
|
287
301
|
handler: async ({ id }, ctx) => {
|
|
288
|
-
await
|
|
302
|
+
await requireCan(ctx, 'content.publish');
|
|
289
303
|
const repo = await getRepo(ctx.locals);
|
|
290
|
-
try { return { page: await applyPageUpdate(repo, id,
|
|
304
|
+
try { return { page: await applyPageUpdate(repo, id, await publishHooks(ctx)) }; } catch (e) { asActionError(e); }
|
|
291
305
|
},
|
|
292
306
|
}),
|
|
293
307
|
discardChanges: defineAction({
|
|
294
308
|
input: z.object({ id: z.string() }),
|
|
295
309
|
handler: async ({ id }, ctx) => {
|
|
296
|
-
await
|
|
310
|
+
await requireCan(ctx, 'content.edit');
|
|
297
311
|
const repo = await getRepo(ctx.locals);
|
|
298
312
|
await repo.discardPageDraft(id);
|
|
299
313
|
const page = await repo.getPage(id);
|
|
@@ -304,7 +318,7 @@ export const server = {
|
|
|
304
318
|
delete: defineAction({
|
|
305
319
|
input: z.object({ id: z.string() }),
|
|
306
320
|
handler: async ({ id }, ctx) => {
|
|
307
|
-
await
|
|
321
|
+
await requireCan(ctx, 'content.delete');
|
|
308
322
|
const repo = await getRepo(ctx.locals);
|
|
309
323
|
await repo.deletePage(id);
|
|
310
324
|
return { ok: true };
|
|
@@ -324,7 +338,7 @@ export const server = {
|
|
|
324
338
|
updateAlt: defineAction({
|
|
325
339
|
input: z.object({ id: z.string(), alt: z.string() }),
|
|
326
340
|
handler: async ({ id, alt }, ctx) => {
|
|
327
|
-
await
|
|
341
|
+
await requireCan(ctx, 'media.upload');
|
|
328
342
|
const repo = await getRepo(ctx.locals);
|
|
329
343
|
await repo.updateMedia(id, { alt });
|
|
330
344
|
return { ok: true };
|
|
@@ -333,7 +347,7 @@ export const server = {
|
|
|
333
347
|
delete: defineAction({
|
|
334
348
|
input: z.object({ id: z.string() }),
|
|
335
349
|
handler: async ({ id }, ctx) => {
|
|
336
|
-
await
|
|
350
|
+
await requireCan(ctx, 'media.delete');
|
|
337
351
|
const repo = await getRepo(ctx.locals);
|
|
338
352
|
const media = await repo.getMedia(id);
|
|
339
353
|
if (media) {
|
|
@@ -358,7 +372,7 @@ export const server = {
|
|
|
358
372
|
meta: z.record(z.any()).default({}),
|
|
359
373
|
}),
|
|
360
374
|
handler: async (data, ctx) => {
|
|
361
|
-
await
|
|
375
|
+
await requireCan(ctx, 'site.links');
|
|
362
376
|
const repo = await getRepo(ctx.locals);
|
|
363
377
|
return { link: await repo.createLink(data) };
|
|
364
378
|
},
|
|
@@ -373,7 +387,7 @@ export const server = {
|
|
|
373
387
|
meta: z.record(z.any()).optional(),
|
|
374
388
|
}),
|
|
375
389
|
handler: async ({ id, ...data }, ctx) => {
|
|
376
|
-
await
|
|
390
|
+
await requireCan(ctx, 'site.links');
|
|
377
391
|
const repo = await getRepo(ctx.locals);
|
|
378
392
|
await repo.updateLink(id, data);
|
|
379
393
|
return { ok: true };
|
|
@@ -382,7 +396,7 @@ export const server = {
|
|
|
382
396
|
reorder: defineAction({
|
|
383
397
|
input: z.object({ ids: z.array(z.string()) }),
|
|
384
398
|
handler: async ({ ids }, ctx) => {
|
|
385
|
-
await
|
|
399
|
+
await requireCan(ctx, 'site.links');
|
|
386
400
|
const repo = await getRepo(ctx.locals);
|
|
387
401
|
await repo.reorderLinks(ids);
|
|
388
402
|
return { ok: true };
|
|
@@ -391,7 +405,7 @@ export const server = {
|
|
|
391
405
|
delete: defineAction({
|
|
392
406
|
input: z.object({ id: z.string() }),
|
|
393
407
|
handler: async ({ id }, ctx) => {
|
|
394
|
-
await
|
|
408
|
+
await requireCan(ctx, 'site.links');
|
|
395
409
|
const repo = await getRepo(ctx.locals);
|
|
396
410
|
await repo.deleteLink(id);
|
|
397
411
|
return { ok: true };
|
|
@@ -412,7 +426,7 @@ export const server = {
|
|
|
412
426
|
})),
|
|
413
427
|
}),
|
|
414
428
|
handler: async ({ items }, ctx) => {
|
|
415
|
-
await
|
|
429
|
+
await requireCan(ctx, 'site.navigation');
|
|
416
430
|
const repo = await getRepo(ctx.locals);
|
|
417
431
|
await repo.saveDesignDraft({ navItems: items });
|
|
418
432
|
const changes = await designChanges(repo);
|
|
@@ -427,7 +441,7 @@ export const server = {
|
|
|
427
441
|
area: z.enum(['header', 'footer', 'social']).default('header'),
|
|
428
442
|
}),
|
|
429
443
|
handler: async (data, ctx) => {
|
|
430
|
-
await
|
|
444
|
+
await requireCan(ctx, 'site.navigation');
|
|
431
445
|
const repo = await getRepo(ctx.locals);
|
|
432
446
|
return { item: await repo.createNavItem(data) };
|
|
433
447
|
},
|
|
@@ -440,7 +454,7 @@ export const server = {
|
|
|
440
454
|
area: z.enum(['header', 'footer', 'social']).optional(),
|
|
441
455
|
}),
|
|
442
456
|
handler: async ({ id, ...data }, ctx) => {
|
|
443
|
-
await
|
|
457
|
+
await requireCan(ctx, 'site.navigation');
|
|
444
458
|
const repo = await getRepo(ctx.locals);
|
|
445
459
|
await repo.updateNavItem(id, data);
|
|
446
460
|
return { ok: true };
|
|
@@ -449,7 +463,7 @@ export const server = {
|
|
|
449
463
|
reorder: defineAction({
|
|
450
464
|
input: z.object({ ids: z.array(z.string()) }),
|
|
451
465
|
handler: async ({ ids }, ctx) => {
|
|
452
|
-
await
|
|
466
|
+
await requireCan(ctx, 'site.navigation');
|
|
453
467
|
const repo = await getRepo(ctx.locals);
|
|
454
468
|
await repo.reorderNavItems(ids);
|
|
455
469
|
return { ok: true };
|
|
@@ -458,7 +472,7 @@ export const server = {
|
|
|
458
472
|
delete: defineAction({
|
|
459
473
|
input: z.object({ id: z.string() }),
|
|
460
474
|
handler: async ({ id }, ctx) => {
|
|
461
|
-
await
|
|
475
|
+
await requireCan(ctx, 'site.navigation');
|
|
462
476
|
const repo = await getRepo(ctx.locals);
|
|
463
477
|
await repo.deleteNavItem(id);
|
|
464
478
|
return { ok: true };
|
|
@@ -487,7 +501,11 @@ export const server = {
|
|
|
487
501
|
// unknown keys are an error, not silently dropped
|
|
488
502
|
}).strict(),
|
|
489
503
|
handler: async (data, ctx) => {
|
|
490
|
-
|
|
504
|
+
// each field belongs to the screen that edits it
|
|
505
|
+
const needs = new Set<Permission>(Object.keys(data).map((k): Permission =>
|
|
506
|
+
k.startsWith('links') ? 'site.links' : ['siteTitle', 'tagline', 'footerText', 'timezone', 'locale'].includes(k) ? 'site.settings' : 'site.design'));
|
|
507
|
+
if (!needs.size) await requireAdmin(ctx);
|
|
508
|
+
for (const permission of needs) await requireCan(ctx, permission);
|
|
491
509
|
const repo = await getRepo(ctx.locals);
|
|
492
510
|
await repo.updateSettings({ ...data, ...(data.locale ? { locale: Intl.getCanonicalLocales(data.locale)[0] } : {}) });
|
|
493
511
|
return { ok: true };
|
|
@@ -506,7 +524,7 @@ export const server = {
|
|
|
506
524
|
siteCover: z.string().optional(),
|
|
507
525
|
}),
|
|
508
526
|
handler: async (data, ctx) => {
|
|
509
|
-
await
|
|
527
|
+
await requireCan(ctx, 'site.design');
|
|
510
528
|
const repo = await getRepo(ctx.locals);
|
|
511
529
|
await repo.saveDesignDraft(data);
|
|
512
530
|
// edits that land back on the live design leave nothing to publish
|
|
@@ -526,7 +544,7 @@ export const server = {
|
|
|
526
544
|
revertDesignChange: defineAction({
|
|
527
545
|
input: z.object({ key: z.string() }),
|
|
528
546
|
handler: async ({ key }, ctx) => {
|
|
529
|
-
await
|
|
547
|
+
await requireCan(ctx, 'site.design');
|
|
530
548
|
const repo = await getRepo(ctx.locals);
|
|
531
549
|
const [settings, nav] = await Promise.all([repo.getSettings(), repo.listNavItems()]);
|
|
532
550
|
if (settings.draftDesign) await repo.setDesignDraft(revertDesignChange(settings, nav, settings.draftDesign, key));
|
|
@@ -537,8 +555,7 @@ export const server = {
|
|
|
537
555
|
applyDesign: defineAction({
|
|
538
556
|
input: z.object({}),
|
|
539
557
|
handler: async (_input, ctx) => {
|
|
540
|
-
const userId = await
|
|
541
|
-
if (!userId) throw new ActionError({ code: 'UNAUTHORIZED', message: 'Not signed in.' });
|
|
558
|
+
const { userId } = await requireCan(ctx, 'site.design');
|
|
542
559
|
const repo = await getRepo(ctx.locals);
|
|
543
560
|
const changes = await designChanges(repo);
|
|
544
561
|
const author = (await repo.listUsers()).find((u) => u.id === userId)?.name ?? 'Admin';
|
|
@@ -557,7 +574,7 @@ export const server = {
|
|
|
557
574
|
discardDesign: defineAction({
|
|
558
575
|
input: z.object({}),
|
|
559
576
|
handler: async (_input, ctx) => {
|
|
560
|
-
await
|
|
577
|
+
await requireCan(ctx, 'site.design');
|
|
561
578
|
const repo = await getRepo(ctx.locals);
|
|
562
579
|
await repo.discardDesignDraft();
|
|
563
580
|
return { ok: true };
|
|
@@ -582,7 +599,7 @@ export const server = {
|
|
|
582
599
|
restoreDesignVersion: defineAction({
|
|
583
600
|
input: z.object({ id: z.string() }),
|
|
584
601
|
handler: async ({ id }, ctx) => {
|
|
585
|
-
await
|
|
602
|
+
await requireCan(ctx, 'site.design');
|
|
586
603
|
const repo = await getRepo(ctx.locals);
|
|
587
604
|
const v = await repo.getDesignVersion(id);
|
|
588
605
|
if (!v) throw new ActionError({ code: 'NOT_FOUND', message: 'That version no longer exists.' });
|
|
@@ -597,7 +614,7 @@ export const server = {
|
|
|
597
614
|
createShareLink: defineAction({
|
|
598
615
|
input: z.object({ days: z.number().int().min(1).max(30).default(7) }),
|
|
599
616
|
handler: async ({ days }, ctx) => {
|
|
600
|
-
await
|
|
617
|
+
await requireCan(ctx, 'site.design');
|
|
601
618
|
const repo = await getRepo(ctx.locals);
|
|
602
619
|
const settings = await repo.getSettings();
|
|
603
620
|
if (!settings.draftDesign) throw new ActionError({ code: 'BAD_REQUEST', message: 'There are no draft changes to share.' });
|
|
@@ -612,7 +629,7 @@ export const server = {
|
|
|
612
629
|
revokeShareLinks: defineAction({
|
|
613
630
|
input: z.object({}),
|
|
614
631
|
handler: async (_input, ctx) => {
|
|
615
|
-
await
|
|
632
|
+
await requireCan(ctx, 'site.design');
|
|
616
633
|
const repo = await getRepo(ctx.locals);
|
|
617
634
|
const settings = await repo.getSettings();
|
|
618
635
|
if (settings.draftDesign) await repo.saveDesignDraft({ shareEpoch: crypto.randomUUID() });
|
|
@@ -699,7 +716,7 @@ export const server = {
|
|
|
699
716
|
create: defineAction({
|
|
700
717
|
input: z.object({ name: z.string().min(1).max(80), contentJson: z.string().min(2) }),
|
|
701
718
|
handler: async ({ name, contentJson }, ctx) => {
|
|
702
|
-
await
|
|
719
|
+
await requireCan(ctx, 'content.edit');
|
|
703
720
|
// must be a JSON array of ProseMirror nodes — reject anything else
|
|
704
721
|
// before it can break the editor on re-insert
|
|
705
722
|
let parsed: unknown;
|
|
@@ -714,7 +731,7 @@ export const server = {
|
|
|
714
731
|
delete: defineAction({
|
|
715
732
|
input: z.object({ id: z.string() }),
|
|
716
733
|
handler: async ({ id }, ctx) => {
|
|
717
|
-
await
|
|
734
|
+
await requireCan(ctx, 'content.edit');
|
|
718
735
|
const repo = await getRepo(ctx.locals);
|
|
719
736
|
await repo.deleteSnippet(id);
|
|
720
737
|
return { ok: true };
|
|
@@ -920,7 +937,7 @@ export const server = {
|
|
|
920
937
|
applyToLinks: z.boolean().optional(),
|
|
921
938
|
}),
|
|
922
939
|
handler: async ({ applyToLinks, ...patch }, ctx) => {
|
|
923
|
-
await
|
|
940
|
+
await requireCan(ctx, 'schedule.manage');
|
|
924
941
|
const repo = await getRepo(ctx.locals);
|
|
925
942
|
const settings = await repo.getSettings();
|
|
926
943
|
if (patch.defaultCalendarId) {
|
|
@@ -976,23 +993,23 @@ export const server = {
|
|
|
976
993
|
scheduledAt: nullableTs.optional(),
|
|
977
994
|
}),
|
|
978
995
|
handler: async ({ id, ...data }, ctx) => {
|
|
979
|
-
await
|
|
996
|
+
await requireCan(ctx, 'content.edit');
|
|
980
997
|
const repo = await getRepo(ctx.locals);
|
|
981
|
-
try { return { event: await saveEvent(repo, id, data,
|
|
998
|
+
try { return { event: await saveEvent(repo, id, data, await publishHooks(ctx)) }; } catch (e) { asActionError(e); }
|
|
982
999
|
},
|
|
983
1000
|
}),
|
|
984
1001
|
applyUpdate: defineAction({
|
|
985
1002
|
input: z.object({ id: z.string() }),
|
|
986
1003
|
handler: async ({ id }, ctx) => {
|
|
987
|
-
await
|
|
1004
|
+
await requireCan(ctx, 'content.publish');
|
|
988
1005
|
const repo = await getRepo(ctx.locals);
|
|
989
|
-
try { return { event: await applyEventUpdate(repo, id,
|
|
1006
|
+
try { return { event: await applyEventUpdate(repo, id, await publishHooks(ctx)) }; } catch (e) { asActionError(e); }
|
|
990
1007
|
},
|
|
991
1008
|
}),
|
|
992
1009
|
discardChanges: defineAction({
|
|
993
1010
|
input: z.object({ id: z.string() }),
|
|
994
1011
|
handler: async ({ id }, ctx) => {
|
|
995
|
-
await
|
|
1012
|
+
await requireCan(ctx, 'content.edit');
|
|
996
1013
|
const repo = await getRepo(ctx.locals);
|
|
997
1014
|
await repo.discardEventDraft(id);
|
|
998
1015
|
return { event: await repo.getEvent(id) };
|
|
@@ -1001,7 +1018,7 @@ export const server = {
|
|
|
1001
1018
|
delete: defineAction({
|
|
1002
1019
|
input: z.object({ id: z.string() }),
|
|
1003
1020
|
handler: async ({ id }, ctx) => {
|
|
1004
|
-
await
|
|
1021
|
+
await requireCan(ctx, 'content.delete');
|
|
1005
1022
|
const repo = await getRepo(ctx.locals);
|
|
1006
1023
|
const before = await repo.getEvent(id);
|
|
1007
1024
|
await repo.deleteEvent(id);
|
|
@@ -1070,7 +1087,7 @@ export const server = {
|
|
|
1070
1087
|
cancelBookingsForEvent: defineAction({
|
|
1071
1088
|
input: z.object({ ids: z.array(z.string()).min(1).max(100), message: z.string().max(2000).default('') }),
|
|
1072
1089
|
handler: async ({ ids, message }, ctx) => {
|
|
1073
|
-
await
|
|
1090
|
+
await requireCan(ctx, 'schedule.bookings');
|
|
1074
1091
|
const repo = await getRepo(ctx.locals);
|
|
1075
1092
|
const settings = await repo.getSettings();
|
|
1076
1093
|
const opts = { origin: siteOrigin(settings, ctx.url), locals: ctx.locals, waitUntil: waitUntilFrom(ctx.locals) };
|
|
@@ -1097,9 +1114,9 @@ export const server = {
|
|
|
1097
1114
|
roomIds: z.array(z.string()).max(50).default([]),
|
|
1098
1115
|
}),
|
|
1099
1116
|
handler: async (input, ctx) => {
|
|
1100
|
-
await
|
|
1117
|
+
await requireCan(ctx, 'content.edit');
|
|
1101
1118
|
const repo = await getRepo(ctx.locals);
|
|
1102
|
-
try { const e = await quickCreate(repo, input,
|
|
1119
|
+
try { const e = await quickCreate(repo, input, await publishHooks(ctx)); return { id: e.id, status: e.status }; } catch (e) { asActionError(e); }
|
|
1103
1120
|
},
|
|
1104
1121
|
}),
|
|
1105
1122
|
// Schedule: drag to another day. `occurrence` + mode 'one' moves a single
|
|
@@ -1107,10 +1124,10 @@ export const server = {
|
|
|
1107
1124
|
move: defineAction({
|
|
1108
1125
|
input: z.object({ id: z.string(), days: z.number().int(), mode: z.enum(['series', 'one']).default('series'), occurrence: z.string().max(20).default('') }),
|
|
1109
1126
|
handler: async ({ id, days, mode, occurrence }, ctx) => {
|
|
1110
|
-
await
|
|
1127
|
+
await requireCan(ctx, 'content.edit');
|
|
1111
1128
|
const repo = await getRepo(ctx.locals);
|
|
1112
1129
|
try {
|
|
1113
|
-
const r = mode === 'one' ? await moveOccurrence(repo, id, occurrence, days,
|
|
1130
|
+
const r = mode === 'one' ? await moveOccurrence(repo, id, occurrence, days, await publishHooks(ctx)) : await moveSeries(repo, id, days, await publishHooks(ctx));
|
|
1114
1131
|
return { id: r.event.id, undo: r.undo };
|
|
1115
1132
|
} catch (e) { asActionError(e); }
|
|
1116
1133
|
},
|
|
@@ -1121,15 +1138,15 @@ export const server = {
|
|
|
1121
1138
|
z.object({ kind: z.literal('split'), id: z.string(), seriesId: z.string(), occurrence: z.string().max(20) }),
|
|
1122
1139
|
]),
|
|
1123
1140
|
handler: async (undo, ctx) => {
|
|
1124
|
-
await
|
|
1141
|
+
await requireCan(ctx, 'content.edit');
|
|
1125
1142
|
const repo = await getRepo(ctx.locals);
|
|
1126
|
-
try { await undoMove(repo, undo,
|
|
1143
|
+
try { await undoMove(repo, undo, await publishHooks(ctx)); return { ok: true }; } catch (e) { asActionError(e); }
|
|
1127
1144
|
},
|
|
1128
1145
|
}),
|
|
1129
1146
|
duplicate: defineAction({
|
|
1130
1147
|
input: z.object({ id: z.string() }),
|
|
1131
1148
|
handler: async ({ id }, ctx) => {
|
|
1132
|
-
await
|
|
1149
|
+
await requireCan(ctx, 'content.edit');
|
|
1133
1150
|
const repo = await getRepo(ctx.locals);
|
|
1134
1151
|
try { return { id: (await duplicateEvent(repo, id)).id }; } catch (e) { asActionError(e); }
|
|
1135
1152
|
},
|
|
@@ -1163,7 +1180,7 @@ export const server = {
|
|
|
1163
1180
|
calendarId: z.string().optional(),
|
|
1164
1181
|
}),
|
|
1165
1182
|
handler: async ({ format, text, url, calendarId }, ctx) => {
|
|
1166
|
-
await
|
|
1183
|
+
await requireCan(ctx, 'content.edit');
|
|
1167
1184
|
const repo = await getRepo(ctx.locals);
|
|
1168
1185
|
const settings = await repo.getSettings();
|
|
1169
1186
|
if (calendarId && !(await repo.getCalendar(calendarId))) throw new ActionError({ code: 'BAD_REQUEST', message: 'Unknown calendar.' });
|
|
@@ -1211,7 +1228,7 @@ export const server = {
|
|
|
1211
1228
|
visibility: z.enum(['public', 'unlisted', 'private']).optional(),
|
|
1212
1229
|
}),
|
|
1213
1230
|
handler: async ({ id, ...data }, ctx) => {
|
|
1214
|
-
await
|
|
1231
|
+
await requireCan(ctx, 'schedule.manage');
|
|
1215
1232
|
const repo = await getRepo(ctx.locals);
|
|
1216
1233
|
try { return { calendar: await saveCalendar(repo, id, data) }; }
|
|
1217
1234
|
catch (e) {
|
|
@@ -1223,7 +1240,7 @@ export const server = {
|
|
|
1223
1240
|
reorder: defineAction({
|
|
1224
1241
|
input: z.object({ ids: z.array(z.string()) }),
|
|
1225
1242
|
handler: async ({ ids }, ctx) => {
|
|
1226
|
-
await
|
|
1243
|
+
await requireCan(ctx, 'schedule.manage');
|
|
1227
1244
|
await (await getRepo(ctx.locals)).reorderCalendars(ids);
|
|
1228
1245
|
return { ok: true };
|
|
1229
1246
|
},
|
|
@@ -1231,7 +1248,7 @@ export const server = {
|
|
|
1231
1248
|
rotateToken: defineAction({
|
|
1232
1249
|
input: z.object({ id: z.string() }),
|
|
1233
1250
|
handler: async ({ id }, ctx) => {
|
|
1234
|
-
await
|
|
1251
|
+
await requireCan(ctx, 'schedule.manage');
|
|
1235
1252
|
const repo = await getRepo(ctx.locals);
|
|
1236
1253
|
const bytes = crypto.getRandomValues(new Uint8Array(18));
|
|
1237
1254
|
const token = btoa(String.fromCharCode(...bytes)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
@@ -1242,7 +1259,7 @@ export const server = {
|
|
|
1242
1259
|
delete: defineAction({
|
|
1243
1260
|
input: z.object({ id: z.string() }),
|
|
1244
1261
|
handler: async ({ id }, ctx) => {
|
|
1245
|
-
await
|
|
1262
|
+
await requireCan(ctx, 'schedule.manage');
|
|
1246
1263
|
await (await getRepo(ctx.locals)).deleteCalendar(id);
|
|
1247
1264
|
return { ok: true };
|
|
1248
1265
|
},
|
|
@@ -1295,7 +1312,7 @@ export const server = {
|
|
|
1295
1312
|
isDefault: z.boolean().default(false),
|
|
1296
1313
|
}),
|
|
1297
1314
|
handler: async (input, ctx) => {
|
|
1298
|
-
await
|
|
1315
|
+
await requireCan(ctx, 'schedule.manage');
|
|
1299
1316
|
const repo = await getRepo(ctx.locals);
|
|
1300
1317
|
try { return await savePlace(repo, input, mapsCtx(ctx)); } catch (e) { asActionError(e); }
|
|
1301
1318
|
},
|
|
@@ -1303,7 +1320,7 @@ export const server = {
|
|
|
1303
1320
|
delete: defineAction({
|
|
1304
1321
|
input: z.object({ id: z.string().max(100) }),
|
|
1305
1322
|
handler: async ({ id }, ctx) => {
|
|
1306
|
-
await
|
|
1323
|
+
await requireCan(ctx, 'schedule.manage');
|
|
1307
1324
|
await (await getRepo(ctx.locals)).deletePlace(id);
|
|
1308
1325
|
return { ok: true };
|
|
1309
1326
|
},
|