@jant/core 0.3.36 → 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/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4012 -3276
- package/dist/index.js +10285 -5809
- package/package.json +11 -3
- package/src/__tests__/helpers/app.ts +9 -9
- package/src/__tests__/helpers/db.ts +91 -93
- package/src/app.tsx +157 -27
- 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/client/avatar-upload.ts +3 -2
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +645 -49
- package/src/client/components/__tests__/jant-compose-editor.test.ts +208 -16
- package/src/client/components/__tests__/jant-post-form.test.ts +19 -9
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +7 -9
- package/src/client/components/compose-types.ts +101 -4
- package/src/client/components/jant-collection-form.ts +43 -7
- package/src/client/components/jant-collection-sidebar.ts +88 -84
- package/src/client/components/jant-compose-dialog.ts +1655 -219
- package/src/client/components/jant-compose-editor.ts +732 -168
- package/src/client/components/jant-compose-fullscreen.ts +23 -78
- package/src/client/components/jant-media-lightbox.ts +2 -0
- package/src/client/components/jant-nav-manager.ts +24 -284
- package/src/client/components/jant-post-form.ts +89 -9
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/client/components/jant-settings-avatar.ts +3 -3
- package/src/client/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/client/components/nav-manager-types.ts +4 -19
- package/src/client/components/post-form-template.ts +107 -12
- package/src/client/components/post-form-types.ts +11 -4
- package/src/client/compose-bridge.ts +410 -109
- package/src/client/image-processor.ts +26 -8
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/client/post-form-bridge.ts +52 -1
- package/src/client/settings-bridge.ts +0 -12
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/create-editor.ts +46 -0
- package/src/client/tiptap/extensions.ts +5 -0
- package/src/client/tiptap/image-node.ts +2 -8
- package/src/client/tiptap/paste-image.ts +2 -13
- package/src/client/tiptap/slash-commands.ts +173 -63
- package/src/client/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +15 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +5 -2
- 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 -145
- 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 +487 -1217
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +613 -996
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +624 -1007
- 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__/schemas.test.ts +181 -63
- 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__/view.test.ts +141 -66
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +885 -68
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +78 -32
- 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 +12 -3
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +20 -2
- package/src/lib/resolve-config.ts +6 -2
- package/src/lib/schemas.ts +224 -55
- 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 +66 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +74 -34
- package/src/lib/tiptap-render.ts +5 -10
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +190 -29
- package/src/lib/url.ts +31 -0
- package/src/lib/view.ts +204 -37
- 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/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +45 -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 +51 -42
- 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 +43 -39
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +85 -19
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/setup.tsx +26 -33
- package/src/routes/auth/signin.tsx +3 -7
- package/src/routes/compose.tsx +10 -55
- package/src/routes/dash/__tests__/settings-avatar.test.ts +1 -1
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +304 -232
- package/src/routes/feed/__tests__/rss.test.ts +27 -28
- package/src/routes/feed/rss.ts +6 -4
- 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 +41 -22
- package/src/routes/pages/archive.tsx +175 -39
- package/src/routes/pages/collection.tsx +22 -10
- package/src/routes/pages/collections.tsx +3 -3
- package/src/routes/pages/featured.tsx +28 -4
- package/src/routes/pages/home.tsx +16 -15
- 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 +713 -234
- package/src/services/__tests__/search.test.ts +67 -10
- 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 +687 -154
- package/src/services/search.ts +160 -75
- package/src/styles/components.css +58 -7
- package/src/styles/tokens.css +84 -6
- package/src/styles/ui.css +2964 -457
- package/src/types/bindings.ts +4 -1
- package/src/types/config.ts +12 -4
- package/src/types/constants.ts +15 -3
- package/src/types/entities.ts +74 -35
- package/src/types/operations.ts +11 -24
- package/src/types/props.ts +51 -16
- package/src/types/views.ts +45 -22
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +239 -17
- 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 +3 -1
- package/src/ui/dash/appearance/AdvancedContent.tsx +22 -1
- package/src/ui/dash/appearance/ColorThemeContent.tsx +22 -2
- package/src/ui/dash/appearance/FontThemeContent.tsx +1 -1
- package/src/ui/dash/appearance/NavigationContent.tsx +5 -45
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +3 -57
- 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 +8 -0
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +45 -15
- 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 +105 -92
- package/src/ui/pages/ArchivePage.tsx +923 -98
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +181 -37
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +47 -37
- package/src/ui/shared/MediaGallery.tsx +445 -149
- 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/dist/client/assets/url-8Dj-5CLW.js +0 -1
- package/src/client/media-upload.ts +0 -161
- package/src/client/page-slug-bridge.ts +0 -42
- 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/0012_add_tiptap_columns.sql +0 -2
- package/src/db/migrations/0013_replace_featured_with_visibility.sql +0 -8
- package/src/db/migrations/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- 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/index.tsx +0 -109
- package/src/routes/dash/media.tsx +0 -135
- package/src/routes/dash/pages.tsx +0 -245
- package/src/routes/dash/posts.tsx +0 -338
- package/src/routes/dash/redirects.tsx +0 -263
- 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 -216
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/ui/dash/PageForm.tsx +0 -187
- package/src/ui/dash/PostList.tsx +0 -95
- package/src/ui/dash/media/MediaListContent.tsx +0 -206
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -75
- package/src/ui/dash/posts/PostForm.tsx +0 -260
- package/src/ui/layouts/DashLayout.tsx +0 -247
- package/src/ui/pages/SinglePage.tsx +0 -23
- package/src/ui/shared/ThreadView.tsx +0 -136
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": {
|
|
@@ -36,15 +36,23 @@
|
|
|
36
36
|
"@tiptap/suggestion": "^3.20.0",
|
|
37
37
|
"basecoat-css": "^0.3.10",
|
|
38
38
|
"better-auth": "^1.4.18",
|
|
39
|
+
"blurhash": "^2.0.5",
|
|
39
40
|
"drizzle-orm": "^0.45.1",
|
|
40
41
|
"emoji-mart": "^5.6.0",
|
|
42
|
+
"fflate": "^0.8.2",
|
|
43
|
+
"fractional-indexing": "^3.2.0",
|
|
44
|
+
"heic-to": "^1.4.2",
|
|
41
45
|
"limax": "^4.2.2",
|
|
42
46
|
"lit": "^3.3.2",
|
|
43
47
|
"lucide-static": "^0.574.0",
|
|
44
48
|
"marked": "^17.0.1",
|
|
49
|
+
"mediabunny": "^1.35.1",
|
|
50
|
+
"nanoid": "^5.1.6",
|
|
51
|
+
"smol-toml": "^1.6.0",
|
|
45
52
|
"sortablejs": "^1.15.6",
|
|
46
|
-
"
|
|
53
|
+
"tiptap-markdown": "^0.9.0",
|
|
47
54
|
"uuidv7": "^1.1.0",
|
|
55
|
+
"yaml": "^2.8.2",
|
|
48
56
|
"zod": "^4.3.6"
|
|
49
57
|
},
|
|
50
58
|
"peerDependencies": {
|
|
@@ -103,7 +111,7 @@
|
|
|
103
111
|
"build:lib": "vite build --config vite.config.worker.ts",
|
|
104
112
|
"typecheck": "tsc -b --noEmit",
|
|
105
113
|
"db:generate": "drizzle-kit generate",
|
|
106
|
-
"i18n:extract": "lingui extract",
|
|
114
|
+
"i18n:extract": "lingui extract --clean",
|
|
107
115
|
"i18n:compile": "lingui compile --typescript",
|
|
108
116
|
"i18n:build": "pnpm i18n:extract && pnpm i18n:compile",
|
|
109
117
|
"dev": "pnpm db:migrate:local && vite dev",
|
|
@@ -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>();
|
|
@@ -1,146 +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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
+
}
|
|
121
88
|
|
|
122
|
-
|
|
123
|
-
|
|
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:");
|
|
124
99
|
|
|
125
|
-
//
|
|
126
|
-
|
|
100
|
+
// Enable WAL mode for better performance
|
|
101
|
+
sqlite.pragma("journal_mode = WAL");
|
|
102
|
+
sqlite.pragma("foreign_keys = ON");
|
|
127
103
|
|
|
128
|
-
// Apply
|
|
129
|
-
|
|
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
|
+
}
|
|
130
121
|
|
|
131
122
|
const db = drizzle(sqlite, { schema });
|
|
132
123
|
|
|
133
124
|
// Polyfill D1 batch() for test compatibility.
|
|
134
125
|
// In production, D1 batch executes statements atomically in a single transaction.
|
|
135
|
-
// In tests,
|
|
136
|
-
//
|
|
126
|
+
// In tests, wrap sequential execution in an explicit transaction so rollback
|
|
127
|
+
// behavior matches D1's all-or-nothing semantics.
|
|
137
128
|
Object.defineProperty(db, "batch", {
|
|
138
129
|
value: async (queries: PromiseLike<unknown>[]) => {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
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;
|
|
142
141
|
}
|
|
143
|
-
return results;
|
|
144
142
|
},
|
|
145
143
|
});
|
|
146
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,23 +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";
|
|
28
|
+
// Routes - Settings (admin)
|
|
29
|
+
import { settingsRoutes } from "./routes/dash/settings.js";
|
|
30
|
+
import { customUrlsRoutes } from "./routes/dash/custom-urls.js";
|
|
35
31
|
|
|
36
32
|
// Routes - API
|
|
37
33
|
import { postsApiRoutes } from "./routes/api/posts.js";
|
|
38
|
-
import { pagesApiRoutes } from "./routes/api/pages.js";
|
|
39
34
|
import { navItemsApiRoutes } from "./routes/api/nav-items.js";
|
|
40
35
|
import { collectionsApiRoutes } from "./routes/api/collections.js";
|
|
41
36
|
import { settingsApiRoutes } from "./routes/api/settings.js";
|
|
42
37
|
import { uploadApiRoutes } from "./routes/api/upload.js";
|
|
38
|
+
import { multipartUploadApiRoutes } from "./routes/api/upload-multipart.js";
|
|
43
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";
|
|
44
42
|
// Routes - Compose
|
|
45
43
|
import { composeRoutes } from "./routes/compose.js";
|
|
46
44
|
|
|
@@ -49,10 +47,11 @@ import { rssRoutes } from "./routes/feed/rss.js";
|
|
|
49
47
|
import { sitemapRoutes } from "./routes/feed/sitemap.js";
|
|
50
48
|
|
|
51
49
|
// Middleware
|
|
52
|
-
import { requireAuth } from "./middleware/auth.js";
|
|
50
|
+
import { requireAuth, isLocalHostname } from "./middleware/auth.js";
|
|
53
51
|
import { requireOnboarding } from "./middleware/onboarding.js";
|
|
54
52
|
import { errorHandler } from "./middleware/error-handler.js";
|
|
55
53
|
import { withConfig } from "./middleware/config.js";
|
|
54
|
+
import { secureHeadersMiddleware } from "./middleware/secure-headers.js";
|
|
56
55
|
|
|
57
56
|
import { createStorageDriver } from "./lib/storage.js";
|
|
58
57
|
import { base64ToUint8Array } from "./lib/favicon.js";
|
|
@@ -80,6 +79,31 @@ export function createApp(): App {
|
|
|
80
79
|
|
|
81
80
|
// Lightweight init — no DB queries
|
|
82
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
|
+
|
|
83
107
|
if (!c.env.AUTH_SECRET) {
|
|
84
108
|
return c.html(
|
|
85
109
|
`<!DOCTYPE html>
|
|
@@ -107,7 +131,11 @@ export function createApp(): App {
|
|
|
107
131
|
// Note: Drizzle ORM doesn't officially support D1DatabaseSession yet (issue #2226)
|
|
108
132
|
// but it works at runtime. We use type assertion as a temporary workaround.
|
|
109
133
|
const db = createDatabase(session as unknown as D1Database);
|
|
110
|
-
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
|
+
);
|
|
111
139
|
c.set("storage", createStorageDriver(c.env));
|
|
112
140
|
|
|
113
141
|
const baseURL = c.env.SITE_URL || new URL(c.req.url).origin;
|
|
@@ -124,20 +152,118 @@ export function createApp(): App {
|
|
|
124
152
|
await next();
|
|
125
153
|
});
|
|
126
154
|
|
|
155
|
+
// Security headers (CSP, X-Frame-Options, etc.)
|
|
156
|
+
app.use("*", secureHeadersMiddleware());
|
|
157
|
+
|
|
127
158
|
// --- Routes that don't need config/theme ---
|
|
128
159
|
|
|
129
160
|
// Health check
|
|
130
161
|
app.get("/health", (c) => c.json({ status: "ok" }));
|
|
131
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
|
+
|
|
132
191
|
// Media files from storage (path matches storage key: media/YYYY/MM/uuid.ext)
|
|
192
|
+
// Supports HTTP Range requests for seekable audio/video playback.
|
|
133
193
|
app.get("/media/*", async (c) => {
|
|
134
194
|
const storage = c.var.storage;
|
|
135
195
|
if (!storage) {
|
|
136
196
|
return c.notFound();
|
|
137
197
|
}
|
|
138
198
|
|
|
139
|
-
// The storage key is the full path without the leading "/"
|
|
140
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
|
|
141
267
|
const object = await storage.get(storageKey);
|
|
142
268
|
if (!object) {
|
|
143
269
|
return c.notFound();
|
|
@@ -149,6 +275,10 @@ export function createApp(): App {
|
|
|
149
275
|
object.contentType || "application/octet-stream",
|
|
150
276
|
);
|
|
151
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
|
+
}
|
|
152
282
|
|
|
153
283
|
return new Response(object.body, { headers });
|
|
154
284
|
});
|
|
@@ -204,7 +334,7 @@ export function createApp(): App {
|
|
|
204
334
|
await next();
|
|
205
335
|
});
|
|
206
336
|
|
|
207
|
-
// Redirect middleware
|
|
337
|
+
// Redirect middleware — only handles redirect-type custom URLs
|
|
208
338
|
app.use("*", async (c, next) => {
|
|
209
339
|
const path = new URL(c.req.url).pathname;
|
|
210
340
|
// Skip redirect check for API routes and static assets
|
|
@@ -212,9 +342,9 @@ export function createApp(): App {
|
|
|
212
342
|
return next();
|
|
213
343
|
}
|
|
214
344
|
|
|
215
|
-
const
|
|
216
|
-
if (redirect) {
|
|
217
|
-
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);
|
|
218
348
|
}
|
|
219
349
|
|
|
220
350
|
await next();
|
|
@@ -228,25 +358,25 @@ export function createApp(): App {
|
|
|
228
358
|
|
|
229
359
|
// API Routes
|
|
230
360
|
app.route("/api/posts", postsApiRoutes);
|
|
231
|
-
app.route("/api/pages", pagesApiRoutes);
|
|
232
361
|
app.route("/api/nav-items", navItemsApiRoutes);
|
|
233
362
|
app.route("/api/collections", collectionsApiRoutes);
|
|
234
363
|
app.route("/api/settings", settingsApiRoutes);
|
|
364
|
+
app.route("/api/custom-urls", customUrlsApiRoutes);
|
|
365
|
+
app.route("/api/export", exportApiRoutes);
|
|
235
366
|
|
|
236
367
|
// Auth routes
|
|
237
368
|
app.route("/", setupRoutes);
|
|
238
369
|
app.route("/", signinRoutes);
|
|
239
370
|
app.route("/", resetRoutes);
|
|
240
371
|
|
|
241
|
-
//
|
|
242
|
-
app.use("/
|
|
243
|
-
app.
|
|
244
|
-
app.route("/
|
|
245
|
-
app.route("/
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
app.route("/
|
|
249
|
-
// Protected 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);
|
|
250
380
|
app.route("/api/upload", uploadApiRoutes);
|
|
251
381
|
app.route("/api/search", searchApiRoutes);
|
|
252
382
|
|
|
@@ -259,12 +389,12 @@ export function createApp(): App {
|
|
|
259
389
|
|
|
260
390
|
// Frontend routes
|
|
261
391
|
app.route("/search", searchRoutes);
|
|
392
|
+
app.route("/", newPostRoutes);
|
|
262
393
|
app.route("/archive", archiveRoutes);
|
|
263
394
|
app.route("/featured", featuredRoutes);
|
|
264
395
|
app.route("/latest", latestRoutes);
|
|
265
396
|
app.route("/c", collectionsPageRoutes);
|
|
266
397
|
app.route("/c", collectionRoutes);
|
|
267
|
-
app.route("/p", postRoutes);
|
|
268
398
|
app.route("/", homeRoutes);
|
|
269
399
|
|
|
270
400
|
// Custom page catch-all (must be last)
|
package/src/auth.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Authentication with better-auth
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { betterAuth } from "better-auth";
|
|
5
|
+
import { betterAuth, APIError } from "better-auth";
|
|
6
6
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
7
7
|
import { drizzle } from "drizzle-orm/d1";
|
|
8
8
|
import * as schema from "./db/schema.js";
|
|
@@ -34,12 +34,30 @@ export function createAuth(
|
|
|
34
34
|
minPasswordLength: 8,
|
|
35
35
|
},
|
|
36
36
|
session: {
|
|
37
|
-
expiresIn: 3600 * 24 *
|
|
37
|
+
expiresIn: 3600 * 24 * 30, // 30 days
|
|
38
38
|
cookieCache: {
|
|
39
39
|
enabled: true,
|
|
40
40
|
maxAge: 60 * 5, // 5 minutes
|
|
41
41
|
},
|
|
42
42
|
},
|
|
43
|
+
databaseHooks: {
|
|
44
|
+
user: {
|
|
45
|
+
create: {
|
|
46
|
+
before: async (userData) => {
|
|
47
|
+
const existing = await db
|
|
48
|
+
.select({ id: schema.user.id })
|
|
49
|
+
.from(schema.user)
|
|
50
|
+
.limit(1);
|
|
51
|
+
if (existing.length > 0) {
|
|
52
|
+
throw new APIError("FORBIDDEN", {
|
|
53
|
+
message: "Registration is closed.",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return { data: { ...userData, role: "admin" } };
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
43
61
|
});
|
|
44
62
|
}
|
|
45
63
|
|