@acms-subir/core 0.3.3-next.20260929113209 → 0.3.4
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 +105 -0
- package/cli/common.mjs +2 -2
- package/cli/legacy.mjs +1 -1
- package/cli/main.mjs +82 -8
- package/cli/mobile-doctor.mjs +430 -0
- package/cli/mobile.mjs +315 -0
- package/cli/native-sections.mjs +168 -0
- package/cli/preview.mjs +8 -4
- package/cli/sections.mjs +188 -0
- package/cli/themes.mjs +52 -8
- package/docs/mobile.md +527 -0
- package/docs/sections.md +203 -0
- package/docs/theme-authoring.md +19 -3
- package/integration.mjs +76 -5
- package/mobile-template/client/AGENTS.md +97 -0
- package/mobile-template/client/README.md +65 -0
- package/mobile-template/client/app/_layout.tsx +56 -0
- package/mobile-template/client/app/index.tsx +21 -0
- package/mobile-template/client/app.json +21 -0
- package/mobile-template/client/components/LanguageSwitch.tsx +31 -0
- package/mobile-template/client/eas.json +21 -0
- package/mobile-template/client/env.example +3 -0
- package/mobile-template/client/gitignore +42 -0
- package/mobile-template/client/package.json +38 -0
- package/mobile-template/client/sections/index.ts +11 -0
- package/mobile-template/client/tsconfig.json +7 -0
- package/mobile-template/editor/AGENTS.md +66 -0
- package/mobile-template/editor/README.md +73 -0
- package/mobile-template/editor/app/_layout.tsx +76 -0
- package/mobile-template/editor/app/auth.tsx +13 -0
- package/mobile-template/editor/app/index.tsx +55 -0
- package/mobile-template/editor/app.json +21 -0
- package/mobile-template/editor/components/Screen.tsx +51 -0
- package/mobile-template/editor/components/SignIn.tsx +36 -0
- package/mobile-template/editor/components/TextTabs.tsx +27 -0
- package/mobile-template/editor/eas.json +21 -0
- package/mobile-template/editor/env.example +5 -0
- package/mobile-template/editor/gitignore +42 -0
- package/mobile-template/editor/lib/appearance.tsx +12 -0
- package/mobile-template/editor/lib/config.ts +17 -0
- package/mobile-template/editor/lib/format.ts +24 -0
- package/mobile-template/editor/lib/tools.ts +16 -0
- package/mobile-template/editor/package.json +40 -0
- package/mobile-template/editor/tools/README.md +2 -0
- package/mobile-template/editor/tsconfig.json +7 -0
- package/mobile-template/recipes/booking/files/app/book/[resource].tsx +57 -0
- package/mobile-template/recipes/booking/files/app/book/details.tsx +105 -0
- package/mobile-template/recipes/booking/files/app/book/index.tsx +38 -0
- package/mobile-template/recipes/booking/recipe.json +17 -0
- package/mobile-template/recipes/editor-bookings/files/app/bookings/index.tsx +113 -0
- package/mobile-template/recipes/editor-bookings/files/tools/bookings.ts +1 -0
- package/mobile-template/recipes/editor-bookings/recipe.json +8 -0
- package/mobile-template/recipes/editor-content/files/app/content/[kind]/[id].tsx +13 -0
- package/mobile-template/recipes/editor-content/files/app/content/index.tsx +76 -0
- package/mobile-template/recipes/editor-content/files/components/DocumentDetail.tsx +55 -0
- package/mobile-template/recipes/editor-content/files/tools/content.ts +1 -0
- package/mobile-template/recipes/editor-content/recipe.json +8 -0
- package/mobile-template/recipes/editor-media/files/app/media/index.tsx +73 -0
- package/mobile-template/recipes/editor-media/files/tools/media.ts +1 -0
- package/mobile-template/recipes/editor-media/recipe.json +8 -0
- package/mobile-template/recipes/editor-sections/files/app/sections/[pageId]/[sectionId].tsx +12 -0
- package/mobile-template/recipes/editor-sections/files/app/sections/index.tsx +48 -0
- package/mobile-template/recipes/editor-sections/files/components/SectionEditorPane.tsx +77 -0
- package/mobile-template/recipes/editor-sections/files/tools/sections.ts +1 -0
- package/mobile-template/recipes/editor-sections/recipe.json +8 -0
- package/mobile-template/recipes/editor-translations/files/app/translations/index.tsx +63 -0
- package/mobile-template/recipes/editor-translations/files/app/translations/review.tsx +14 -0
- package/mobile-template/recipes/editor-translations/files/components/ReviewPane.tsx +72 -0
- package/mobile-template/recipes/editor-translations/files/tools/translations.ts +1 -0
- package/mobile-template/recipes/editor-translations/recipe.json +8 -0
- package/mobile-template/recipes/editor-writer/files/app/write/[kind]/[id].tsx +132 -0
- package/mobile-template/recipes/editor-writer/files/app/write/new.tsx +61 -0
- package/mobile-template/recipes/editor-writer/files/tools/writer.ts +1 -0
- package/mobile-template/recipes/editor-writer/recipe.json +8 -0
- package/mobile-template/recipes/events/files/app/events/[slug].tsx +41 -0
- package/mobile-template/recipes/events/files/app/events/index.tsx +50 -0
- package/mobile-template/recipes/events/files/lib/addToCalendar.ts +27 -0
- package/mobile-template/recipes/events/recipe.json +15 -0
- package/mobile-template/recipes/news/files/app/news/[slug].tsx +25 -0
- package/mobile-template/recipes/news/files/app/news/index.tsx +50 -0
- package/mobile-template/recipes/news/recipe.json +13 -0
- package/mobile-template/recipes/push/files/app/notifications.tsx +64 -0
- package/mobile-template/recipes/push/files/components/PushSetup.tsx +15 -0
- package/mobile-template/recipes/push/files/lib/push.ts +15 -0
- package/mobile-template/recipes/push/recipe.json +20 -0
- package/package.json +17 -13
- package/section-library/README.md +16 -0
- package/section-library/contact/Contact.astro +49 -0
- package/section-library/contact/section.json +106 -0
- package/section-library/cta-band/CtaBand.astro +28 -0
- package/section-library/cta-band/section.json +51 -0
- package/section-library/feature-grid/FeatureGrid.astro +54 -0
- package/section-library/feature-grid/section.json +107 -0
- package/section-library/gallery/Gallery.astro +29 -0
- package/section-library/gallery/section.json +51 -0
- package/section-library/hero-banner/HeroBanner.astro +42 -0
- package/section-library/hero-banner/section.json +81 -0
- package/section-library/latest-posts/LatestPosts.astro +22 -0
- package/section-library/latest-posts/section.json +37 -0
- package/section-library/pricing/Pricing.astro +40 -0
- package/section-library/pricing/section.json +117 -0
- package/section-library/questions/Questions.astro +38 -0
- package/section-library/questions/section.json +69 -0
- package/section-library/service-times/ServiceTimes.astro +42 -0
- package/section-library/service-times/section.json +127 -0
- package/section-library/team/Team.astro +45 -0
- package/section-library/team/section.json +88 -0
- package/section-library/upcoming-events/UpcomingEvents.astro +54 -0
- package/section-library/upcoming-events/section.json +51 -0
- package/src/actions/index.ts +205 -174
- package/src/components/AiSettings.tsx +45 -5
- package/src/components/DesignManager.tsx +112 -16
- package/src/components/MobileApp.tsx +392 -0
- package/src/components/ThemeSections.astro +40 -21
- package/src/components/ThemeSectionsEditor.tsx +757 -229
- package/src/components/booking/BookingFlow.astro +6 -3
- package/src/components/calendar/BookingsHead.astro +51 -0
- package/src/components/calendar/CalendarBoard.tsx +618 -0
- package/src/components/calendar/MiniMonth.tsx +54 -0
- package/src/components/calendar/NewEventDialog.tsx +44 -0
- package/src/components/calendar/QuickAdd.tsx +263 -0
- package/src/components/calendar/RoomHolds.tsx +203 -0
- package/src/components/calendar/WhereItLands.tsx +51 -0
- package/src/components/editor/EventPanel.tsx +23 -2
- package/src/components/editor/PostEditor.tsx +206 -14
- package/src/components/import/ImportExport.tsx +4 -1
- package/src/components/import/runner.ts +1 -0
- package/src/components/sections/SectionFields.tsx +560 -0
- package/src/components/sections/SectionPicker.tsx +120 -0
- package/src/components/sections/TranslationPanel.tsx +143 -0
- package/src/components/sections/checks.ts +64 -0
- package/src/layouts/Admin.astro +11 -7
- package/src/layouts/Site.astro +136 -21
- package/src/lib/agent-discovery.ts +11 -7
- package/src/lib/agent.ts +35 -2
- package/src/lib/ai-schema.ts +5 -0
- package/src/lib/ai.ts +5 -0
- package/src/lib/api.ts +12 -0
- package/src/lib/backup/content.ts +6 -1
- package/src/lib/booking/agent.ts +3 -2
- package/src/lib/booking/approval.ts +70 -0
- package/src/lib/booking/holds.ts +120 -0
- package/src/lib/booking/service.ts +37 -3
- package/src/lib/calendar/agent-schema.ts +9 -1
- package/src/lib/calendar/agent.ts +16 -1
- package/src/lib/calendar/board.ts +171 -0
- package/src/lib/calendar/config.ts +3 -0
- package/src/lib/calendar/grid.ts +169 -0
- package/src/lib/calendar/migration.ts +16 -0
- package/src/lib/calendar/render.ts +9 -7
- package/src/lib/calendar/schedule.ts +166 -0
- package/src/lib/content-service.ts +203 -0
- package/src/lib/design-service.ts +153 -5
- package/src/lib/drafts.ts +3 -3
- package/src/lib/editor/auth.ts +264 -0
- package/src/lib/editor/bookings.ts +60 -0
- package/src/lib/editor/config.ts +25 -0
- package/src/lib/editor/documents.ts +227 -0
- package/src/lib/editor/http.ts +48 -0
- package/src/lib/editor/me.ts +30 -0
- package/src/lib/editor/migration.ts +57 -0
- package/src/lib/editor/next.ts +11 -0
- package/src/lib/editor/openapi.ts +124 -0
- package/src/lib/editor/schemas.ts +91 -0
- package/src/lib/editor/sections.ts +96 -0
- package/src/lib/editor/store.ts +184 -0
- package/src/lib/editor/token-route.ts +74 -0
- package/src/lib/editor/translations.ts +163 -0
- package/src/lib/email/accounts.ts +2 -1
- package/src/lib/email/types.ts +2 -1
- package/src/lib/home-source.ts +34 -0
- package/src/lib/i18n.ts +34 -0
- package/src/lib/import/apply.ts +31 -2
- package/src/lib/import/bundle.ts +4 -0
- package/src/lib/import/export.ts +1 -0
- package/src/lib/import/ghost.ts +1 -0
- package/src/lib/media/upload.ts +83 -0
- package/src/lib/mobile/associations.ts +25 -0
- package/src/lib/mobile/calendar.ts +26 -14
- package/src/lib/mobile/config.mjs +59 -0
- package/src/lib/mobile/content.ts +18 -13
- package/src/lib/mobile/embed.ts +83 -0
- package/src/lib/mobile/http.ts +14 -6
- package/src/lib/mobile/locale.ts +18 -0
- package/src/lib/mobile/openapi.ts +111 -0
- package/src/lib/mobile/pages.ts +71 -0
- package/src/lib/mobile/route.ts +3 -3
- package/src/lib/mobile/schemas.ts +60 -0
- package/src/lib/mobile/site.ts +41 -0
- package/src/lib/push/api.ts +158 -0
- package/src/lib/push/config.ts +20 -0
- package/src/lib/push/expo.ts +76 -0
- package/src/lib/push/migration.ts +60 -0
- package/src/lib/push/service.ts +302 -0
- package/src/lib/push/store.ts +144 -0
- package/src/lib/push/types.ts +43 -0
- package/src/lib/repo/migrations.ts +14 -0
- package/src/lib/repo/sql.ts +65 -20
- package/src/lib/repo/types.ts +10 -4
- package/src/lib/roles.ts +5 -0
- package/src/lib/scheduler.ts +6 -1
- package/src/lib/schema.ts +76 -1
- package/src/lib/section-ai.ts +55 -0
- package/src/lib/section-targets.ts +62 -0
- package/src/lib/sections-agent.ts +146 -0
- package/src/lib/sections.ts +242 -0
- package/src/lib/site-chrome.ts +34 -0
- package/src/lib/theme-assets.ts +16 -0
- package/src/lib/theme.ts +33 -4
- package/src/lib/translate.ts +97 -0
- package/src/lib/translations.ts +39 -0
- package/src/middleware.ts +38 -2
- package/src/pages/.well-known/apple-app-site-association.ts +6 -0
- package/src/pages/.well-known/assetlinks.json.ts +6 -0
- package/src/pages/[...slug].astro +8 -3
- package/src/pages/admin/app-auth.astro +159 -0
- package/src/pages/admin/c/[collection]/[id].astro +9 -0
- package/src/pages/admin/calendar/bookings.astro +32 -26
- package/src/pages/admin/calendar/calendars.astro +4 -263
- package/src/pages/admin/calendar/events/[id].astro +9 -0
- package/src/pages/admin/calendar/events/index.astro +41 -83
- package/src/pages/admin/calendar/index.astro +43 -0
- package/src/pages/admin/calendar/resources/[id].astro +27 -5
- package/src/pages/admin/calendar/resources/index.astro +16 -17
- package/src/pages/admin/calendar/resources/link.astro +52 -12
- package/src/pages/admin/calendar/settings.astro +458 -0
- package/src/pages/admin/integrations.astro +5 -11
- package/src/pages/admin/login.astro +6 -2
- package/src/pages/admin/logout.ts +5 -2
- package/src/pages/admin/mobile.astro +18 -0
- package/src/pages/admin/pages/[id].astro +62 -0
- package/src/pages/admin/section-preview.astro +61 -0
- package/src/pages/admin/users.astro +107 -11
- package/src/pages/api/agent/docs.ts +6 -2
- package/src/pages/api/agent/index.ts +1 -1
- package/src/pages/api/ai/settings.ts +7 -4
- package/src/pages/api/design/ai.ts +15 -0
- package/src/pages/api/design/index.ts +12 -42
- package/src/pages/api/design/translate.ts +15 -0
- package/src/pages/api/live/thumb.ts +1 -1
- package/src/pages/api/mcp.ts +2 -2
- package/src/pages/api/media/upload.ts +10 -78
- package/src/pages/api/push/admin.ts +27 -0
- package/src/pages/api/v1/admin/bookings/[id]/[action].ts +14 -0
- package/src/pages/api/v1/admin/bookings/index.ts +6 -0
- package/src/pages/api/v1/admin/documents/[kind]/[id]/[action].ts +17 -0
- package/src/pages/api/v1/admin/documents/[kind]/[id]/index.ts +20 -0
- package/src/pages/api/v1/admin/documents/[kind]/[id]/translations/[locale]/[action].ts +26 -0
- package/src/pages/api/v1/admin/documents/[kind]/[id]/translations/[locale]/index.ts +18 -0
- package/src/pages/api/v1/admin/documents/index.ts +13 -0
- package/src/pages/api/v1/admin/me.ts +6 -0
- package/src/pages/api/v1/admin/media/index.ts +32 -0
- package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/index.ts +14 -0
- package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/publish.ts +14 -0
- package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]/[action].ts +22 -0
- package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]/index.ts +14 -0
- package/src/pages/api/v1/admin/sections/index.ts +6 -0
- package/src/pages/api/v1/auth/revoke.ts +5 -0
- package/src/pages/api/v1/auth/token.ts +5 -0
- package/src/pages/api/v1/devices/[token].ts +20 -0
- package/src/pages/api/v1/devices/index.ts +17 -0
- package/src/pages/api/v1/openapi.json.ts +7 -0
- package/src/pages/api/v1/pages/[...slug].ts +7 -0
- package/src/pages/api/v1/pages/home.ts +7 -0
- package/src/pages/api/v1/push/topics.ts +7 -0
- package/src/pages/api/v1/site.ts +7 -0
- package/src/pages/api/v1/theme.ts +7 -0
- package/src/pages/blog/[slug].astro +8 -2
- package/src/pages/blog/index.astro +11 -4
- package/src/pages/book/[resource].ics.ts +7 -1
- package/src/pages/embed/section/[...path].astro +42 -0
- package/src/pages/events/[slug].astro +7 -2
- package/src/pages/index.astro +11 -5
- package/src/pages/sitemap.xml.ts +8 -0
- package/src/styles/admin.css +117 -0
- package/src/styles/cards.css +5 -1
- package/src/styles/editor.css +57 -0
- package/src/styles/mobile-app.css +151 -0
- package/src/styles/schedule.css +325 -0
- package/src/styles/sections.css +21 -12
- package/src/styles/theme-editor.css +255 -0
- package/src/theme-sdk/contract.d.mts +2193 -89
- package/src/theme-sdk/contract.mjs +77 -20
- package/src/theme-sdk/fields.d.mts +88 -0
- package/src/theme-sdk/fields.mjs +330 -0
- package/src/theme-sdk/index.ts +14 -2
- package/src/theme-sdk/tokens.d.mts +11 -0
- package/src/virtual.d.ts +4 -0
- package/src/pages/admin/calendar/index.ts +0 -4
|
@@ -1,89 +1,21 @@
|
|
|
1
1
|
import type { APIRoute } from 'astro';
|
|
2
2
|
import { validateSession, SESSION_COOKIE } from '../../../lib/auth';
|
|
3
3
|
import { getRepo } from '../../../lib/repo';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
// Multipart upload from the admin. For images the client has already done
|
|
7
|
-
// the heavy lifting (EXIF rotation, downscale to max 1080px, WebP re-encode,
|
|
8
|
-
// variant generation); this route just stores the optimized file + variants
|
|
9
|
-
// and records the media row. SVG/GIF arrive untouched. Documents (for the
|
|
10
|
-
// file/download card) are stored as-is. Video/audio is deliberately rejected
|
|
11
|
-
// — embeds cover video until a streaming upload + Range-serving pipeline
|
|
12
|
-
// exists.
|
|
13
|
-
//
|
|
14
|
-
// Fields: file (primary), width, height, alt?, variant_<w> (File, webp)…
|
|
15
|
-
const DOCUMENT_TYPES = new Set([
|
|
16
|
-
'application/pdf',
|
|
17
|
-
'application/zip',
|
|
18
|
-
'application/x-zip-compressed',
|
|
19
|
-
'application/json',
|
|
20
|
-
'text/plain',
|
|
21
|
-
'text/csv',
|
|
22
|
-
'text/markdown',
|
|
23
|
-
'application/msword',
|
|
24
|
-
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
25
|
-
'application/vnd.ms-excel',
|
|
26
|
-
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
27
|
-
'application/vnd.ms-powerpoint',
|
|
28
|
-
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
29
|
-
'application/epub+zip',
|
|
30
|
-
]);
|
|
31
|
-
const MAX_IMAGE = 30 * 1024 * 1024;
|
|
32
|
-
const MAX_DOCUMENT = 25 * 1024 * 1024;
|
|
4
|
+
import { ApiError } from '../../../lib/api';
|
|
5
|
+
import { storeUpload } from '../../../lib/media/upload';
|
|
33
6
|
|
|
7
|
+
// Multipart upload from the admin; the rules live in lib/media/upload.ts
|
|
8
|
+
// (shared with editor apps).
|
|
34
9
|
export const POST: APIRoute = async ({ request, cookies, locals }) => {
|
|
35
10
|
if (!(await validateSession(cookies.get(SESSION_COOKIE)?.value, locals))) {
|
|
36
11
|
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
37
12
|
}
|
|
38
|
-
|
|
39
13
|
const form = await request.formData();
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return Response.json({
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (!isImage && !isDocument) {
|
|
47
|
-
const hint = file.type.startsWith('video/') || file.type.startsWith('audio/')
|
|
48
|
-
? 'Video and audio uploads aren’t supported yet — use an embed card instead.'
|
|
49
|
-
: 'Unsupported file type.';
|
|
50
|
-
return Response.json({ error: hint }, { status: 415 });
|
|
14
|
+
try {
|
|
15
|
+
const media = await storeUpload(form, locals, await getRepo(locals));
|
|
16
|
+
return Response.json({ media });
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (error instanceof ApiError) return Response.json({ error: error.message }, { status: error.status });
|
|
19
|
+
throw error;
|
|
51
20
|
}
|
|
52
|
-
const cap = isImage ? MAX_IMAGE : MAX_DOCUMENT;
|
|
53
|
-
if (file.size > cap) {
|
|
54
|
-
return Response.json({ error: `File too large (max ${Math.round(cap / 1024 / 1024)} MB)` }, { status: 413 });
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const id = crypto.randomUUID();
|
|
58
|
-
const ext = (file.name.split('.').pop() || 'bin').toLowerCase().replace(/[^a-z0-9]/g, '') || 'bin';
|
|
59
|
-
const key = `${id}/original.${ext}`;
|
|
60
|
-
const store = getMediaStore(locals);
|
|
61
|
-
await store.put(key, await file.arrayBuffer(), file.type);
|
|
62
|
-
|
|
63
|
-
const variants: { w: number; key: string }[] = [];
|
|
64
|
-
for (const [field, value] of form.entries()) {
|
|
65
|
-
const m = field.match(/^variant_(\d+)$/);
|
|
66
|
-
if (m && value instanceof File && value.size > 0) {
|
|
67
|
-
const w = Number(m[1]);
|
|
68
|
-
const vkey = `${id}/w${w}.webp`;
|
|
69
|
-
await store.put(vkey, await value.arrayBuffer(), 'image/webp');
|
|
70
|
-
variants.push({ w, key: vkey });
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
variants.sort((a, b) => a.w - b.w);
|
|
74
|
-
|
|
75
|
-
const repo = await getRepo(locals);
|
|
76
|
-
const media = await repo.createMedia({
|
|
77
|
-
id,
|
|
78
|
-
key,
|
|
79
|
-
filename: file.name,
|
|
80
|
-
mime: file.type,
|
|
81
|
-
width: Number(form.get('width') ?? 0),
|
|
82
|
-
height: Number(form.get('height') ?? 0),
|
|
83
|
-
size: file.size,
|
|
84
|
-
alt: String(form.get('alt') ?? ''),
|
|
85
|
-
variants,
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
return Response.json({ media });
|
|
89
21
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { getRepo } from '../../../lib/repo';
|
|
3
|
+
import { ApiError, apiError, json, readJson, requireSession } from '../../../lib/api';
|
|
4
|
+
import { OWNER_ENV, SESSION_COOKIE, validateSession } from '../../../lib/auth';
|
|
5
|
+
import { AdminPushInput, adminPushAction, adminPushState } from '../../../lib/push/api';
|
|
6
|
+
|
|
7
|
+
// Admin → Mobile app (owner only; registered only when push is enabled).
|
|
8
|
+
export const GET: APIRoute = async ({ request, cookies, locals, url }) => {
|
|
9
|
+
try {
|
|
10
|
+
const repo = await getRepo(locals);
|
|
11
|
+
await requireSession(request, cookies, repo, true);
|
|
12
|
+
return json(await adminPushState(repo, url, locals));
|
|
13
|
+
} catch (e) { return apiError(e); }
|
|
14
|
+
};
|
|
15
|
+
export const POST: APIRoute = async ({ request, cookies, locals, url }) => {
|
|
16
|
+
try {
|
|
17
|
+
const repo = await getRepo(locals);
|
|
18
|
+
await requireSession(request, cookies, repo, true);
|
|
19
|
+
// sending reaches every installed app: only from this site's admin
|
|
20
|
+
if (request.headers.get('origin') !== url.origin) throw new ApiError(403, 'Send from this site’s admin.');
|
|
21
|
+
const input = AdminPushInput.parse(await readJson(request, 20_000));
|
|
22
|
+
const id = await validateSession(cookies.get(SESSION_COOKIE)?.value, repo);
|
|
23
|
+
const author = id === OWNER_ENV ? 'Owner' : (await repo.getUser(id ?? ''))?.name ?? '';
|
|
24
|
+
const result = await adminPushAction(repo, input, { url, locals, author });
|
|
25
|
+
return json({ ...result, state: input.action === 'count' ? undefined : await adminPushState(repo, url, locals) });
|
|
26
|
+
} catch (e) { return apiError(e); }
|
|
27
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { editorRoute, logWrite } from '../../../../../../lib/editor/http';
|
|
2
|
+
import { BookingActionInput, bookingAction } from '../../../../../../lib/editor/bookings';
|
|
3
|
+
import { methodNotAllowed } from '../../../../../../lib/mobile/http';
|
|
4
|
+
import { readJson } from '../../../../../../lib/api';
|
|
5
|
+
export { preflight as OPTIONS } from '../../../../../../lib/mobile/http';
|
|
6
|
+
// approve | decline | cancel — the guest is emailed, as from Admin → Bookings
|
|
7
|
+
export const POST = editorRoute(async (c) => {
|
|
8
|
+
const { id, action } = c.context.params;
|
|
9
|
+
const body = c.context.request.headers.get('content-type') ? BookingActionInput.parse(await readJson(c.context.request, 5_000)) : {};
|
|
10
|
+
const result = await bookingAction(c.repo, id!, action!, body, { url: c.context.url, locals: c.context.locals });
|
|
11
|
+
await logWrite(c, `booking-${action}`, `booking:${id}`);
|
|
12
|
+
return result;
|
|
13
|
+
}, { write: true });
|
|
14
|
+
export const ALL = () => methodNotAllowed('POST, OPTIONS');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { editorRoute } from '../../../../../lib/editor/http';
|
|
2
|
+
import { InboxQuery, inbox } from '../../../../../lib/editor/bookings';
|
|
3
|
+
import { methodNotAllowed, queryParams } from '../../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
|
|
5
|
+
export const GET = editorRoute(async ({ repo, context }) => inbox(repo, InboxQuery.parse(queryParams(context.url))));
|
|
6
|
+
export const ALL = () => methodNotAllowed('GET, OPTIONS');
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { editorRoute, logWrite } from '../../../../../../../lib/editor/http';
|
|
2
|
+
import { ActionInput, DocKindParam, docAction, docJson } from '../../../../../../../lib/editor/documents';
|
|
3
|
+
import { methodNotAllowed } from '../../../../../../../lib/mobile/http';
|
|
4
|
+
import { ApiError, readJson } from '../../../../../../../lib/api';
|
|
5
|
+
import { invalidateRedirectCache } from '../../../../../../../middleware';
|
|
6
|
+
export { preflight as OPTIONS } from '../../../../../../../lib/mobile/http';
|
|
7
|
+
// publish | unpublish | schedule | discard
|
|
8
|
+
export const POST = editorRoute(async (c) => {
|
|
9
|
+
const kind = DocKindParam.parse(c.context.params.kind);
|
|
10
|
+
const action = c.context.params.action!;
|
|
11
|
+
if (!['publish', 'unpublish', 'schedule', 'discard'].includes(action)) throw new ApiError(404, 'Unknown action.');
|
|
12
|
+
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
|
+
await logWrite(c, action, `${kind}:${doc.id}`);
|
|
15
|
+
return docJson(kind, doc);
|
|
16
|
+
}, { write: true });
|
|
17
|
+
export const ALL = () => methodNotAllowed('POST, OPTIONS');
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { editorRoute, logWrite } from '../../../../../../../lib/editor/http';
|
|
2
|
+
import { DocKindParam, SaveInput, docJson, loadDoc, saveDoc } from '../../../../../../../lib/editor/documents';
|
|
3
|
+
import { methodNotAllowed, queryParams } from '../../../../../../../lib/mobile/http';
|
|
4
|
+
import { readJson } from '../../../../../../../lib/api';
|
|
5
|
+
import { invalidateRedirectCache } from '../../../../../../../middleware';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
export { preflight as OPTIONS } from '../../../../../../../lib/mobile/http';
|
|
8
|
+
export const GET = editorRoute(async ({ repo, context }) => {
|
|
9
|
+
z.object({}).strict().parse(queryParams(context.url));
|
|
10
|
+
const kind = DocKindParam.parse(context.params.kind);
|
|
11
|
+
return docJson(kind, await loadDoc(repo, kind, context.params.id!));
|
|
12
|
+
});
|
|
13
|
+
export const PUT = editorRoute(async (c) => {
|
|
14
|
+
const kind = DocKindParam.parse(c.context.params.kind);
|
|
15
|
+
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
|
+
await logWrite(c, 'save', `${kind}:${doc.id}`);
|
|
18
|
+
return docJson(kind, doc);
|
|
19
|
+
}, { write: true });
|
|
20
|
+
export const ALL = () => methodNotAllowed('GET, PUT, OPTIONS');
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { editorRoute, logWrite } from '../../../../../../../../../lib/editor/http';
|
|
2
|
+
import { DocKindParam } from '../../../../../../../../../lib/editor/documents';
|
|
3
|
+
import { AiDocInput, ReviewDocInput, changeDocTranslation } from '../../../../../../../../../lib/editor/translations';
|
|
4
|
+
import { failure, methodNotAllowed } from '../../../../../../../../../lib/mobile/http';
|
|
5
|
+
import { ApiError, readJson } from '../../../../../../../../../lib/api';
|
|
6
|
+
import { invalidateRedirectCache } from '../../../../../../../../../middleware';
|
|
7
|
+
export { preflight as OPTIONS } from '../../../../../../../../../lib/mobile/http';
|
|
8
|
+
// review: mark translated units reviewed · ai-draft: machine drafts for what's missing
|
|
9
|
+
const handler = (ai: boolean) => editorRoute(async (c) => {
|
|
10
|
+
const { params } = c.context;
|
|
11
|
+
const kind = DocKindParam.parse(params.kind);
|
|
12
|
+
const hooks = { url: c.context.url, locals: c.context.locals, onRedirect: invalidateRedirectCache };
|
|
13
|
+
const raw = await readJson(c.context.request, 10_000);
|
|
14
|
+
const result = ai
|
|
15
|
+
? 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); })()
|
|
16
|
+
: await (async () => { const b = ReviewDocInput.parse(raw); return changeDocTranslation(c.repo, kind, params.id!, params.locale!, b.revision, { op: 'review', paths: b.paths }, hooks); })();
|
|
17
|
+
await logWrite(c, ai ? 'translate-ai' : 'translate-review', `${kind}:${params.id}:${params.locale}`);
|
|
18
|
+
return result;
|
|
19
|
+
}, { write: true, ...(ai ? { limit: 10 } : {}) });
|
|
20
|
+
const review = handler(false), aiDraft = handler(true);
|
|
21
|
+
export const POST: import('astro').APIRoute = (context) => {
|
|
22
|
+
if (context.params.action === 'review') return review(context);
|
|
23
|
+
if (context.params.action === 'ai-draft') return aiDraft(context);
|
|
24
|
+
return failure(new ApiError(404, 'Unknown action.'));
|
|
25
|
+
};
|
|
26
|
+
export const ALL = () => methodNotAllowed('POST, OPTIONS');
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { editorRoute, logWrite } from '../../../../../../../../../lib/editor/http';
|
|
2
|
+
import { DocKindParam } from '../../../../../../../../../lib/editor/documents';
|
|
3
|
+
import { SetDocInput, changeDocTranslation, getDocTranslation } from '../../../../../../../../../lib/editor/translations';
|
|
4
|
+
import { methodNotAllowed } from '../../../../../../../../../lib/mobile/http';
|
|
5
|
+
import { readJson } from '../../../../../../../../../lib/api';
|
|
6
|
+
import { invalidateRedirectCache } from '../../../../../../../../../middleware';
|
|
7
|
+
export { preflight as OPTIONS } from '../../../../../../../../../lib/mobile/http';
|
|
8
|
+
export const GET = editorRoute(async ({ repo, context: { params } }) => getDocTranslation(repo, DocKindParam.parse(params.kind), params.id!, params.locale!));
|
|
9
|
+
// a person's words: stored as reviewed
|
|
10
|
+
export const PUT = editorRoute(async (c) => {
|
|
11
|
+
const { params } = c.context;
|
|
12
|
+
const kind = DocKindParam.parse(params.kind);
|
|
13
|
+
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
|
+
await logWrite(c, 'translate', `${kind}:${params.id}:${params.locale}`);
|
|
16
|
+
return result;
|
|
17
|
+
}, { write: true });
|
|
18
|
+
export const ALL = () => methodNotAllowed('GET, PUT, OPTIONS');
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { editorRoute, logWrite } from '../../../../../lib/editor/http';
|
|
2
|
+
import { CreateInput, createDoc, docJson, listDocs, listQuery } from '../../../../../lib/editor/documents';
|
|
3
|
+
import { methodNotAllowed, queryParams } from '../../../../../lib/mobile/http';
|
|
4
|
+
import { readJson } from '../../../../../lib/api';
|
|
5
|
+
export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
|
|
6
|
+
export const GET = editorRoute(async ({ repo, context }) => listDocs(repo, listQuery.parse(queryParams(context.url))));
|
|
7
|
+
export const POST = editorRoute(async (c) => {
|
|
8
|
+
const input = CreateInput.parse(await readJson(c.context.request, 20_000));
|
|
9
|
+
const doc = await createDoc(c.repo, input, c.who.user.id);
|
|
10
|
+
await logWrite(c, 'create', `${input.kind}:${doc.id}`);
|
|
11
|
+
return docJson(input.kind, doc);
|
|
12
|
+
}, { write: true, status: 201 });
|
|
13
|
+
export const ALL = () => methodNotAllowed('GET, POST, OPTIONS');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { editorRoute } from '../../../../lib/editor/http';
|
|
2
|
+
import { me } from '../../../../lib/editor/me';
|
|
3
|
+
import { methodNotAllowed } from '../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../lib/mobile/http';
|
|
5
|
+
export const GET = editorRoute(({ repo, who, context }) => me(repo, who, context.url, context.locals));
|
|
6
|
+
export const ALL = () => methodNotAllowed('GET, OPTIONS');
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { editorRoute, logWrite } from '../../../../../lib/editor/http';
|
|
3
|
+
import { methodNotAllowed, pageQuery, queryParams } from '../../../../../lib/mobile/http';
|
|
4
|
+
import { ApiError, readResponseBytes } from '../../../../../lib/api';
|
|
5
|
+
import { MAX_UPLOAD_BODY, storeUpload } from '../../../../../lib/media/upload';
|
|
6
|
+
import type { Media } from '../../../../../lib/schema';
|
|
7
|
+
export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
|
|
8
|
+
const view = (m: Media) => ({ id: m.id, url: `/media/${m.key}`, filename: m.filename, mime: m.mime, width: m.width, height: m.height, size: m.size, alt: m.alt,
|
|
9
|
+
variants: m.variants.map((v) => ({ width: v.w, url: `/media/${v.key}` })), createdAt: new Date(m.createdAt).toISOString() });
|
|
10
|
+
const ListQuery = z.object({ q: z.string().max(200).optional(), type: z.enum(['image', 'document']).optional(), ...pageQuery }).strict();
|
|
11
|
+
export const GET = editorRoute(async ({ repo, context }) => {
|
|
12
|
+
const q = ListQuery.parse(queryParams(context.url));
|
|
13
|
+
let list = await repo.listMedia();
|
|
14
|
+
if (q.type) list = list.filter((m) => (q.type === 'image') === m.mime.startsWith('image/'));
|
|
15
|
+
if (q.q) { const needle = q.q.toLowerCase(); list = list.filter((m) => `${m.filename} ${m.alt}`.toLowerCase().includes(needle)); }
|
|
16
|
+
list.sort((a, b) => b.createdAt - a.createdAt);
|
|
17
|
+
const start = (q.page - 1) * q.limit;
|
|
18
|
+
return { items: list.slice(start, start + q.limit).map(view), pagination: { page: q.page, limit: q.limit, nextPage: start + q.limit < list.length ? q.page + 1 : null } };
|
|
19
|
+
});
|
|
20
|
+
// same types, limits and storage as the admin uploader (images arrive pre-compressed)
|
|
21
|
+
export const POST = editorRoute(async (c) => {
|
|
22
|
+
const type = c.context.request.headers.get('content-type') ?? '';
|
|
23
|
+
if (!type.toLowerCase().startsWith('multipart/form-data')) throw new ApiError(415, 'Use Content-Type: multipart/form-data with a "file" field.');
|
|
24
|
+
const bytes = await readResponseBytes(c.context.request, MAX_UPLOAD_BODY);
|
|
25
|
+
let form: FormData;
|
|
26
|
+
try { form = await new Response(bytes as BodyInit, { headers: { 'content-type': type } }).formData(); }
|
|
27
|
+
catch { throw new ApiError(400, 'The upload is not valid multipart form data.'); }
|
|
28
|
+
const media = await storeUpload(form, c.context.locals, c.repo);
|
|
29
|
+
await logWrite(c, 'upload', `media:${media.id}`);
|
|
30
|
+
return view(media);
|
|
31
|
+
}, { write: true, status: 201, limit: 30 });
|
|
32
|
+
export const ALL = () => methodNotAllowed('GET, POST, OPTIONS');
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { editorRoute, logWrite } from '../../../../../../../lib/editor/http';
|
|
2
|
+
import { SectionPatch, patchSection } from '../../../../../../../lib/editor/sections';
|
|
3
|
+
import { methodNotAllowed } from '../../../../../../../lib/mobile/http';
|
|
4
|
+
import { readJson } from '../../../../../../../lib/api';
|
|
5
|
+
export { preflight as OPTIONS } from '../../../../../../../lib/mobile/http';
|
|
6
|
+
// stage new field values for one section (content only for editors; locks apply)
|
|
7
|
+
export const PATCH = editorRoute(async (c) => {
|
|
8
|
+
const { pageId, sectionId } = c.context.params;
|
|
9
|
+
const body = SectionPatch.parse(await readJson(c.context.request, 500_000));
|
|
10
|
+
const result = await patchSection(c.repo, c.who, pageId!, sectionId!, body, c.context.url.origin);
|
|
11
|
+
await logWrite(c, 'section', `${pageId}:${sectionId}`);
|
|
12
|
+
return result;
|
|
13
|
+
}, { write: true });
|
|
14
|
+
export const ALL = () => methodNotAllowed('PATCH, OPTIONS');
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { editorRoute, logWrite } from '../../../../../../../lib/editor/http';
|
|
2
|
+
import { RevisionInput, publishSection } from '../../../../../../../lib/editor/sections';
|
|
3
|
+
import { methodNotAllowed } from '../../../../../../../lib/mobile/http';
|
|
4
|
+
import { readJson } from '../../../../../../../lib/api';
|
|
5
|
+
export { preflight as OPTIONS } from '../../../../../../../lib/mobile/http';
|
|
6
|
+
// publish just this section's staged copy (same rules as the Design screen)
|
|
7
|
+
export const POST = editorRoute(async (c) => {
|
|
8
|
+
const { pageId, sectionId } = c.context.params;
|
|
9
|
+
const body = RevisionInput.parse(await readJson(c.context.request, 5_000));
|
|
10
|
+
const result = await publishSection(c.repo, c.who, pageId!, sectionId!, body.revision, c.context.url.origin);
|
|
11
|
+
await logWrite(c, 'section-publish', `${pageId}:${sectionId}`);
|
|
12
|
+
return result;
|
|
13
|
+
}, { write: true });
|
|
14
|
+
export const ALL = () => methodNotAllowed('POST, OPTIONS');
|
package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]/[action].ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { editorRoute, logWrite } from '../../../../../../../../../lib/editor/http';
|
|
3
|
+
import { AiSectionInput, ReviewSectionInput, changeSectionTranslation } from '../../../../../../../../../lib/editor/translations';
|
|
4
|
+
import { failure, methodNotAllowed } from '../../../../../../../../../lib/mobile/http';
|
|
5
|
+
import { ApiError, readJson } from '../../../../../../../../../lib/api';
|
|
6
|
+
export { preflight as OPTIONS } from '../../../../../../../../../lib/mobile/http';
|
|
7
|
+
const handler = (ai: boolean) => editorRoute(async (c) => {
|
|
8
|
+
const { params } = c.context;
|
|
9
|
+
const raw = await readJson(c.context.request, 10_000);
|
|
10
|
+
const mode = ai ? (() => { const b = AiSectionInput.parse(raw); return { revision: b.revision, mode: { op: 'ai' as const, paths: b.paths, includeReviewed: b.includeReviewed } }; })()
|
|
11
|
+
: (() => { const b = ReviewSectionInput.parse(raw); return { revision: b.revision, mode: { op: 'review' as const, paths: b.paths } }; })();
|
|
12
|
+
const result = await changeSectionTranslation(c.repo, c.who, params.pageId!, params.sectionId!, params.locale!, mode.revision, mode.mode, c.context.url.origin, c.context.locals);
|
|
13
|
+
await logWrite(c, ai ? 'translate-ai' : 'translate-review', `${params.pageId}:${params.sectionId}:${params.locale}`);
|
|
14
|
+
return result;
|
|
15
|
+
}, { write: true, ...(ai ? { limit: 10 } : {}) });
|
|
16
|
+
const review = handler(false), aiDraft = handler(true);
|
|
17
|
+
export const POST: APIRoute = (context) => {
|
|
18
|
+
if (context.params.action === 'review') return review(context);
|
|
19
|
+
if (context.params.action === 'ai-draft') return aiDraft(context);
|
|
20
|
+
return failure(new ApiError(404, 'Unknown action.'));
|
|
21
|
+
};
|
|
22
|
+
export const ALL = () => methodNotAllowed('POST, OPTIONS');
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { editorRoute, logWrite } from '../../../../../../../../../lib/editor/http';
|
|
2
|
+
import { SetSectionInput, changeSectionTranslation, getSectionTranslation } from '../../../../../../../../../lib/editor/translations';
|
|
3
|
+
import { methodNotAllowed } from '../../../../../../../../../lib/mobile/http';
|
|
4
|
+
import { readJson } from '../../../../../../../../../lib/api';
|
|
5
|
+
export { preflight as OPTIONS } from '../../../../../../../../../lib/mobile/http';
|
|
6
|
+
export const GET = editorRoute(async ({ repo, context: { params } }) => getSectionTranslation(repo, params.pageId!, params.sectionId!, params.locale!));
|
|
7
|
+
export const PUT = editorRoute(async (c) => {
|
|
8
|
+
const { params } = c.context;
|
|
9
|
+
const body = SetSectionInput.parse(await readJson(c.context.request, 500_000));
|
|
10
|
+
const result = await changeSectionTranslation(c.repo, c.who, params.pageId!, params.sectionId!, params.locale!, body.revision, { op: 'set', values: body.values }, c.context.url.origin, c.context.locals);
|
|
11
|
+
await logWrite(c, 'translate', `${params.pageId}:${params.sectionId}:${params.locale}`);
|
|
12
|
+
return result;
|
|
13
|
+
}, { write: true });
|
|
14
|
+
export const ALL = () => methodNotAllowed('GET, PUT, OPTIONS');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { editorRoute } from '../../../../../lib/editor/http';
|
|
2
|
+
import { SectionsQuery, listSections } from '../../../../../lib/editor/sections';
|
|
3
|
+
import { methodNotAllowed, queryParams } from '../../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
|
|
5
|
+
export const GET = editorRoute(async ({ repo, who, context }) => listSections(repo, SectionsQuery.parse(queryParams(context.url)), who));
|
|
6
|
+
export const ALL = () => methodNotAllowed('GET, OPTIONS');
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { authPreflight, revokeRoute } from '../../../../lib/editor/token-route';
|
|
3
|
+
// Editor apps: sign this device out (RFC 7009). Either token revokes the device.
|
|
4
|
+
export const POST: APIRoute = (context) => revokeRoute(context);
|
|
5
|
+
export const OPTIONS: APIRoute = () => authPreflight();
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { authPreflight, tokenRoute } from '../../../../lib/editor/token-route';
|
|
3
|
+
// Editor apps: exchange a sign-in code (+ PKCE verifier) or a refresh token for device tokens.
|
|
4
|
+
export const POST: APIRoute = (context) => tokenRoute(context);
|
|
5
|
+
export const OPTIONS: APIRoute = () => authPreflight();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { getRepo } from '../../../../lib/repo';
|
|
3
|
+
import { unregisterDevice, updateDevice } from '../../../../lib/push/api';
|
|
4
|
+
import { failure, limitRequest, methodNotAllowed, response } from '../../../../lib/mobile/http';
|
|
5
|
+
export { preflight as OPTIONS } from '../../../../lib/mobile/http';
|
|
6
|
+
// PUT: change topics/locale. DELETE: opt out (idempotent).
|
|
7
|
+
function handler(run: (repo: Awaited<ReturnType<typeof getRepo>>, request: Request, token: string) => Promise<unknown>): APIRoute {
|
|
8
|
+
return async context => {
|
|
9
|
+
try {
|
|
10
|
+
const repo = await getRepo(context.locals);
|
|
11
|
+
let address = 'unknown';
|
|
12
|
+
try { address = context.clientAddress || address; } catch { /* shared fallback bucket */ }
|
|
13
|
+
await limitRequest(repo, address, true);
|
|
14
|
+
return response(await run(repo, context.request, context.params.token ?? ''));
|
|
15
|
+
} catch (error) { return failure(error); }
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export const PUT = handler((repo, request, token) => updateDevice(repo, request, token));
|
|
19
|
+
export const DELETE = handler((repo, _request, token) => unregisterDevice(repo, token));
|
|
20
|
+
export const ALL = () => methodNotAllowed('PUT, DELETE, OPTIONS');
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { getRepo } from '../../../../lib/repo';
|
|
3
|
+
import { registerDevice } from '../../../../lib/push/api';
|
|
4
|
+
import { failure, limitRequest, methodNotAllowed, response } from '../../../../lib/mobile/http';
|
|
5
|
+
export { preflight as OPTIONS } from '../../../../lib/mobile/http';
|
|
6
|
+
// Register or refresh this device's Expo push token (only when push is enabled).
|
|
7
|
+
export const POST: APIRoute = async context => {
|
|
8
|
+
try {
|
|
9
|
+
const repo = await getRepo(context.locals);
|
|
10
|
+
let address = 'unknown';
|
|
11
|
+
try { address = context.clientAddress || address; } catch { /* shared fallback bucket */ }
|
|
12
|
+
await limitRequest(repo, address, true);
|
|
13
|
+
const result = await registerDevice(repo, context.request);
|
|
14
|
+
return response(result, result.created ? 201 : 200);
|
|
15
|
+
} catch (error) { return failure(error); }
|
|
16
|
+
};
|
|
17
|
+
export const ALL = () => methodNotAllowed('POST, OPTIONS');
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { getOpenApi } from '../../../lib/mobile/openapi';
|
|
2
|
+
import { readRoute } from '../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../lib/mobile/http';
|
|
5
|
+
export const GET = readRoute(getOpenApi, true, true);
|
|
6
|
+
export const HEAD = GET;
|
|
7
|
+
export const ALL = () => methodNotAllowed();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { getPage } from '../../../../lib/mobile/pages';
|
|
2
|
+
import { readRoute } from '../../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../lib/mobile/http';
|
|
5
|
+
export const GET = readRoute(getPage);
|
|
6
|
+
export const HEAD = GET;
|
|
7
|
+
export const ALL = () => methodNotAllowed();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { getPage } from '../../../../lib/mobile/pages';
|
|
2
|
+
import { readRoute } from '../../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../lib/mobile/http';
|
|
5
|
+
export const GET = readRoute((repo, url) => getPage(repo, url, 'home'));
|
|
6
|
+
export const HEAD = GET;
|
|
7
|
+
export const ALL = () => methodNotAllowed();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { listPushTopics } from '../../../../lib/push/api';
|
|
2
|
+
import { readRoute } from '../../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../lib/mobile/http';
|
|
5
|
+
export const GET = readRoute(listPushTopics, true);
|
|
6
|
+
export const HEAD = GET;
|
|
7
|
+
export const ALL = () => methodNotAllowed();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { getSite } from '../../../lib/mobile/site';
|
|
2
|
+
import { readRoute } from '../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../lib/mobile/http';
|
|
5
|
+
export const GET = readRoute(getSite);
|
|
6
|
+
export const HEAD = GET;
|
|
7
|
+
export const ALL = () => methodNotAllowed();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { getThemeTokens } from '../../../lib/mobile/site';
|
|
2
|
+
import { readRoute } from '../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../lib/mobile/http';
|
|
5
|
+
export const GET = readRoute(getThemeTokens);
|
|
6
|
+
export const HEAD = GET;
|
|
7
|
+
export const ALL = () => methodNotAllowed();
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { requestLocale } from '../../lib/i18n';
|
|
3
|
+
import { localizeDoc, isExtraLocale } from '../../lib/translations';
|
|
2
4
|
import Site from '../../layouts/Site.astro';
|
|
3
5
|
import { getRepo } from '../../lib/repo';
|
|
4
6
|
import { renderMarkdown } from '../../lib/markdown';
|
|
@@ -20,7 +22,10 @@ if (!col) return new Response(null, { status: 404, statusText: 'Not found' });
|
|
|
20
22
|
|
|
21
23
|
const { slug } = Astro.params;
|
|
22
24
|
const repo = await getRepo(Astro.locals);
|
|
23
|
-
|
|
25
|
+
// another language (/es/…): translated words over the shared post
|
|
26
|
+
const locale = requestLocale(Astro.url, Astro.locals);
|
|
27
|
+
const rawPost = slug ? await repo.getPostBySlug(slug, col.key) : null;
|
|
28
|
+
const post = rawPost ? localizeDoc(rawPost, locale) : null;
|
|
24
29
|
|
|
25
30
|
const isAdmin = (await validateSession(Astro.cookies.get(SESSION_COOKIE)?.value, Astro.locals));
|
|
26
31
|
if (!post || (post.status !== 'published' && !isAdmin)) {
|
|
@@ -35,7 +40,8 @@ const [tags, allUsers, allPosts, rawSettings, allMedia] = await Promise.all([
|
|
|
35
40
|
post.cover.startsWith('/media/') ? repo.listMedia() : Promise.resolve([]),
|
|
36
41
|
]);
|
|
37
42
|
// ?designPreview=1 (admin-only) renders the staged Design-screen changes
|
|
38
|
-
const
|
|
43
|
+
const baseSettings = await effectiveSettings(rawSettings, Astro.url, Astro.cookies, Astro.locals);
|
|
44
|
+
const settings = isExtraLocale(locale) ? { ...baseSettings, locale } : baseSettings;
|
|
39
45
|
// ordered author objects; the first is the primary (gets the author card)
|
|
40
46
|
const authors = authorIds
|
|
41
47
|
.map((id) => allUsers.find((u) => u.id === id))
|
|
@@ -8,6 +8,8 @@ import { formatDate } from '../../lib/dates';
|
|
|
8
8
|
import { resolveDesign } from '../../lib/theme';
|
|
9
9
|
import { effectiveSettings } from '../../lib/designPreview';
|
|
10
10
|
import { collectionFromPath, collectionUrl, tagUrl } from '../../lib/content';
|
|
11
|
+
import { requestLocale, localePath } from '../../lib/i18n';
|
|
12
|
+
import { localizeDoc, isExtraLocale } from '../../lib/translations';
|
|
11
13
|
|
|
12
14
|
// One file serves every collection's index — injectRoute can't pass props,
|
|
13
15
|
// so the collection is resolved from the request path.
|
|
@@ -16,8 +18,13 @@ if (!col) return new Response(null, { status: 404, statusText: 'Not found' });
|
|
|
16
18
|
|
|
17
19
|
const PAGE_SIZE = 12;
|
|
18
20
|
const repo = await getRepo(Astro.locals);
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
+
// another language (/es/blog): translated titles/excerpts, links stay in the language
|
|
22
|
+
const locale = requestLocale(Astro.url, Astro.locals);
|
|
23
|
+
const lp = (path: string) => (isExtraLocale(locale) ? localePath(path, locale) : path);
|
|
24
|
+
const [rawPosts, tags, users, rawSettings] = await Promise.all([repo.listPublishedPosts(col.key), repo.listTags(), repo.listUsers(), repo.getSettings()]);
|
|
25
|
+
const allPosts = rawPosts.map((p) => localizeDoc(p, locale));
|
|
26
|
+
const baseSettings = await effectiveSettings(rawSettings, Astro.url, Astro.cookies, Astro.locals);
|
|
27
|
+
const settings = isExtraLocale(locale) ? { ...baseSettings, locale } : baseSettings;
|
|
21
28
|
const design = resolveDesign(settings);
|
|
22
29
|
|
|
23
30
|
// page 1 leads with the featured post (if any) as a big split card; it's
|
|
@@ -81,7 +88,7 @@ const iso = (ts: number | null) => (ts ? new Date(ts).toISOString() : undefined)
|
|
|
81
88
|
<PostCard
|
|
82
89
|
variant="feature"
|
|
83
90
|
headingLevel={2}
|
|
84
|
-
href={collectionUrl(col.key, lead.slug)}
|
|
91
|
+
href={lp(collectionUrl(col.key, lead.slug))}
|
|
85
92
|
title={lead.title}
|
|
86
93
|
excerpt={lead.excerpt}
|
|
87
94
|
cover={lead.cover}
|
|
@@ -99,7 +106,7 @@ const iso = (ts: number | null) => (ts ? new Date(ts).toISOString() : undefined)
|
|
|
99
106
|
<PostCard
|
|
100
107
|
variant={layout === 'cards' ? 'card' : 'row'}
|
|
101
108
|
headingLevel={2}
|
|
102
|
-
href={collectionUrl(col.key, p.slug)}
|
|
109
|
+
href={lp(collectionUrl(col.key, p.slug))}
|
|
103
110
|
title={p.title}
|
|
104
111
|
excerpt={p.excerpt}
|
|
105
112
|
cover={p.cover}
|
|
@@ -4,8 +4,9 @@ import { getRepo } from '../../lib/repo';
|
|
|
4
4
|
import { bookingsEnabled } from '../../lib/calendar/config';
|
|
5
5
|
import { siteOrigin } from '../../lib/seo';
|
|
6
6
|
import { escapeText, foldLine } from '../../lib/calendar/ics-write';
|
|
7
|
+
import { eventHolds } from '../../lib/booking/holds';
|
|
7
8
|
|
|
8
|
-
// Staff feed of a resource's bookings (names included), for Google/Apple/
|
|
9
|
+
// Staff feed of a resource's bookings (names included) and event holds, for Google/Apple/
|
|
9
10
|
// Outlook subscriptions: /book/<slug>.ics?t=<secret from the admin>.
|
|
10
11
|
const same = (a: string, b: string) => a.length === b.length && a.length > 0 && timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
11
12
|
const utc = (ms: number) => new Date(ms).toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
|
|
@@ -31,6 +32,11 @@ export const GET: APIRoute = async ({ params, url, locals }) => {
|
|
|
31
32
|
lines.push('BEGIN:VEVENT', `UID:booking-${b.id}@${host}`, `DTSTAMP:${utc(Date.now())}`, `DTSTART:${utc(b.start)}`, `DTEND:${utc(b.end)}`,
|
|
32
33
|
`SUMMARY:${escapeText(title)}`, `DESCRIPTION:${escapeText(desc)}`, `STATUS:${b.status === 'pending' ? 'TENTATIVE' : 'CONFIRMED'}`, 'END:VEVENT');
|
|
33
34
|
}
|
|
35
|
+
// events holding this room (next year), so staff calendars show them too
|
|
36
|
+
for (const h of await eventHolds(repo, settings, Date.now() - 60 * 86_400_000, Date.now() + 366 * 86_400_000, resource.id)) {
|
|
37
|
+
lines.push('BEGIN:VEVENT', `UID:hold-${h.eventId}-${h.start}@${host}`, `DTSTAMP:${utc(Date.now())}`, `DTSTART:${utc(h.start)}`, `DTEND:${utc(h.end)}`,
|
|
38
|
+
`SUMMARY:${escapeText(`Held · ${h.title}`)}`, `DESCRIPTION:${escapeText(`${origin}/admin/calendar/events/${h.eventId}`)}`, 'STATUS:CONFIRMED', 'END:VEVENT');
|
|
39
|
+
}
|
|
34
40
|
lines.push('END:VCALENDAR');
|
|
35
41
|
return new Response(lines.map(foldLine).join('\r\n') + '\r\n', {
|
|
36
42
|
headers: { 'Content-Type': 'text/calendar; charset=utf-8', 'Cache-Control': 'private, no-store' },
|