@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/src/pages/api/export.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { APIRoute } from 'astro';
|
|
2
2
|
import { getRepo } from '../../lib/repo';
|
|
3
3
|
import { getMediaStore } from '../../lib/media/store';
|
|
4
|
-
import { apiError
|
|
4
|
+
import { apiError } from '../../lib/api';
|
|
5
5
|
import { exportBundle } from '../../lib/import/export';
|
|
6
6
|
import { zipStream, type ZipEntry } from '../../lib/backup/zip';
|
|
7
|
+
import { requirePermission } from '../../lib/permissions/access';
|
|
7
8
|
|
|
8
9
|
// Portable content export (acms-export v1): bundle.json + media/<key>.
|
|
9
10
|
// Import it into any acms site from Admin → Import & export; unlike a backup
|
|
@@ -11,7 +12,7 @@ import { zipStream, type ZipEntry } from '../../lib/backup/zip';
|
|
|
11
12
|
export const GET: APIRoute = async ({ request, cookies, locals }) => {
|
|
12
13
|
try {
|
|
13
14
|
const repo = await getRepo(locals);
|
|
14
|
-
await
|
|
15
|
+
await requirePermission(request, cookies, repo, 'data.import');
|
|
15
16
|
const now = Date.now();
|
|
16
17
|
const bundle = await exportBundle(repo, now);
|
|
17
18
|
const store = getMediaStore(locals);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { APIRoute } from 'astro';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { getRepo } from '../../../lib/repo';
|
|
4
|
-
import { ApiError, apiError, readJson, readResponseBytes
|
|
4
|
+
import { ApiError, apiError, readJson, readResponseBytes } from '../../../lib/api';
|
|
5
5
|
import { fetchPublicUrl } from '../../../lib/net';
|
|
6
|
+
import { requirePermission } from '../../../lib/permissions/access';
|
|
6
7
|
|
|
7
8
|
// Download one file from the site being imported. The old site won't send
|
|
8
9
|
// CORS headers, so the browser can't fetch it directly; it asks here, then
|
|
@@ -15,7 +16,7 @@ const Body = z.object({ url: z.string().url().max(2000) });
|
|
|
15
16
|
export const POST: APIRoute = async ({ request, cookies, locals }) => {
|
|
16
17
|
try {
|
|
17
18
|
const repo = await getRepo(locals);
|
|
18
|
-
await
|
|
19
|
+
await requirePermission(request, cookies, repo, 'data.import');
|
|
19
20
|
const { url } = Body.parse(await readJson(request, 10_000));
|
|
20
21
|
const got = await fetchPublicUrl(url, { accept: '*/*', userAgent: 'acms-importer/1.0', timeoutMs: 30_000 });
|
|
21
22
|
if (!got) throw new ApiError(502, 'The old site could not be reached for this file.');
|
|
@@ -2,9 +2,10 @@ import type { APIRoute } from 'astro';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { getRepo } from '../../../lib/repo';
|
|
4
4
|
import { getMediaStore } from '../../../lib/media/store';
|
|
5
|
-
import { ApiError, apiError, json, readJson
|
|
5
|
+
import { ApiError, apiError, json, readJson } from '../../../lib/api';
|
|
6
6
|
import { ImportBundle } from '../../../lib/import/bundle';
|
|
7
7
|
import { FinishInput, ImportOptions, ItemsInput, StartInput, finishImport, importItems, planImport, startImport, undoImport } from '../../../lib/import/apply';
|
|
8
|
+
import { requirePermission } from '../../../lib/permissions/access';
|
|
8
9
|
|
|
9
10
|
// Import pipeline steps (see lib/import/apply.ts). Owner-only: an import can
|
|
10
11
|
// create accounts, rewrite navigation and change site settings.
|
|
@@ -19,7 +20,7 @@ const Body = z.discriminatedUnion('op', [
|
|
|
19
20
|
export const GET: APIRoute = async ({ request, cookies, locals }) => {
|
|
20
21
|
try {
|
|
21
22
|
const repo = await getRepo(locals);
|
|
22
|
-
await
|
|
23
|
+
await requirePermission(request, cookies, repo, 'data.import');
|
|
23
24
|
return json({ runs: await repo.listImportRuns() });
|
|
24
25
|
} catch (e) { return apiError(e); }
|
|
25
26
|
};
|
|
@@ -27,7 +28,7 @@ export const GET: APIRoute = async ({ request, cookies, locals }) => {
|
|
|
27
28
|
export const POST: APIRoute = async ({ request, cookies, locals }) => {
|
|
28
29
|
try {
|
|
29
30
|
const repo = await getRepo(locals);
|
|
30
|
-
await
|
|
31
|
+
await requirePermission(request, cookies, repo, 'data.import');
|
|
31
32
|
// item batches carry full post bodies
|
|
32
33
|
const body = Body.parse(await readJson(request, 12 * 1024 * 1024));
|
|
33
34
|
switch (body.op) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { APIRoute } from 'astro';
|
|
2
2
|
import { getRepo } from '../../../lib/repo';
|
|
3
3
|
import { getMediaStore } from '../../../lib/media/store';
|
|
4
|
-
import { ApiError, apiError
|
|
4
|
+
import { ApiError, apiError } from '../../../lib/api';
|
|
5
5
|
import { Media } from '../../../lib/schema';
|
|
6
|
+
import { requirePermission } from '../../../lib/permissions/access';
|
|
6
7
|
|
|
7
8
|
// Verbatim media from an acms export: the row keeps its id and storage keys,
|
|
8
9
|
// so every /media URL in the imported content resolves unchanged. Fields:
|
|
@@ -10,7 +11,7 @@ import { Media } from '../../../lib/schema';
|
|
|
10
11
|
export const POST: APIRoute = async ({ request, cookies, locals }) => {
|
|
11
12
|
try {
|
|
12
13
|
const repo = await getRepo(locals);
|
|
13
|
-
await
|
|
14
|
+
await requirePermission(request, cookies, repo, 'data.import');
|
|
14
15
|
const form = await request.formData();
|
|
15
16
|
const media = Media.parse(JSON.parse(String(form.get('media') ?? 'null')));
|
|
16
17
|
const keys = [media.key, ...media.variants.map((v) => v.key)];
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import type { APIRoute } from 'astro';
|
|
2
2
|
import { getRepo } from '../../../../../lib/repo';
|
|
3
3
|
import { ApiError } from '../../../../../lib/api';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { requestSessionBinding } from '../../../../../lib/auth/request';
|
|
5
|
+
import { accessFor } from '../../../../../lib/permissions/access';
|
|
6
6
|
import { finishOAuth } from '../../../../../lib/live/service';
|
|
7
7
|
export const GET:APIRoute=async({request,cookies,locals,url})=>{
|
|
8
8
|
const headers={'Cache-Control':'no-store','Referrer-Policy':'no-referrer'};
|
|
9
9
|
try{
|
|
10
10
|
const repo=await getRepo(locals),session=await requestSessionBinding(request,cookies,locals);
|
|
11
11
|
// OAuth is intentionally a cross-site GET. Session-bound, one-use state provides CSRF protection.
|
|
12
|
-
const
|
|
13
|
-
if(!session||!
|
|
12
|
+
const access=await accessFor(request,cookies,repo);
|
|
13
|
+
if(!session||!access?.can('services.manage'))throw new ApiError(403,'Sign in as someone who manages connected services and start authorization again.');
|
|
14
14
|
await finishOAuth(repo,session,url,locals);
|
|
15
15
|
return new Response(null,{status:303,headers:{...headers,Location:'/admin/live?live=connected'}});
|
|
16
16
|
}catch{
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { APIRoute } from 'astro';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { getRepo } from '../../../lib/repo';
|
|
4
|
-
import { ApiError, apiError, json, readJson
|
|
4
|
+
import { ApiError, apiError, json, readJson } from '../../../lib/api';
|
|
5
5
|
import { requestSessionBinding } from '../../../lib/auth/request';
|
|
6
6
|
import { Provider } from '../../../lib/live/types';
|
|
7
7
|
import { adminState, beginOAuth, disconnect, saveConnection, syncConnection, validateConfig } from '../../../lib/live/service';
|
|
8
|
+
import { requirePermission } from '../../../lib/permissions/access';
|
|
8
9
|
const Input=z.discriminatedUnion('action',[
|
|
9
10
|
z.object({action:z.literal('saveConnection'),provider:Provider,clientId:z.string().trim().min(1).max(200),clientSecret:z.string().trim().max(8192).default(''),channelInput:z.string().trim().max(2048).default(''),googleStatus:z.enum(['testing','production']).default('testing')}).strict(),
|
|
10
11
|
z.object({action:z.literal('test'),provider:Provider}).strict(),
|
|
@@ -15,11 +16,11 @@ const Input=z.discriminatedUnion('action',[
|
|
|
15
16
|
z.object({action:z.literal('saveConfig'),config:z.unknown()}).strict(),
|
|
16
17
|
]);
|
|
17
18
|
export const GET:APIRoute=async({request,cookies,locals,url})=>{
|
|
18
|
-
try{const repo=await getRepo(locals);await
|
|
19
|
+
try{const repo=await getRepo(locals);await requirePermission(request, cookies, repo, 'services.manage');return json(await adminState(repo,url,locals));}catch(e){return apiError(e);}
|
|
19
20
|
};
|
|
20
21
|
export const POST:APIRoute=async({request,cookies,locals,url})=>{
|
|
21
22
|
try{
|
|
22
|
-
const repo=await getRepo(locals);await
|
|
23
|
+
const repo=await getRepo(locals);await requirePermission(request, cookies, repo, 'services.manage');
|
|
23
24
|
// Browser-based credential changes must originate at this site.
|
|
24
25
|
if(request.headers.get('origin')!==url.origin)throw new ApiError(403,'Submit changes from this site’s admin.');
|
|
25
26
|
const input=Input.parse(await readJson(request,24_000));
|
|
@@ -3,6 +3,8 @@ import { validateRequestSession } from '../../../lib/auth/request';
|
|
|
3
3
|
import { getRepo } from '../../../lib/repo';
|
|
4
4
|
import { ApiError } from '../../../lib/api';
|
|
5
5
|
import { storeUpload } from '../../../lib/media/upload';
|
|
6
|
+
import { accessFor } from '../../../lib/permissions/access';
|
|
7
|
+
import { deniedMessage } from '../../../lib/permissions/catalog';
|
|
6
8
|
|
|
7
9
|
// Multipart upload from the admin; the rules live in lib/media/upload.ts
|
|
8
10
|
// (shared with editor apps).
|
|
@@ -10,6 +12,8 @@ export const POST: APIRoute = async ({ request, cookies, locals }) => {
|
|
|
10
12
|
if (!(await validateRequestSession(request, cookies, locals))) {
|
|
11
13
|
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
12
14
|
}
|
|
15
|
+
const access = await accessFor(request, cookies, await getRepo(locals));
|
|
16
|
+
if (!access?.can('media.upload')) return Response.json({ error: deniedMessage('media.upload') }, { status: 403 });
|
|
13
17
|
const form = await request.formData();
|
|
14
18
|
try {
|
|
15
19
|
const media = await storeUpload(form, locals, await getRepo(locals));
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import type { APIRoute } from 'astro';
|
|
2
2
|
import { getRepo } from '../../../lib/repo';
|
|
3
|
-
import { ApiError, apiError, json, readJson
|
|
3
|
+
import { ApiError, apiError, json, readJson } from '../../../lib/api';
|
|
4
4
|
import { AppAccessInput, appAccessAction, appAccessState } from '../../../lib/mobile/access-admin';
|
|
5
|
+
import { requirePermission } from '../../../lib/permissions/access';
|
|
5
6
|
|
|
6
7
|
// Admin → Mobile app → App access (owner only, this site's admin only).
|
|
7
8
|
export const GET: APIRoute = async ({ request, cookies, locals }) => {
|
|
8
9
|
try {
|
|
9
10
|
const repo = await getRepo(locals);
|
|
10
|
-
await
|
|
11
|
+
await requirePermission(request, cookies, repo, 'services.manage');
|
|
11
12
|
return json(await appAccessState(repo, locals));
|
|
12
13
|
} catch (e) { return apiError(e); }
|
|
13
14
|
};
|
|
14
15
|
export const POST: APIRoute = async ({ request, cookies, locals, url }) => {
|
|
15
16
|
try {
|
|
16
17
|
const repo = await getRepo(locals);
|
|
17
|
-
await
|
|
18
|
+
await requirePermission(request, cookies, repo, 'services.manage');
|
|
18
19
|
if (request.headers.get('origin') !== url.origin) throw new ApiError(403, 'Change this from this site’s admin.');
|
|
19
20
|
await appAccessAction(repo, AppAccessInput.parse(await readJson(request, 5_000)));
|
|
20
21
|
return json(await appAccessState(repo, locals));
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import type { APIRoute } from 'astro';
|
|
2
2
|
import { getRepo } from '../../lib/repo';
|
|
3
|
-
import { ApiError, apiError, json, readJson
|
|
3
|
+
import { ApiError, apiError, json, readJson } from '../../lib/api';
|
|
4
4
|
import { MonitoringInput, monitoringAction, monitoringState } from '../../lib/monitoring';
|
|
5
|
+
import { requirePermission } from '../../lib/permissions/access';
|
|
5
6
|
|
|
6
7
|
// Settings → Monitoring (owner only, this site's admin only).
|
|
7
8
|
export const GET: APIRoute = async ({ request, cookies, locals }) => {
|
|
8
9
|
try {
|
|
9
10
|
const repo = await getRepo(locals);
|
|
10
|
-
await
|
|
11
|
+
await requirePermission(request, cookies, repo, 'services.manage');
|
|
11
12
|
return json(await monitoringState(repo, locals));
|
|
12
13
|
} catch (e) { return apiError(e); }
|
|
13
14
|
};
|
|
14
15
|
export const POST: APIRoute = async ({ request, cookies, locals, url }) => {
|
|
15
16
|
try {
|
|
16
17
|
const repo = await getRepo(locals);
|
|
17
|
-
await
|
|
18
|
+
await requirePermission(request, cookies, repo, 'services.manage');
|
|
18
19
|
if (request.headers.get('origin') !== url.origin) throw new ApiError(403, 'Change this from this site’s admin.');
|
|
19
20
|
await monitoringAction(repo, MonitoringInput.parse(await readJson(request, 5_000)), locals);
|
|
20
21
|
return json(await monitoringState(repo, locals));
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import type { APIRoute } from 'astro';
|
|
2
2
|
import { getRepo } from '../../../lib/repo';
|
|
3
|
-
import { ApiError, apiError, json, readJson
|
|
3
|
+
import { ApiError, apiError, json, readJson } from '../../../lib/api';
|
|
4
4
|
import { OWNER_ENV } from '../../../lib/auth';
|
|
5
5
|
import { validateRequestSession } from '../../../lib/auth/request';
|
|
6
6
|
import { AdminPushInput, adminPushAction, adminPushState } from '../../../lib/push/api';
|
|
7
|
+
import { requirePermission } from '../../../lib/permissions/access';
|
|
7
8
|
|
|
8
9
|
// Admin → Mobile app (owner only; registered only when push is enabled).
|
|
9
10
|
export const GET: APIRoute = async ({ request, cookies, locals, url }) => {
|
|
10
11
|
try {
|
|
11
12
|
const repo = await getRepo(locals);
|
|
12
|
-
await
|
|
13
|
+
await requirePermission(request, cookies, repo, 'services.manage');
|
|
13
14
|
return json(await adminPushState(repo, url, locals));
|
|
14
15
|
} catch (e) { return apiError(e); }
|
|
15
16
|
};
|
|
16
17
|
export const POST: APIRoute = async ({ request, cookies, locals, url }) => {
|
|
17
18
|
try {
|
|
18
19
|
const repo = await getRepo(locals);
|
|
19
|
-
await
|
|
20
|
+
await requirePermission(request, cookies, repo, 'services.manage');
|
|
20
21
|
// sending reaches every installed app: only from this site's admin
|
|
21
22
|
if (request.headers.get('origin') !== url.origin) throw new ApiError(403, 'Send from this site’s admin.');
|
|
22
23
|
const input = AdminPushInput.parse(await readJson(request, 20_000));
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { getRepo } from '../../lib/repo';
|
|
3
|
+
import { ApiError, apiError, json, readJson, requireSession } from '../../lib/api';
|
|
4
|
+
import { accessFor } from '../../lib/permissions/access';
|
|
5
|
+
import { AUTH } from '../../lib/auth/config';
|
|
6
|
+
import { getRequestAuth } from '../../lib/auth/request';
|
|
7
|
+
import { RolesSave, rolesState, saveRoles } from '../../lib/permissions/service';
|
|
8
|
+
|
|
9
|
+
// Team → Roles. Owners and administrators can read the policy (admins invite
|
|
10
|
+
// people and benefit from seeing what each role does); only owners save.
|
|
11
|
+
async function reader(request: Request, cookies: Parameters<APIRoute>[0]['cookies'], repo: Awaited<ReturnType<typeof getRepo>>) {
|
|
12
|
+
await requireSession(request, cookies, repo);
|
|
13
|
+
const access = await accessFor(request, cookies, repo);
|
|
14
|
+
if (!access) throw new ApiError(401, 'Sign in to continue.');
|
|
15
|
+
if (access.role !== 'owner' && access.role !== 'administrator') throw new ApiError(403, 'Only owners and administrators can see role permissions.');
|
|
16
|
+
return access;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const GET: APIRoute = async ({ request, cookies, locals }) => {
|
|
20
|
+
try {
|
|
21
|
+
const repo = await getRepo(locals);
|
|
22
|
+
return json(await rolesState(repo, await reader(request, cookies, repo)));
|
|
23
|
+
} catch (e) { return apiError(e); }
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const PUT: APIRoute = async ({ request, cookies, locals, url }) => {
|
|
27
|
+
try {
|
|
28
|
+
const repo = await getRepo(locals);
|
|
29
|
+
const access = await reader(request, cookies, repo);
|
|
30
|
+
if (request.headers.get('origin') !== url.origin) throw new ApiError(403, 'Change this from this site’s admin.');
|
|
31
|
+
// like Users: with Clerk, permission changes need a recent sign-in
|
|
32
|
+
if (AUTH.provider === 'clerk' && !(await getRequestAuth(request, cookies, repo)).reverified) throw new ApiError(403, 'Changing permissions needs a sign-in from the last ten minutes. Sign in again, then save.');
|
|
33
|
+
await saveRoles(repo, access, RolesSave.parse(await readJson(request, 50_000)));
|
|
34
|
+
return json(await rolesState(repo, access));
|
|
35
|
+
} catch (e) { return apiError(e); }
|
|
36
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { editorRoute, logWrite } from '../../../../../../lib/editor/http';
|
|
1
|
+
import { editorRoute, logWrite, requireCan } from '../../../../../../lib/editor/http';
|
|
2
2
|
import { BookingActionInput, bookingAction } from '../../../../../../lib/editor/bookings';
|
|
3
3
|
import { RescheduleInput, staffReschedule } from '../../../../../../lib/editor/schedule';
|
|
4
4
|
import { methodNotAllowed } from '../../../../../../lib/mobile/http';
|
|
@@ -7,6 +7,7 @@ export { preflight as OPTIONS } from '../../../../../../lib/mobile/http';
|
|
|
7
7
|
// approve | decline | cancel | reschedule — the guest is emailed, as from Admin → Bookings
|
|
8
8
|
export const POST = editorRoute(async (c) => {
|
|
9
9
|
const { id, action } = c.context.params;
|
|
10
|
+
await requireCan(c, 'schedule.bookings');
|
|
10
11
|
if (action === 'reschedule') {
|
|
11
12
|
const result = await staffReschedule(c, id!, RescheduleInput.parse(await readJson(c.context.request, 5_000)));
|
|
12
13
|
await logWrite(c, 'booking-reschedule', `booking:${id}`);
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { editorRoute, logWrite } from '../../../../../lib/editor/http';
|
|
1
|
+
import { editorRoute, logWrite, requireCan } from '../../../../../lib/editor/http';
|
|
2
2
|
import { InboxQuery, inbox } from '../../../../../lib/editor/bookings';
|
|
3
3
|
import { StaffBookingInput, staffBook } from '../../../../../lib/editor/schedule';
|
|
4
4
|
import { methodNotAllowed, queryParams } from '../../../../../lib/mobile/http';
|
|
5
5
|
import { readJson } from '../../../../../lib/api';
|
|
6
6
|
export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
|
|
7
|
-
export const GET = editorRoute(async ({
|
|
7
|
+
export const GET = editorRoute(async (c) => { await requireCan(c, 'schedule.bookings'); return inbox(c.repo, InboxQuery.parse(queryParams(c.context.url))); });
|
|
8
8
|
// book on someone's behalf (the guest gets the usual emails)
|
|
9
9
|
export const POST = editorRoute(async (c) => {
|
|
10
|
+
await requireCan(c, 'schedule.bookings');
|
|
10
11
|
const b = await staffBook(c, StaffBookingInput.parse(await readJson(c.context.request, 50_000)));
|
|
11
12
|
await logWrite(c, 'booking-create', `booking:${b.id}`);
|
|
12
13
|
return b;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { editorRoute, logWrite } from '../../../../../../../lib/editor/http';
|
|
1
|
+
import { canPublish, editorRoute, logWrite, requireCan } from '../../../../../../../lib/editor/http';
|
|
2
2
|
import { ActionInput, DocKindParam, docAction, docJson } from '../../../../../../../lib/editor/documents';
|
|
3
3
|
import { methodNotAllowed } from '../../../../../../../lib/mobile/http';
|
|
4
4
|
import { ApiError, readJson } from '../../../../../../../lib/api';
|
|
@@ -9,8 +9,9 @@ export const POST = editorRoute(async (c) => {
|
|
|
9
9
|
const kind = DocKindParam.parse(c.context.params.kind);
|
|
10
10
|
const action = c.context.params.action!;
|
|
11
11
|
if (!['publish', 'unpublish', 'schedule', 'discard'].includes(action)) throw new ApiError(404, 'Unknown action.');
|
|
12
|
+
await requireCan(c, action === 'discard' ? 'content.edit' : 'content.publish');
|
|
12
13
|
const body = ActionInput.parse(await readJson(c.context.request, 5_000));
|
|
13
|
-
const doc = await docAction(c.repo, kind, c.context.params.id!, action, body, { url: c.context.url, locals: c.context.locals, onRedirect: invalidateRedirectCache });
|
|
14
|
+
const doc = await docAction(c.repo, kind, c.context.params.id!, action, body, { url: c.context.url, locals: c.context.locals, onRedirect: invalidateRedirectCache, canPublish: await canPublish(c) });
|
|
14
15
|
await logWrite(c, action, `${kind}:${doc.id}`);
|
|
15
16
|
return docJson(kind, doc);
|
|
16
17
|
}, { write: true });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { editorRoute, logWrite } from '../../../../../../../lib/editor/http';
|
|
1
|
+
import { canPublish, editorRoute, logWrite, requireCan } from '../../../../../../../lib/editor/http';
|
|
2
2
|
import { DocKindParam, SaveInput, docJson, loadDoc, saveDoc } from '../../../../../../../lib/editor/documents';
|
|
3
3
|
import { methodNotAllowed, queryParams } from '../../../../../../../lib/mobile/http';
|
|
4
4
|
import { readJson } from '../../../../../../../lib/api';
|
|
@@ -11,9 +11,10 @@ export const GET = editorRoute(async ({ repo, context }) => {
|
|
|
11
11
|
return docJson(kind, await loadDoc(repo, kind, context.params.id!));
|
|
12
12
|
});
|
|
13
13
|
export const PUT = editorRoute(async (c) => {
|
|
14
|
+
await requireCan(c, 'content.edit');
|
|
14
15
|
const kind = DocKindParam.parse(c.context.params.kind);
|
|
15
16
|
const body = SaveInput.parse(await readJson(c.context.request, 2_500_000));
|
|
16
|
-
const doc = await saveDoc(c.repo, kind, c.context.params.id!, body, { url: c.context.url, locals: c.context.locals, onRedirect: invalidateRedirectCache });
|
|
17
|
+
const doc = await saveDoc(c.repo, kind, c.context.params.id!, body, { url: c.context.url, locals: c.context.locals, onRedirect: invalidateRedirectCache, canPublish: await canPublish(c) });
|
|
17
18
|
await logWrite(c, 'save', `${kind}:${doc.id}`);
|
|
18
19
|
return docJson(kind, doc);
|
|
19
20
|
}, { write: true });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { editorRoute, logWrite } from '../../../../../../../../../lib/editor/http';
|
|
1
|
+
import { canPublish, editorRoute, logWrite, requireCan } from '../../../../../../../../../lib/editor/http';
|
|
2
2
|
import { DocKindParam } from '../../../../../../../../../lib/editor/documents';
|
|
3
3
|
import { AiDocInput, ReviewDocInput, changeDocTranslation } from '../../../../../../../../../lib/editor/translations';
|
|
4
4
|
import { failure, methodNotAllowed } from '../../../../../../../../../lib/mobile/http';
|
|
@@ -9,7 +9,8 @@ export { preflight as OPTIONS } from '../../../../../../../../../lib/mobile/http
|
|
|
9
9
|
const handler = (ai: boolean) => editorRoute(async (c) => {
|
|
10
10
|
const { params } = c.context;
|
|
11
11
|
const kind = DocKindParam.parse(params.kind);
|
|
12
|
-
|
|
12
|
+
await requireCan(c, 'content.edit');
|
|
13
|
+
const hooks = { url: c.context.url, locals: c.context.locals, onRedirect: invalidateRedirectCache, canPublish: await canPublish(c) };
|
|
13
14
|
const raw = await readJson(c.context.request, 10_000);
|
|
14
15
|
const result = ai
|
|
15
16
|
? await (async () => { const b = AiDocInput.parse(raw); return changeDocTranslation(c.repo, kind, params.id!, params.locale!, b.revision, { op: 'ai', paths: b.paths, includeReviewed: b.includeReviewed }, hooks); })()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { editorRoute, logWrite } from '../../../../../../../../../lib/editor/http';
|
|
1
|
+
import { canPublish, editorRoute, logWrite, requireCan } from '../../../../../../../../../lib/editor/http';
|
|
2
2
|
import { DocKindParam } from '../../../../../../../../../lib/editor/documents';
|
|
3
3
|
import { SetDocInput, changeDocTranslation, getDocTranslation } from '../../../../../../../../../lib/editor/translations';
|
|
4
4
|
import { methodNotAllowed } from '../../../../../../../../../lib/mobile/http';
|
|
@@ -8,10 +8,11 @@ export { preflight as OPTIONS } from '../../../../../../../../../lib/mobile/http
|
|
|
8
8
|
export const GET = editorRoute(async ({ repo, context: { params } }) => getDocTranslation(repo, DocKindParam.parse(params.kind), params.id!, params.locale!));
|
|
9
9
|
// a person's words: stored as reviewed
|
|
10
10
|
export const PUT = editorRoute(async (c) => {
|
|
11
|
+
await requireCan(c, 'content.edit');
|
|
11
12
|
const { params } = c.context;
|
|
12
13
|
const kind = DocKindParam.parse(params.kind);
|
|
13
14
|
const body = SetDocInput.parse(await readJson(c.context.request, 2_500_000));
|
|
14
|
-
const result = await changeDocTranslation(c.repo, kind, params.id!, params.locale!, body.revision, { op: 'set', values: body.values }, { url: c.context.url, locals: c.context.locals, onRedirect: invalidateRedirectCache });
|
|
15
|
+
const result = await changeDocTranslation(c.repo, kind, params.id!, params.locale!, body.revision, { op: 'set', values: body.values }, { url: c.context.url, locals: c.context.locals, onRedirect: invalidateRedirectCache, canPublish: await canPublish(c) });
|
|
15
16
|
await logWrite(c, 'translate', `${kind}:${params.id}:${params.locale}`);
|
|
16
17
|
return result;
|
|
17
18
|
}, { write: true });
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { editorRoute, logWrite } from '../../../../../lib/editor/http';
|
|
1
|
+
import { canPublish, editorRoute, logWrite, requireCan } from '../../../../../lib/editor/http';
|
|
2
2
|
import { CreateInput, createDoc, docJson, listDocs, listQuery } from '../../../../../lib/editor/documents';
|
|
3
3
|
import { methodNotAllowed, queryParams } from '../../../../../lib/mobile/http';
|
|
4
4
|
import { readJson } from '../../../../../lib/api';
|
|
5
5
|
export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
|
|
6
6
|
export const GET = editorRoute(async ({ repo, context }) => listDocs(repo, listQuery.parse(queryParams(context.url))));
|
|
7
7
|
export const POST = editorRoute(async (c) => {
|
|
8
|
+
await requireCan(c, 'content.edit');
|
|
8
9
|
const input = CreateInput.parse(await readJson(c.context.request, 20_000));
|
|
9
10
|
const doc = await createDoc(c.repo, input, c.who.user.id);
|
|
10
11
|
await logWrite(c, 'create', `${input.kind}:${doc.id}`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { editorRoute, logWrite } from '../../../../lib/editor/http';
|
|
1
|
+
import { editorRoute, logWrite, requireCan } from '../../../../lib/editor/http';
|
|
2
2
|
import { LinksPut, getLinksFor, putLinksFor } from '../../../../lib/editor/site-data';
|
|
3
3
|
import { methodNotAllowed } from '../../../../lib/mobile/http';
|
|
4
4
|
import { readJson } from '../../../../lib/api';
|
|
@@ -6,6 +6,7 @@ export { preflight as OPTIONS } from '../../../../lib/mobile/http';
|
|
|
6
6
|
export const GET = editorRoute(async (c) => getLinksFor(c));
|
|
7
7
|
// replaces the whole page (profile + blocks in order)
|
|
8
8
|
export const PUT = editorRoute(async (c) => {
|
|
9
|
+
await requireCan(c, 'site.links');
|
|
9
10
|
const result = await putLinksFor(c, LinksPut.parse(await readJson(c.context.request, 300_000)));
|
|
10
11
|
await logWrite(c, 'links-update', 'links');
|
|
11
12
|
return result;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { editorRoute, logWrite } from '../../../../../lib/editor/http';
|
|
2
|
+
import { editorRoute, logWrite, requireCan } from '../../../../../lib/editor/http';
|
|
3
3
|
import { methodNotAllowed, pageQuery, queryParams } from '../../../../../lib/mobile/http';
|
|
4
4
|
import { ApiError, readResponseBytes } from '../../../../../lib/api';
|
|
5
5
|
import { MAX_UPLOAD_BODY, storeUpload } from '../../../../../lib/media/upload';
|
|
@@ -19,6 +19,7 @@ export const GET = editorRoute(async ({ repo, context }) => {
|
|
|
19
19
|
});
|
|
20
20
|
// same types, limits and storage as the admin uploader (images arrive pre-compressed)
|
|
21
21
|
export const POST = editorRoute(async (c) => {
|
|
22
|
+
await requireCan(c, 'media.upload');
|
|
22
23
|
const type = c.context.request.headers.get('content-type') ?? '';
|
|
23
24
|
if (!type.toLowerCase().startsWith('multipart/form-data')) throw new ApiError(415, 'Use Content-Type: multipart/form-data with a "file" field.');
|
|
24
25
|
const bytes = await readResponseBytes(c.context.request, MAX_UPLOAD_BODY);
|