@jant/core 0.3.35 → 0.3.37
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/bin/commands/export.js +1 -1
- package/bin/commands/import-site.js +529 -0
- package/bin/commands/reset-password.js +3 -2
- package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
- package/dist/client/assets/module-RjUF93sV.js +716 -0
- package/dist/client/assets/native-48B9X9Wg.js +1 -0
- package/dist/client/assets/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4564 -3013
- package/dist/index.js +12885 -8161
- package/package.json +23 -6
- package/src/__tests__/helpers/app.ts +10 -10
- package/src/__tests__/helpers/db.ts +91 -87
- package/src/app.tsx +157 -31
- package/src/auth.ts +20 -2
- package/src/client/archive-nav.js +187 -0
- package/src/client/audio-player.ts +478 -0
- package/src/client/audio-processor.ts +84 -0
- package/src/{lib → client}/avatar-upload.ts +4 -3
- package/src/{lib → client}/collection-form-bridge.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
- package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
- package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
- package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +43 -0
- package/src/{ui → client}/components/collection-types.ts +3 -4
- package/src/client/components/compose-types.ts +174 -0
- package/src/client/components/jant-collection-form.ts +667 -0
- package/src/client/components/jant-collection-sidebar.ts +805 -0
- package/src/client/components/jant-compose-dialog.ts +2161 -0
- package/src/client/components/jant-compose-editor.ts +1813 -0
- package/src/client/components/jant-compose-fullscreen.ts +283 -0
- package/src/client/components/jant-media-lightbox.ts +259 -0
- package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
- package/src/{ui → client}/components/jant-post-form.ts +141 -12
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
- package/src/{ui → client}/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/{ui → client}/components/nav-manager-types.ts +6 -18
- package/src/{ui → client}/components/post-form-template.ts +137 -38
- package/src/{ui → client}/components/post-form-types.ts +15 -4
- package/src/client/compose-bridge.ts +583 -0
- package/src/{lib → client}/image-processor.ts +26 -8
- package/src/client/lazy-slugify.ts +51 -0
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/{lib → client}/nav-manager-bridge.ts +1 -1
- package/src/{lib → client}/post-form-bridge.ts +53 -2
- package/src/{lib → client}/settings-bridge.ts +3 -15
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/bubble-menu.ts +205 -0
- package/src/client/tiptap/create-editor.ts +86 -0
- package/src/client/tiptap/exitable-marks.ts +73 -0
- package/src/client/tiptap/extensions.ts +65 -0
- package/src/client/tiptap/image-node.ts +482 -0
- package/src/client/tiptap/link-toolbar.ts +371 -0
- package/src/client/tiptap/more-break.ts +50 -0
- package/src/client/tiptap/paste-image.ts +129 -0
- package/src/client/tiptap/slash-commands.ts +438 -0
- package/src/{lib → client}/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +44 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +27 -17
- package/src/db/__tests__/migrations.test.ts +118 -0
- package/src/db/index.ts +52 -0
- package/src/db/migrations/0000_baseline.sql +269 -0
- package/src/db/migrations/0001_fts_setup.sql +31 -0
- package/src/db/migrations/meta/0000_snapshot.json +703 -119
- package/src/db/migrations/meta/0001_snapshot.json +1337 -0
- package/src/db/migrations/meta/_journal.json +4 -39
- package/src/db/schema.ts +409 -140
- package/src/i18n/__tests__/detect.test.ts +115 -0
- package/src/i18n/context.tsx +2 -2
- package/src/i18n/detect.ts +85 -1
- package/src/i18n/i18n.ts +1 -1
- package/src/i18n/index.ts +2 -1
- package/src/i18n/locales/en.po +783 -1087
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +867 -812
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +878 -823
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +6 -0
- package/src/index.ts +5 -7
- package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
- package/src/lib/__tests__/constants.test.ts +0 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
- package/src/lib/__tests__/nanoid.test.ts +26 -0
- package/src/lib/__tests__/resolve-config.test.ts +2 -2
- package/src/lib/__tests__/schemas.test.ts +186 -65
- package/src/lib/__tests__/slug.test.ts +126 -0
- package/src/lib/__tests__/sse.test.ts +6 -6
- package/src/lib/__tests__/summary.test.ts +264 -0
- package/src/lib/__tests__/theme.test.ts +1 -1
- package/src/lib/__tests__/timeline.test.ts +33 -30
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
- package/src/lib/__tests__/url.test.ts +2 -2
- package/src/lib/__tests__/view.test.ts +140 -65
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +963 -0
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +77 -31
- package/src/lib/html.ts +2 -1
- package/src/lib/icon-catalog.ts +5033 -1
- package/src/lib/icons.ts +3 -2
- package/src/lib/index.ts +0 -1
- package/src/lib/markdown-to-tiptap.ts +286 -0
- package/src/lib/media-helpers.ts +22 -12
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +24 -5
- package/src/lib/resolve-config.ts +13 -2
- package/src/lib/schemas.ts +226 -58
- package/src/lib/search-snippet.ts +34 -0
- package/src/lib/slug.ts +96 -0
- package/src/lib/sse.ts +6 -6
- package/src/lib/storage.ts +115 -7
- package/src/lib/summary.ts +158 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +76 -34
- package/src/lib/tiptap-render.ts +191 -0
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +263 -14
- package/src/lib/url.ts +37 -22
- package/src/lib/view.ts +236 -55
- package/src/middleware/__tests__/auth.test.ts +191 -11
- package/src/middleware/__tests__/onboarding.test.ts +12 -10
- package/src/middleware/auth.ts +63 -9
- package/src/middleware/error-handler.ts +3 -3
- package/src/middleware/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +83 -2
- package/src/routes/__tests__/compose.test.ts +17 -24
- package/src/routes/api/__tests__/collections.test.ts +109 -61
- package/src/routes/api/__tests__/nav-items.test.ts +46 -29
- package/src/routes/api/__tests__/posts.test.ts +132 -68
- package/src/routes/api/__tests__/search.test.ts +15 -2
- package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
- package/src/routes/api/collections.ts +57 -31
- package/src/routes/api/custom-urls.ts +80 -0
- package/src/routes/api/export.ts +31 -0
- package/src/routes/api/nav-items.ts +23 -19
- package/src/routes/api/posts.ts +81 -62
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +92 -24
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/reset.tsx +5 -4
- package/src/routes/auth/setup.tsx +39 -31
- package/src/routes/auth/signin.tsx +13 -14
- package/src/routes/compose.tsx +27 -63
- package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +475 -99
- package/src/routes/feed/__tests__/rss.test.ts +22 -23
- package/src/routes/feed/rss.ts +6 -2
- package/src/routes/feed/sitemap.ts +2 -12
- package/src/routes/pages/__tests__/collections.test.ts +5 -6
- package/src/routes/pages/__tests__/featured.test.ts +36 -18
- package/src/routes/pages/archive.tsx +177 -37
- package/src/routes/pages/collection.tsx +43 -14
- package/src/routes/pages/collections.tsx +11 -2
- package/src/routes/pages/featured.tsx +27 -3
- package/src/routes/pages/home.tsx +15 -14
- package/src/routes/pages/latest.tsx +1 -11
- package/src/routes/pages/new.tsx +39 -0
- package/src/routes/pages/page.tsx +95 -49
- package/src/routes/pages/search.tsx +1 -1
- package/src/services/__tests__/api-token.test.ts +135 -0
- package/src/services/__tests__/collection.test.ts +275 -227
- package/src/services/__tests__/custom-url.test.ts +213 -0
- package/src/services/__tests__/media.test.ts +162 -22
- package/src/services/__tests__/navigation.test.ts +109 -68
- package/src/services/__tests__/post-timeline.test.ts +205 -32
- package/src/services/__tests__/post.test.ts +800 -230
- package/src/services/__tests__/search.test.ts +67 -10
- package/src/services/__tests__/settings.test.ts +3 -3
- package/src/services/api-token.ts +166 -0
- package/src/services/auth.ts +17 -2
- package/src/services/collection.ts +397 -131
- package/src/services/custom-url.ts +188 -0
- package/src/services/export.ts +802 -0
- package/src/services/index.ts +26 -19
- package/src/services/media.ts +100 -22
- package/src/services/navigation.ts +158 -47
- package/src/services/path.ts +339 -0
- package/src/services/post.ts +764 -172
- package/src/services/search.ts +161 -74
- package/src/services/settings.ts +6 -2
- package/src/styles/components.css +293 -62
- package/src/styles/tokens.css +93 -5
- package/src/styles/ui.css +4349 -766
- package/src/types/bindings.ts +8 -0
- package/src/types/config.ts +34 -4
- package/src/types/constants.ts +17 -2
- package/src/types/entities.ts +83 -37
- package/src/types/operations.ts +20 -27
- package/src/types/props.ts +52 -17
- package/src/types/views.ts +48 -24
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +255 -16
- package/src/ui/compose/ComposePrompt.tsx +1 -1
- package/src/ui/dash/CrudPageHeader.tsx +1 -1
- package/src/ui/dash/ListItemRow.tsx +1 -1
- package/src/ui/dash/StatusBadge.tsx +12 -2
- package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
- package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
- package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
- package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +87 -146
- package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
- package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
- package/src/ui/dash/settings/AvatarContent.tsx +78 -0
- package/src/ui/dash/settings/GeneralContent.tsx +3 -62
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
- package/src/ui/feed/LinkCard.tsx +89 -40
- package/src/ui/feed/NoteCard.tsx +39 -25
- package/src/ui/feed/PostStatusBadges.tsx +67 -0
- package/src/ui/feed/QuoteCard.tsx +38 -23
- package/src/ui/feed/ThreadPreview.tsx +90 -26
- package/src/ui/feed/TimelineFeed.tsx +3 -2
- package/src/ui/feed/TimelineItem.tsx +15 -6
- package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
- package/src/ui/feed/thread-preview-state.ts +61 -0
- package/src/ui/font-themes.ts +2 -2
- package/src/ui/layouts/BaseLayout.tsx +2 -2
- package/src/ui/layouts/SiteLayout.tsx +116 -103
- package/src/ui/pages/ArchivePage.tsx +923 -95
- package/src/ui/pages/CollectionPage.tsx +6 -35
- package/src/ui/pages/CollectionsPage.tsx +2 -1
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/FeaturedPage.tsx +2 -1
- package/src/ui/pages/HomePage.tsx +1 -1
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +182 -38
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +239 -4
- package/src/ui/shared/MediaGallery.tsx +475 -41
- package/src/ui/shared/PostFooter.tsx +204 -0
- package/src/ui/shared/StarRating.tsx +27 -0
- package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
- package/src/ui/shared/index.ts +0 -1
- package/src/db/migrations/0000_square_wallflower.sql +0 -118
- package/src/db/migrations/0001_add_search_fts.sql +0 -34
- package/src/db/migrations/0002_add_media_attachments.sql +0 -3
- package/src/db/migrations/0003_add_navigation_links.sql +0 -8
- package/src/db/migrations/0004_add_storage_provider.sql +0 -3
- package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
- package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
- package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
- package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
- package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
- package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
- package/src/db/migrations/0011_add_path_registry.sql +0 -23
- package/src/db/migrations/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- package/src/lib/collections-reorder.ts +0 -28
- package/src/lib/compose-bridge.ts +0 -280
- package/src/lib/media-upload.ts +0 -148
- package/src/lib/sqid.ts +0 -79
- package/src/routes/api/__tests__/pages.test.ts +0 -218
- package/src/routes/api/pages.ts +0 -73
- package/src/routes/dash/__tests__/pages.test.ts +0 -226
- package/src/routes/dash/appearance.tsx +0 -240
- package/src/routes/dash/collections.tsx +0 -211
- package/src/routes/dash/index.tsx +0 -103
- package/src/routes/dash/media.tsx +0 -132
- package/src/routes/dash/pages.tsx +0 -239
- package/src/routes/dash/posts.tsx +0 -334
- package/src/routes/dash/redirects.tsx +0 -257
- package/src/routes/pages/post.tsx +0 -59
- package/src/services/__tests__/page.test.ts +0 -298
- package/src/services/__tests__/path-registry.test.ts +0 -165
- package/src/services/__tests__/redirect.test.ts +0 -159
- package/src/services/page.ts +0 -203
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/types/sortablejs.d.ts +0 -29
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
- package/src/ui/components/compose-types.ts +0 -75
- package/src/ui/components/jant-collection-form.ts +0 -512
- package/src/ui/components/jant-compose-dialog.ts +0 -495
- package/src/ui/components/jant-compose-editor.ts +0 -814
- package/src/ui/dash/PageForm.tsx +0 -185
- package/src/ui/dash/PostList.tsx +0 -95
- package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
- package/src/ui/dash/collections/CollectionForm.tsx +0 -166
- package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
- package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
- package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
- package/src/ui/dash/media/MediaListContent.tsx +0 -201
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -74
- package/src/ui/dash/posts/PostForm.tsx +0 -248
- package/src/ui/dash/settings/SettingsNav.tsx +0 -52
- package/src/ui/layouts/DashLayout.tsx +0 -165
- package/src/ui/pages/SinglePage.tsx +0 -23
- package/src/ui/shared/ThreadView.tsx +0 -136
- /package/src/{ui → client}/components/settings-types.ts +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jant/core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.37",
|
|
4
4
|
"description": "A modern, open-source microblogging platform built on Cloudflare Workers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -23,19 +23,36 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@aws-sdk/client-s3": "^3.987.0",
|
|
26
|
+
"@emoji-mart/data": "^1.2.1",
|
|
26
27
|
"@lingui/core": "^5.9.0",
|
|
27
28
|
"@lingui/react": "^5.9.0",
|
|
28
29
|
"@tailwindcss/typography": "^0.5.19",
|
|
30
|
+
"@tiptap/core": "^3.20.0",
|
|
31
|
+
"@tiptap/extension-image": "^3.20.0",
|
|
32
|
+
"@tiptap/extension-placeholder": "^3.20.0",
|
|
33
|
+
"@tiptap/extension-table": "^3.20.0",
|
|
34
|
+
"@tiptap/pm": "^3.20.0",
|
|
35
|
+
"@tiptap/starter-kit": "^3.20.0",
|
|
36
|
+
"@tiptap/suggestion": "^3.20.0",
|
|
29
37
|
"basecoat-css": "^0.3.10",
|
|
30
38
|
"better-auth": "^1.4.18",
|
|
39
|
+
"blurhash": "^2.0.5",
|
|
31
40
|
"drizzle-orm": "^0.45.1",
|
|
41
|
+
"emoji-mart": "^5.6.0",
|
|
42
|
+
"fflate": "^0.8.2",
|
|
43
|
+
"fractional-indexing": "^3.2.0",
|
|
44
|
+
"heic-to": "^1.4.2",
|
|
45
|
+
"limax": "^4.2.2",
|
|
32
46
|
"lit": "^3.3.2",
|
|
33
47
|
"lucide-static": "^0.574.0",
|
|
34
48
|
"marked": "^17.0.1",
|
|
35
|
-
"
|
|
49
|
+
"mediabunny": "^1.35.1",
|
|
50
|
+
"nanoid": "^5.1.6",
|
|
51
|
+
"smol-toml": "^1.6.0",
|
|
36
52
|
"sortablejs": "^1.15.6",
|
|
37
|
-
"
|
|
53
|
+
"tiptap-markdown": "^0.9.0",
|
|
38
54
|
"uuidv7": "^1.1.0",
|
|
55
|
+
"yaml": "^2.8.2",
|
|
39
56
|
"zod": "^4.3.6"
|
|
40
57
|
},
|
|
41
58
|
"peerDependencies": {
|
|
@@ -50,13 +67,13 @@
|
|
|
50
67
|
"@lingui/format-po": "^5.9.0",
|
|
51
68
|
"@lingui/swc-plugin": "^5.10.1",
|
|
52
69
|
"@swc/core": "^1.15.11",
|
|
70
|
+
"@tailwindcss/vite": "^4.1.18",
|
|
53
71
|
"@types/better-sqlite3": "^7.6.13",
|
|
54
72
|
"@types/node": "^25.1.0",
|
|
55
73
|
"better-sqlite3": "^12.6.2",
|
|
56
74
|
"drizzle-kit": "^0.31.8",
|
|
57
75
|
"glob": "^13.0.0",
|
|
58
76
|
"happy-dom": "^20.6.3",
|
|
59
|
-
"@tailwindcss/vite": "^4.1.18",
|
|
60
77
|
"tailwindcss": "^4.1.18",
|
|
61
78
|
"tsx": "^4.21.0",
|
|
62
79
|
"typescript": "^5.9.3",
|
|
@@ -94,11 +111,11 @@
|
|
|
94
111
|
"build:lib": "vite build --config vite.config.worker.ts",
|
|
95
112
|
"typecheck": "tsc -b --noEmit",
|
|
96
113
|
"db:generate": "drizzle-kit generate",
|
|
97
|
-
"i18n:extract": "lingui extract",
|
|
114
|
+
"i18n:extract": "lingui extract --clean",
|
|
98
115
|
"i18n:compile": "lingui compile --typescript",
|
|
99
116
|
"i18n:build": "pnpm i18n:extract && pnpm i18n:compile",
|
|
100
117
|
"dev": "pnpm db:migrate:local && vite dev",
|
|
101
|
-
"dev:debug": "pnpm db:migrate:local && vite dev --port
|
|
118
|
+
"dev:debug": "pnpm db:migrate:local && vite dev --port 19020",
|
|
102
119
|
"db:migrate:local": "echo y | wrangler d1 migrations apply DB --local",
|
|
103
120
|
"db:migrate:remote": "wrangler d1 migrations apply DB --remote",
|
|
104
121
|
"test": "vitest run",
|
|
@@ -9,15 +9,15 @@ import type { Bindings } from "../../types.js";
|
|
|
9
9
|
import type { AppVariables } from "../../types/app-context.js";
|
|
10
10
|
import { createTestDatabase } from "./db.js";
|
|
11
11
|
import { createPostService } from "../../services/post.js";
|
|
12
|
-
import {
|
|
12
|
+
import { createPathService } from "../../services/path.js";
|
|
13
13
|
import { createSettingsService } from "../../services/settings.js";
|
|
14
|
-
import {
|
|
14
|
+
import { createCustomUrlService } from "../../services/custom-url.js";
|
|
15
15
|
import { createMediaService } from "../../services/media.js";
|
|
16
16
|
import { createCollectionService } from "../../services/collection.js";
|
|
17
17
|
import { createSearchService } from "../../services/search.js";
|
|
18
18
|
import { createNavItemService } from "../../services/navigation.js";
|
|
19
19
|
import { createAuthService } from "../../services/auth.js";
|
|
20
|
-
import {
|
|
20
|
+
import { createApiTokenService } from "../../services/api-token.js";
|
|
21
21
|
import type { Database } from "../../db/index.js";
|
|
22
22
|
import type BetterSqlite3 from "better-sqlite3";
|
|
23
23
|
import { errorHandler } from "../../middleware/error-handler.js";
|
|
@@ -46,18 +46,18 @@ export function createTestApp(options: TestAppOptions = {}) {
|
|
|
46
46
|
const mockD1 = createMockD1(sqlite);
|
|
47
47
|
|
|
48
48
|
const settingsService = createSettingsService(db);
|
|
49
|
-
const
|
|
49
|
+
const pathService = createPathService(db);
|
|
50
50
|
const services = {
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
paths: pathService,
|
|
52
|
+
posts: createPostService(db, { slugIdLength: 5 }, pathService),
|
|
53
53
|
settings: settingsService,
|
|
54
|
-
|
|
55
|
-
redirects: createRedirectService(db, pathRegistryService),
|
|
54
|
+
customUrls: createCustomUrlService(db, pathService),
|
|
56
55
|
media: createMediaService(db),
|
|
57
|
-
collections: createCollectionService(db),
|
|
56
|
+
collections: createCollectionService(db, pathService),
|
|
58
57
|
search: createSearchService(mockD1),
|
|
59
58
|
navItems: createNavItemService(db),
|
|
60
59
|
auth: createAuthService(db, settingsService),
|
|
60
|
+
apiTokens: createApiTokenService(db),
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
const app = new Hono<Env>();
|
|
@@ -69,7 +69,7 @@ export function createTestApp(options: TestAppOptions = {}) {
|
|
|
69
69
|
app.use("*", async (c, next) => {
|
|
70
70
|
// Provide mock env bindings so c.env.* works in route handlers
|
|
71
71
|
c.env = {
|
|
72
|
-
SITE_URL: "http://localhost:
|
|
72
|
+
SITE_URL: "http://localhost:9020",
|
|
73
73
|
} as AppVariables["services"] extends never ? never : Bindings;
|
|
74
74
|
|
|
75
75
|
c.set("services", services as AppVariables["services"]);
|
|
@@ -1,140 +1,144 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Test Database Helper
|
|
3
3
|
*
|
|
4
|
-
* Creates an in-memory SQLite database with all migrations applied
|
|
4
|
+
* Creates an in-memory SQLite database with all migrations applied.
|
|
5
5
|
* Used for service integration tests.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import Database from "better-sqlite3";
|
|
9
9
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
10
10
|
import * as schema from "../../db/schema.js";
|
|
11
|
-
import { readFileSync } from "fs";
|
|
11
|
+
import { readFileSync, readdirSync } from "fs";
|
|
12
12
|
import { resolve } from "path";
|
|
13
13
|
|
|
14
14
|
const MIGRATIONS_DIR = resolve(import.meta.dirname, "../../db/migrations");
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Applies a migration file, splitting on Drizzle statement breakpoints.
|
|
18
|
+
* When `skipFts` is true, silently skips statements that reference the
|
|
19
|
+
* FTS virtual table (triggers, rebuild) since it may not exist.
|
|
18
20
|
*/
|
|
19
|
-
function applyMigration(
|
|
21
|
+
function applyMigration(
|
|
22
|
+
sqlite: Database.Database,
|
|
23
|
+
filename: string,
|
|
24
|
+
options?: { skipFts?: boolean },
|
|
25
|
+
) {
|
|
20
26
|
const migration = readFileSync(resolve(MIGRATIONS_DIR, filename), "utf-8");
|
|
21
27
|
for (const sql of migration.split("--> statement-breakpoint")) {
|
|
22
28
|
const trimmed = sql.trim();
|
|
23
|
-
if (trimmed)
|
|
29
|
+
if (!trimmed) continue;
|
|
30
|
+
if (options?.skipFts && trimmed.includes("post_fts")) continue;
|
|
31
|
+
sqlite.exec(trimmed);
|
|
24
32
|
}
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* @param options.fts - Whether to enable FTS5 for search tests (default: false).
|
|
32
|
-
* The trigram tokenizer used in production may not be available in all
|
|
33
|
-
* better-sqlite3 builds, so FTS is opt-in for tests that need it.
|
|
36
|
+
* Applies the FTS migration with fallback for environments lacking
|
|
37
|
+
* the trigram tokenizer.
|
|
34
38
|
*/
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
// Enable WAL mode for better performance
|
|
39
|
-
sqlite.pragma("journal_mode = WAL");
|
|
40
|
-
sqlite.pragma("foreign_keys = ON");
|
|
41
|
-
|
|
42
|
-
// Apply v1 base migrations (0000-0004)
|
|
43
|
-
applyMigration(sqlite, "0000_square_wallflower.sql");
|
|
44
|
-
// Skip 0001 (FTS) — v2 migration will create updated FTS if needed
|
|
45
|
-
applyMigration(sqlite, "0002_add_media_attachments.sql");
|
|
46
|
-
applyMigration(sqlite, "0003_add_navigation_links.sql");
|
|
47
|
-
applyMigration(sqlite, "0004_add_storage_provider.sql");
|
|
48
|
-
|
|
49
|
-
// Apply v2 schema migration (0005)
|
|
50
|
-
// Split FTS-related statements so we can handle them separately
|
|
51
|
-
const v2Migration = readFileSync(
|
|
52
|
-
resolve(MIGRATIONS_DIR, "0005_v2_schema_migration.sql"),
|
|
53
|
-
"utf-8",
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
for (const stmt of v2Migration.split("--> statement-breakpoint")) {
|
|
39
|
+
function applyFtsMigration(sqlite: Database.Database, filename: string) {
|
|
40
|
+
const ftsSql = readFileSync(resolve(MIGRATIONS_DIR, filename), "utf-8");
|
|
41
|
+
for (const stmt of ftsSql.split("--> statement-breakpoint")) {
|
|
57
42
|
const trimmed = stmt.trim();
|
|
58
43
|
if (!trimmed) continue;
|
|
59
|
-
|
|
60
|
-
// Skip FTS-related statements if FTS not requested
|
|
61
|
-
const isFts = trimmed.includes("posts_fts");
|
|
62
|
-
if (!options?.fts && isFts) continue;
|
|
63
|
-
|
|
64
44
|
try {
|
|
65
45
|
sqlite.exec(trimmed);
|
|
66
46
|
} catch {
|
|
67
|
-
//
|
|
68
|
-
if (
|
|
47
|
+
// Trigram tokenizer may not be available — fall back to default tokenizer
|
|
48
|
+
if (trimmed.includes("CREATE VIRTUAL TABLE")) {
|
|
69
49
|
sqlite.exec(`
|
|
70
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS
|
|
50
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS post_fts USING fts5(
|
|
71
51
|
title,
|
|
72
|
-
|
|
52
|
+
body_text,
|
|
73
53
|
quote_text,
|
|
74
|
-
|
|
75
|
-
|
|
54
|
+
url,
|
|
55
|
+
content='post',
|
|
56
|
+
content_rowid='rowid'
|
|
76
57
|
);
|
|
77
58
|
`);
|
|
78
59
|
}
|
|
79
|
-
// Ignore
|
|
80
|
-
else if (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
throw new Error(`Migration statement failed: ${trimmed.slice(0, 100)}`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Apply 0006: rename slug to path on posts
|
|
90
|
-
applyMigration(sqlite, "0006_rename_slug_to_path.sql");
|
|
91
|
-
|
|
92
|
-
// Apply 0007: post_collections M:N junction table
|
|
93
|
-
const m7 = readFileSync(
|
|
94
|
-
resolve(MIGRATIONS_DIR, "0007_post_collections_m2m.sql"),
|
|
95
|
-
"utf-8",
|
|
96
|
-
);
|
|
97
|
-
for (const stmt of m7.split("--> statement-breakpoint")) {
|
|
98
|
-
const trimmed = stmt.trim();
|
|
99
|
-
if (!trimmed) continue;
|
|
100
|
-
// Skip FTS trigger statements if FTS not requested
|
|
101
|
-
const isFts = trimmed.includes("posts_fts");
|
|
102
|
-
if (!options?.fts && isFts) continue;
|
|
103
|
-
try {
|
|
104
|
-
sqlite.exec(trimmed);
|
|
105
|
-
} catch {
|
|
106
|
-
// Ignore DROP TRIGGER failures silently
|
|
107
|
-
if (!trimmed.startsWith("DROP TRIGGER")) {
|
|
108
|
-
throw new Error(`Migration 0007 failed: ${trimmed.slice(0, 100)}`);
|
|
60
|
+
// Ignore trigger failures if virtual table creation failed
|
|
61
|
+
else if (!trimmed.startsWith("CREATE TRIGGER")) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`FTS migration statement failed: ${trimmed.slice(0, 100)}`,
|
|
64
|
+
);
|
|
109
65
|
}
|
|
110
66
|
}
|
|
111
67
|
}
|
|
112
68
|
|
|
113
|
-
//
|
|
114
|
-
|
|
69
|
+
// If trigram fallback was used, triggers need to be created without trigram
|
|
70
|
+
// Re-create triggers unconditionally (IF NOT EXISTS handles idempotency)
|
|
71
|
+
sqlite.exec(`
|
|
72
|
+
CREATE TRIGGER IF NOT EXISTS post_ai AFTER INSERT ON post BEGIN
|
|
73
|
+
INSERT INTO post_fts(rowid, title, body_text, quote_text, url)
|
|
74
|
+
VALUES (new.rowid, new.title, new.body_text, new.quote_text, new.url);
|
|
75
|
+
END;
|
|
76
|
+
CREATE TRIGGER IF NOT EXISTS post_ad AFTER DELETE ON post BEGIN
|
|
77
|
+
INSERT INTO post_fts(post_fts, rowid, title, body_text, quote_text, url)
|
|
78
|
+
VALUES ('delete', old.rowid, old.title, old.body_text, old.quote_text, old.url);
|
|
79
|
+
END;
|
|
80
|
+
CREATE TRIGGER IF NOT EXISTS post_au AFTER UPDATE ON post BEGIN
|
|
81
|
+
INSERT INTO post_fts(post_fts, rowid, title, body_text, quote_text, url)
|
|
82
|
+
VALUES ('delete', old.rowid, old.title, old.body_text, old.quote_text, old.url);
|
|
83
|
+
INSERT INTO post_fts(rowid, title, body_text, quote_text, url)
|
|
84
|
+
VALUES (new.rowid, new.title, new.body_text, new.quote_text, new.url);
|
|
85
|
+
END;
|
|
86
|
+
`);
|
|
87
|
+
}
|
|
115
88
|
|
|
116
|
-
|
|
117
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Creates a fresh in-memory SQLite database with all migrations applied.
|
|
91
|
+
* Each call returns an isolated database instance for test isolation.
|
|
92
|
+
*
|
|
93
|
+
* @param options.fts - Whether to enable FTS5 for search tests (default: false).
|
|
94
|
+
* The trigram tokenizer used in production may not be available in all
|
|
95
|
+
* better-sqlite3 builds, so FTS is opt-in for tests that need it.
|
|
96
|
+
*/
|
|
97
|
+
export function createTestDatabase(options?: { fts?: boolean }) {
|
|
98
|
+
const sqlite = new Database(":memory:");
|
|
118
99
|
|
|
119
|
-
//
|
|
120
|
-
|
|
100
|
+
// Enable WAL mode for better performance
|
|
101
|
+
sqlite.pragma("journal_mode = WAL");
|
|
102
|
+
sqlite.pragma("foreign_keys = ON");
|
|
121
103
|
|
|
122
|
-
// Apply
|
|
123
|
-
|
|
104
|
+
// Apply all migrations in order (sorted by filename prefix: 0000_, 0001_, …)
|
|
105
|
+
// FTS migration (0001_*) is only applied when requested because the trigram
|
|
106
|
+
// tokenizer may not be available in all better-sqlite3 builds.
|
|
107
|
+
const allFiles = readdirSync(MIGRATIONS_DIR)
|
|
108
|
+
.filter((f) => f.endsWith(".sql"))
|
|
109
|
+
.sort();
|
|
110
|
+
|
|
111
|
+
for (const file of allFiles) {
|
|
112
|
+
const isFts = file.startsWith("0001_");
|
|
113
|
+
if (isFts && !options?.fts) continue;
|
|
114
|
+
|
|
115
|
+
if (isFts) {
|
|
116
|
+
applyFtsMigration(sqlite, file);
|
|
117
|
+
} else {
|
|
118
|
+
applyMigration(sqlite, file, { skipFts: !options?.fts });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
124
121
|
|
|
125
122
|
const db = drizzle(sqlite, { schema });
|
|
126
123
|
|
|
127
124
|
// Polyfill D1 batch() for test compatibility.
|
|
128
125
|
// In production, D1 batch executes statements atomically in a single transaction.
|
|
129
|
-
// In tests,
|
|
130
|
-
//
|
|
126
|
+
// In tests, wrap sequential execution in an explicit transaction so rollback
|
|
127
|
+
// behavior matches D1's all-or-nothing semantics.
|
|
131
128
|
Object.defineProperty(db, "batch", {
|
|
132
129
|
value: async (queries: PromiseLike<unknown>[]) => {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
results
|
|
130
|
+
sqlite.exec("BEGIN");
|
|
131
|
+
try {
|
|
132
|
+
const results = [];
|
|
133
|
+
for (const q of queries) {
|
|
134
|
+
results.push(await q);
|
|
135
|
+
}
|
|
136
|
+
sqlite.exec("COMMIT");
|
|
137
|
+
return results;
|
|
138
|
+
} catch (err) {
|
|
139
|
+
sqlite.exec("ROLLBACK");
|
|
140
|
+
throw err;
|
|
136
141
|
}
|
|
137
|
-
return results;
|
|
138
142
|
},
|
|
139
143
|
});
|
|
140
144
|
|
package/src/app.tsx
CHANGED
|
@@ -16,7 +16,6 @@ import { resetRoutes } from "./routes/auth/reset.js";
|
|
|
16
16
|
|
|
17
17
|
// Routes - Pages
|
|
18
18
|
import { homeRoutes } from "./routes/pages/home.js";
|
|
19
|
-
import { postRoutes } from "./routes/pages/post.js";
|
|
20
19
|
import { pageRoutes } from "./routes/pages/page.js";
|
|
21
20
|
import { collectionRoutes } from "./routes/pages/collection.js";
|
|
22
21
|
import { archiveRoutes } from "./routes/pages/archive.js";
|
|
@@ -24,25 +23,22 @@ import { searchRoutes } from "./routes/pages/search.js";
|
|
|
24
23
|
import { featuredRoutes } from "./routes/pages/featured.js";
|
|
25
24
|
import { latestRoutes } from "./routes/pages/latest.js";
|
|
26
25
|
import { collectionsPageRoutes } from "./routes/pages/collections.js";
|
|
26
|
+
import { newPostRoutes } from "./routes/pages/new.js";
|
|
27
27
|
|
|
28
|
-
// Routes -
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import { pagesRoutes as dashPagesRoutes } from "./routes/dash/pages.js";
|
|
32
|
-
import { mediaRoutes as dashMediaRoutes } from "./routes/dash/media.js";
|
|
33
|
-
import { settingsRoutes as dashSettingsRoutes } from "./routes/dash/settings.js";
|
|
34
|
-
import { redirectsRoutes as dashRedirectsRoutes } from "./routes/dash/redirects.js";
|
|
35
|
-
import { collectionsRoutes as dashCollectionsRoutes } from "./routes/dash/collections.js";
|
|
36
|
-
import { appearanceRoutes as dashAppearanceRoutes } from "./routes/dash/appearance.js";
|
|
28
|
+
// Routes - Settings (admin)
|
|
29
|
+
import { settingsRoutes } from "./routes/dash/settings.js";
|
|
30
|
+
import { customUrlsRoutes } from "./routes/dash/custom-urls.js";
|
|
37
31
|
|
|
38
32
|
// Routes - API
|
|
39
33
|
import { postsApiRoutes } from "./routes/api/posts.js";
|
|
40
|
-
import { pagesApiRoutes } from "./routes/api/pages.js";
|
|
41
34
|
import { navItemsApiRoutes } from "./routes/api/nav-items.js";
|
|
42
35
|
import { collectionsApiRoutes } from "./routes/api/collections.js";
|
|
43
36
|
import { settingsApiRoutes } from "./routes/api/settings.js";
|
|
44
37
|
import { uploadApiRoutes } from "./routes/api/upload.js";
|
|
38
|
+
import { multipartUploadApiRoutes } from "./routes/api/upload-multipart.js";
|
|
45
39
|
import { searchApiRoutes } from "./routes/api/search.js";
|
|
40
|
+
import { customUrlsApiRoutes } from "./routes/api/custom-urls.js";
|
|
41
|
+
import { exportApiRoutes } from "./routes/api/export.js";
|
|
46
42
|
// Routes - Compose
|
|
47
43
|
import { composeRoutes } from "./routes/compose.js";
|
|
48
44
|
|
|
@@ -51,10 +47,11 @@ import { rssRoutes } from "./routes/feed/rss.js";
|
|
|
51
47
|
import { sitemapRoutes } from "./routes/feed/sitemap.js";
|
|
52
48
|
|
|
53
49
|
// Middleware
|
|
54
|
-
import { requireAuth } from "./middleware/auth.js";
|
|
50
|
+
import { requireAuth, isLocalHostname } from "./middleware/auth.js";
|
|
55
51
|
import { requireOnboarding } from "./middleware/onboarding.js";
|
|
56
52
|
import { errorHandler } from "./middleware/error-handler.js";
|
|
57
53
|
import { withConfig } from "./middleware/config.js";
|
|
54
|
+
import { secureHeadersMiddleware } from "./middleware/secure-headers.js";
|
|
58
55
|
|
|
59
56
|
import { createStorageDriver } from "./lib/storage.js";
|
|
60
57
|
import { base64ToUint8Array } from "./lib/favicon.js";
|
|
@@ -82,6 +79,31 @@ export function createApp(): App {
|
|
|
82
79
|
|
|
83
80
|
// Lightweight init — no DB queries
|
|
84
81
|
app.use("*", async (c, next) => {
|
|
82
|
+
// Fail fast: DEV_API_TOKEN must never be reachable from a non-local hostname
|
|
83
|
+
if (c.env.DEV_API_TOKEN) {
|
|
84
|
+
const hostname = new URL(c.req.url).hostname;
|
|
85
|
+
if (!isLocalHostname(hostname)) {
|
|
86
|
+
return c.html(
|
|
87
|
+
`<!DOCTYPE html>
|
|
88
|
+
<html lang="en">
|
|
89
|
+
<head>
|
|
90
|
+
<meta charset="utf-8">
|
|
91
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
92
|
+
<title>Configuration Error</title>
|
|
93
|
+
<style>body{font-family:system-ui,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#fafafa;color:#111}div{max-width:480px;text-align:center}h1{font-size:1.25rem;font-weight:600}p{color:#666;line-height:1.6}code{background:#eee;padding:2px 6px;border-radius:4px;font-size:.9em}</style>
|
|
94
|
+
</head>
|
|
95
|
+
<body>
|
|
96
|
+
<div>
|
|
97
|
+
<h1>DEV_API_TOKEN must not be set in production</h1>
|
|
98
|
+
<p>Remove <code>DEV_API_TOKEN</code> from your environment variables. This token is only for local development.</p>
|
|
99
|
+
</div>
|
|
100
|
+
</body>
|
|
101
|
+
</html>`,
|
|
102
|
+
500,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
85
107
|
if (!c.env.AUTH_SECRET) {
|
|
86
108
|
return c.html(
|
|
87
109
|
`<!DOCTYPE html>
|
|
@@ -109,7 +131,11 @@ export function createApp(): App {
|
|
|
109
131
|
// Note: Drizzle ORM doesn't officially support D1DatabaseSession yet (issue #2226)
|
|
110
132
|
// but it works at runtime. We use type assertion as a temporary workaround.
|
|
111
133
|
const db = createDatabase(session as unknown as D1Database);
|
|
112
|
-
c.
|
|
134
|
+
const slugIdLength = parseInt(c.env.SLUG_ID_LENGTH ?? "5", 10) || 5;
|
|
135
|
+
c.set(
|
|
136
|
+
"services",
|
|
137
|
+
createServices(db, session as unknown as D1Database, { slugIdLength }),
|
|
138
|
+
);
|
|
113
139
|
c.set("storage", createStorageDriver(c.env));
|
|
114
140
|
|
|
115
141
|
const baseURL = c.env.SITE_URL || new URL(c.req.url).origin;
|
|
@@ -126,20 +152,118 @@ export function createApp(): App {
|
|
|
126
152
|
await next();
|
|
127
153
|
});
|
|
128
154
|
|
|
155
|
+
// Security headers (CSP, X-Frame-Options, etc.)
|
|
156
|
+
app.use("*", secureHeadersMiddleware());
|
|
157
|
+
|
|
129
158
|
// --- Routes that don't need config/theme ---
|
|
130
159
|
|
|
131
160
|
// Health check
|
|
132
161
|
app.get("/health", (c) => c.json({ status: "ok" }));
|
|
133
162
|
|
|
163
|
+
// Fetch text media content by ID (same-origin proxy to avoid CORS with CDN URLs)
|
|
164
|
+
app.get("/api/media/:id/content", async (c) => {
|
|
165
|
+
const media = await c.var.services.media.getById(c.req.param("id"));
|
|
166
|
+
if (!media) return c.notFound();
|
|
167
|
+
|
|
168
|
+
const storage = c.var.storage;
|
|
169
|
+
if (!storage) return c.notFound();
|
|
170
|
+
|
|
171
|
+
const object = await storage.get(media.storageKey);
|
|
172
|
+
if (!object) return c.notFound();
|
|
173
|
+
|
|
174
|
+
const headers = new Headers();
|
|
175
|
+
headers.set(
|
|
176
|
+
"Content-Type",
|
|
177
|
+
object.contentType || "application/octet-stream",
|
|
178
|
+
);
|
|
179
|
+
// Use updatedAt as ETag so browsers can cache but revalidate on change
|
|
180
|
+
const etag = `"${media.updatedAt}"`;
|
|
181
|
+
headers.set("Cache-Control", "public, no-cache");
|
|
182
|
+
headers.set("ETag", etag);
|
|
183
|
+
|
|
184
|
+
if (c.req.header("If-None-Match") === etag) {
|
|
185
|
+
return new Response(null, { status: 304, headers });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return new Response(object.body, { headers });
|
|
189
|
+
});
|
|
190
|
+
|
|
134
191
|
// Media files from storage (path matches storage key: media/YYYY/MM/uuid.ext)
|
|
192
|
+
// Supports HTTP Range requests for seekable audio/video playback.
|
|
135
193
|
app.get("/media/*", async (c) => {
|
|
136
194
|
const storage = c.var.storage;
|
|
137
195
|
if (!storage) {
|
|
138
196
|
return c.notFound();
|
|
139
197
|
}
|
|
140
198
|
|
|
141
|
-
// The storage key is the full path without the leading "/"
|
|
142
199
|
const storageKey = c.req.path.slice(1);
|
|
200
|
+
if (storageKey.includes("..") || !storageKey.startsWith("media/")) {
|
|
201
|
+
return c.notFound();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const rangeHeader = c.req.header("Range");
|
|
205
|
+
|
|
206
|
+
// First fetch without range to get the total size
|
|
207
|
+
if (rangeHeader) {
|
|
208
|
+
// Get total size via a full request first
|
|
209
|
+
const full = await storage.get(storageKey);
|
|
210
|
+
if (!full) return c.notFound();
|
|
211
|
+
|
|
212
|
+
const totalSize = full.size;
|
|
213
|
+
if (!totalSize) {
|
|
214
|
+
// Driver doesn't report size — fall back to full response
|
|
215
|
+
const headers = new Headers();
|
|
216
|
+
headers.set(
|
|
217
|
+
"Content-Type",
|
|
218
|
+
full.contentType || "application/octet-stream",
|
|
219
|
+
);
|
|
220
|
+
headers.set("Cache-Control", "public, max-age=31536000, immutable");
|
|
221
|
+
return new Response(full.body, { headers });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Cancel the full stream — we'll re-fetch with the range
|
|
225
|
+
await full.body.cancel();
|
|
226
|
+
|
|
227
|
+
// Parse "bytes=START-END" (END is optional)
|
|
228
|
+
const match = /^bytes=(\d+)-(\d*)$/.exec(rangeHeader);
|
|
229
|
+
if (!match) {
|
|
230
|
+
return new Response("Invalid Range", {
|
|
231
|
+
status: 416,
|
|
232
|
+
headers: { "Content-Range": `bytes */${totalSize}` },
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const start = parseInt(match[1] ?? "0", 10);
|
|
237
|
+
const end = match[2]
|
|
238
|
+
? Math.min(parseInt(match[2], 10), totalSize - 1)
|
|
239
|
+
: totalSize - 1;
|
|
240
|
+
|
|
241
|
+
if (start > end || start >= totalSize) {
|
|
242
|
+
return new Response("Range Not Satisfiable", {
|
|
243
|
+
status: 416,
|
|
244
|
+
headers: { "Content-Range": `bytes */${totalSize}` },
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const rangeObj = await storage.get(storageKey, {
|
|
249
|
+
range: { offset: start, length: end - start + 1 },
|
|
250
|
+
});
|
|
251
|
+
if (!rangeObj) return c.notFound();
|
|
252
|
+
|
|
253
|
+
const headers = new Headers();
|
|
254
|
+
headers.set(
|
|
255
|
+
"Content-Type",
|
|
256
|
+
rangeObj.contentType || "application/octet-stream",
|
|
257
|
+
);
|
|
258
|
+
headers.set("Cache-Control", "public, max-age=31536000, immutable");
|
|
259
|
+
headers.set("Accept-Ranges", "bytes");
|
|
260
|
+
headers.set("Content-Range", `bytes ${start}-${end}/${totalSize}`);
|
|
261
|
+
headers.set("Content-Length", String(end - start + 1));
|
|
262
|
+
|
|
263
|
+
return new Response(rangeObj.body, { status: 206, headers });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// No Range header — serve full file
|
|
143
267
|
const object = await storage.get(storageKey);
|
|
144
268
|
if (!object) {
|
|
145
269
|
return c.notFound();
|
|
@@ -151,6 +275,10 @@ export function createApp(): App {
|
|
|
151
275
|
object.contentType || "application/octet-stream",
|
|
152
276
|
);
|
|
153
277
|
headers.set("Cache-Control", "public, max-age=31536000, immutable");
|
|
278
|
+
headers.set("Accept-Ranges", "bytes");
|
|
279
|
+
if (object.size) {
|
|
280
|
+
headers.set("Content-Length", String(object.size));
|
|
281
|
+
}
|
|
154
282
|
|
|
155
283
|
return new Response(object.body, { headers });
|
|
156
284
|
});
|
|
@@ -206,7 +334,7 @@ export function createApp(): App {
|
|
|
206
334
|
await next();
|
|
207
335
|
});
|
|
208
336
|
|
|
209
|
-
// Redirect middleware
|
|
337
|
+
// Redirect middleware — only handles redirect-type custom URLs
|
|
210
338
|
app.use("*", async (c, next) => {
|
|
211
339
|
const path = new URL(c.req.url).pathname;
|
|
212
340
|
// Skip redirect check for API routes and static assets
|
|
@@ -214,9 +342,9 @@ export function createApp(): App {
|
|
|
214
342
|
return next();
|
|
215
343
|
}
|
|
216
344
|
|
|
217
|
-
const
|
|
218
|
-
if (redirect) {
|
|
219
|
-
return c.redirect(
|
|
345
|
+
const customUrl = await c.var.services.customUrls.getByPath(path.slice(1));
|
|
346
|
+
if (customUrl?.targetType === "redirect" && customUrl.toPath) {
|
|
347
|
+
return c.redirect(customUrl.toPath, customUrl.redirectType ?? 301);
|
|
220
348
|
}
|
|
221
349
|
|
|
222
350
|
await next();
|
|
@@ -230,27 +358,25 @@ export function createApp(): App {
|
|
|
230
358
|
|
|
231
359
|
// API Routes
|
|
232
360
|
app.route("/api/posts", postsApiRoutes);
|
|
233
|
-
app.route("/api/pages", pagesApiRoutes);
|
|
234
361
|
app.route("/api/nav-items", navItemsApiRoutes);
|
|
235
362
|
app.route("/api/collections", collectionsApiRoutes);
|
|
236
363
|
app.route("/api/settings", settingsApiRoutes);
|
|
364
|
+
app.route("/api/custom-urls", customUrlsApiRoutes);
|
|
365
|
+
app.route("/api/export", exportApiRoutes);
|
|
237
366
|
|
|
238
367
|
// Auth routes
|
|
239
368
|
app.route("/", setupRoutes);
|
|
240
369
|
app.route("/", signinRoutes);
|
|
241
370
|
app.route("/", resetRoutes);
|
|
242
371
|
|
|
243
|
-
//
|
|
244
|
-
app.use("/
|
|
245
|
-
app.
|
|
246
|
-
app.route("/
|
|
247
|
-
app.route("/
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
app.route("/
|
|
251
|
-
app.route("/dash/settings/redirects", dashRedirectsRoutes);
|
|
252
|
-
app.route("/dash/collections", dashCollectionsRoutes);
|
|
253
|
-
// API routes
|
|
372
|
+
// Settings routes (protected)
|
|
373
|
+
app.use("/settings/*", requireAuth());
|
|
374
|
+
app.use("/settings", requireAuth());
|
|
375
|
+
app.route("/settings/custom-urls", customUrlsRoutes);
|
|
376
|
+
app.route("/settings", settingsRoutes);
|
|
377
|
+
|
|
378
|
+
// Protected API routes (multipart must be registered before base upload)
|
|
379
|
+
app.route("/api/upload/multipart", multipartUploadApiRoutes);
|
|
254
380
|
app.route("/api/upload", uploadApiRoutes);
|
|
255
381
|
app.route("/api/search", searchApiRoutes);
|
|
256
382
|
|
|
@@ -263,12 +389,12 @@ export function createApp(): App {
|
|
|
263
389
|
|
|
264
390
|
// Frontend routes
|
|
265
391
|
app.route("/search", searchRoutes);
|
|
392
|
+
app.route("/", newPostRoutes);
|
|
266
393
|
app.route("/archive", archiveRoutes);
|
|
267
394
|
app.route("/featured", featuredRoutes);
|
|
268
395
|
app.route("/latest", latestRoutes);
|
|
269
396
|
app.route("/c", collectionsPageRoutes);
|
|
270
397
|
app.route("/c", collectionRoutes);
|
|
271
|
-
app.route("/p", postRoutes);
|
|
272
398
|
app.route("/", homeRoutes);
|
|
273
399
|
|
|
274
400
|
// Custom page catch-all (must be last)
|