@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/src/lib/view.ts
CHANGED
|
@@ -8,22 +8,21 @@
|
|
|
8
8
|
import type {
|
|
9
9
|
Post,
|
|
10
10
|
PostWithMedia,
|
|
11
|
-
Page,
|
|
12
11
|
Media,
|
|
13
12
|
MediaView,
|
|
14
13
|
PostView,
|
|
15
|
-
|
|
14
|
+
CollectionTagView,
|
|
16
15
|
NavItemView,
|
|
17
16
|
NavItem,
|
|
18
17
|
SearchResult,
|
|
19
18
|
SearchResultView,
|
|
20
19
|
ArchiveGroup,
|
|
20
|
+
Collection,
|
|
21
21
|
Format,
|
|
22
22
|
Status,
|
|
23
23
|
NavItemType,
|
|
24
24
|
AppConfig,
|
|
25
25
|
} from "../types.js";
|
|
26
|
-
import { encode } from "./sqid.js";
|
|
27
26
|
import {
|
|
28
27
|
toISOString,
|
|
29
28
|
formatDate,
|
|
@@ -32,6 +31,9 @@ import {
|
|
|
32
31
|
} from "./time.js";
|
|
33
32
|
import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "./image.js";
|
|
34
33
|
import { getHtmlExcerpt } from "./excerpt.js";
|
|
34
|
+
import { highlightText } from "./search-snippet.js";
|
|
35
|
+
import { renderCollectionIcon } from "./icons.js";
|
|
36
|
+
import { escapeHtml } from "./html.js";
|
|
35
37
|
|
|
36
38
|
// =============================================================================
|
|
37
39
|
// Media Context
|
|
@@ -90,6 +92,18 @@ export function toMediaView(media: Media, ctx: MediaContext): MediaView {
|
|
|
90
92
|
})
|
|
91
93
|
: url;
|
|
92
94
|
|
|
95
|
+
const posterRawUrl = media.posterKey
|
|
96
|
+
? getMediaUrl(media.posterKey, publicUrl)
|
|
97
|
+
: undefined;
|
|
98
|
+
const posterUrl = posterRawUrl
|
|
99
|
+
? getImageUrl(posterRawUrl, ctx.imageTransformUrl, {
|
|
100
|
+
width: 640,
|
|
101
|
+
quality: 80,
|
|
102
|
+
format: "auto",
|
|
103
|
+
fit: "scale-down",
|
|
104
|
+
})
|
|
105
|
+
: undefined;
|
|
106
|
+
|
|
93
107
|
return {
|
|
94
108
|
id: media.id,
|
|
95
109
|
url,
|
|
@@ -99,6 +113,10 @@ export function toMediaView(media: Media, ctx: MediaContext): MediaView {
|
|
|
99
113
|
width: media.width ?? undefined,
|
|
100
114
|
height: media.height ?? undefined,
|
|
101
115
|
size: media.size,
|
|
116
|
+
blurhash: media.blurhash ?? undefined,
|
|
117
|
+
waveform: media.waveform ?? undefined,
|
|
118
|
+
posterUrl,
|
|
119
|
+
chars: media.chars ?? undefined,
|
|
102
120
|
};
|
|
103
121
|
}
|
|
104
122
|
|
|
@@ -111,10 +129,19 @@ export function toMediaView(media: Media, ctx: MediaContext): MediaView {
|
|
|
111
129
|
*
|
|
112
130
|
* @param post - Post with media attachments from database
|
|
113
131
|
* @param _ctx - Media context with URL configuration
|
|
132
|
+
* @param postCollections - Optional collections this post belongs to
|
|
114
133
|
* @returns Render-ready PostView with pre-computed fields
|
|
115
134
|
*/
|
|
116
|
-
export function toPostView(
|
|
117
|
-
|
|
135
|
+
export function toPostView(
|
|
136
|
+
post: PostWithMedia,
|
|
137
|
+
_ctx: MediaContext,
|
|
138
|
+
postCollections?: Collection[],
|
|
139
|
+
threadRootPermalink?: string,
|
|
140
|
+
isLastInThread?: boolean,
|
|
141
|
+
): PostView {
|
|
142
|
+
const id = post.id;
|
|
143
|
+
const permalink = `/${post.slug}`;
|
|
144
|
+
const publishedAt = post.publishedAt ?? post.updatedAt;
|
|
118
145
|
|
|
119
146
|
// Pre-compute excerpt from raw body
|
|
120
147
|
let excerpt: string | undefined;
|
|
@@ -132,7 +159,7 @@ export function toPostView(post: PostWithMedia, _ctx: MediaContext): PostView {
|
|
|
132
159
|
// Use stored summary (generated from Tiptap JSON)
|
|
133
160
|
summaryHtml = post.summary
|
|
134
161
|
.split("\n\n")
|
|
135
|
-
.map((p) => `<p>${p}</p>`)
|
|
162
|
+
.map((p) => `<p>${escapeHtml(p)}</p>`)
|
|
136
163
|
.join("");
|
|
137
164
|
summaryHasMore = true;
|
|
138
165
|
} else {
|
|
@@ -152,6 +179,12 @@ export function toPostView(post: PostWithMedia, _ctx: MediaContext): PostView {
|
|
|
152
179
|
}
|
|
153
180
|
}
|
|
154
181
|
|
|
182
|
+
// Convert collection tags
|
|
183
|
+
const collections: CollectionTagView[] = (postCollections ?? []).map((c) => {
|
|
184
|
+
const iconHtml = renderCollectionIcon(c.icon, { size: 12 }) || undefined;
|
|
185
|
+
return { slug: c.slug, title: c.title, iconHtml };
|
|
186
|
+
});
|
|
187
|
+
|
|
155
188
|
// Convert media attachments
|
|
156
189
|
const media: MediaView[] = post.mediaAttachments.map((m) => ({
|
|
157
190
|
id: m.id,
|
|
@@ -161,12 +194,19 @@ export function toPostView(post: PostWithMedia, _ctx: MediaContext): PostView {
|
|
|
161
194
|
altText: m.alt ?? undefined,
|
|
162
195
|
width: m.width ?? undefined,
|
|
163
196
|
height: m.height ?? undefined,
|
|
197
|
+
size: m.size ?? undefined,
|
|
198
|
+
blurhash: m.blurhash ?? undefined,
|
|
199
|
+
waveform: m.waveform ?? undefined,
|
|
200
|
+
posterUrl: m.posterUrl ?? undefined,
|
|
201
|
+
originalName: m.originalName ?? undefined,
|
|
202
|
+
summary: m.summary ?? undefined,
|
|
203
|
+
chars: m.chars ?? undefined,
|
|
164
204
|
}));
|
|
165
205
|
|
|
166
206
|
return {
|
|
167
|
-
id
|
|
207
|
+
id,
|
|
168
208
|
permalink,
|
|
169
|
-
|
|
209
|
+
slug: post.slug,
|
|
170
210
|
title: post.title ?? undefined,
|
|
171
211
|
bodyHtml: bodyHtmlWithAnchor ?? undefined,
|
|
172
212
|
excerpt,
|
|
@@ -177,35 +217,68 @@ export function toPostView(post: PostWithMedia, _ctx: MediaContext): PostView {
|
|
|
177
217
|
format: post.format as Format,
|
|
178
218
|
status: post.status as Status,
|
|
179
219
|
visibility: post.visibility,
|
|
180
|
-
pinned: post.
|
|
220
|
+
pinned: post.pinnedAt !== null,
|
|
221
|
+
featured: post.featuredAt !== null,
|
|
181
222
|
rating: post.rating ?? undefined,
|
|
182
|
-
publishedAt: toISOString(
|
|
183
|
-
publishedAtFormatted: formatDate(
|
|
184
|
-
publishedAtTime: formatTime(
|
|
185
|
-
publishedAtRelative: formatRelativeTime(
|
|
223
|
+
publishedAt: toISOString(publishedAt),
|
|
224
|
+
publishedAtFormatted: formatDate(publishedAt),
|
|
225
|
+
publishedAtTime: formatTime(publishedAt),
|
|
226
|
+
publishedAtRelative: formatRelativeTime(publishedAt),
|
|
186
227
|
updatedAt: toISOString(post.updatedAt),
|
|
187
228
|
media,
|
|
229
|
+
collections,
|
|
188
230
|
replyToId: post.replyToId ?? undefined,
|
|
189
|
-
threadRootId: post.threadId
|
|
231
|
+
threadRootId: post.replyToId ? post.threadId : undefined,
|
|
232
|
+
threadRootPermalink,
|
|
233
|
+
isLastInThread: isLastInThread ?? true,
|
|
190
234
|
body: post.body ?? undefined,
|
|
191
235
|
};
|
|
192
236
|
}
|
|
193
237
|
|
|
194
238
|
/**
|
|
195
239
|
* Batch converts PostWithMedia[] to PostView[].
|
|
240
|
+
*
|
|
241
|
+
* @param posts - Posts with media attachments
|
|
242
|
+
* @param ctx - Media context with URL configuration
|
|
243
|
+
* @param threadRootPermalinkMap - Optional map of thread root ID → permalink
|
|
244
|
+
* @returns Render-ready PostView[]
|
|
196
245
|
*/
|
|
197
246
|
export function toPostViews(
|
|
198
247
|
posts: PostWithMedia[],
|
|
199
248
|
ctx: MediaContext,
|
|
249
|
+
threadRootPermalinkMap?: Map<string, string>,
|
|
250
|
+
isLastInThreadMap?: Map<string, boolean>,
|
|
200
251
|
): PostView[] {
|
|
201
|
-
return posts.map((p) =>
|
|
252
|
+
return posts.map((p) => {
|
|
253
|
+
const rootPermalink = p.replyToId
|
|
254
|
+
? threadRootPermalinkMap?.get(p.threadId)
|
|
255
|
+
: undefined;
|
|
256
|
+
return toPostView(
|
|
257
|
+
p,
|
|
258
|
+
ctx,
|
|
259
|
+
undefined,
|
|
260
|
+
rootPermalink,
|
|
261
|
+
isLastInThreadMap?.get(p.id),
|
|
262
|
+
);
|
|
263
|
+
});
|
|
202
264
|
}
|
|
203
265
|
|
|
204
266
|
/**
|
|
205
267
|
* Converts a bare Post (no media) to a PostView with empty media array.
|
|
206
268
|
*/
|
|
207
|
-
export function toPostViewFromPost(
|
|
208
|
-
|
|
269
|
+
export function toPostViewFromPost(
|
|
270
|
+
post: Post,
|
|
271
|
+
ctx: MediaContext,
|
|
272
|
+
threadRootPermalink?: string,
|
|
273
|
+
isLastInThread?: boolean,
|
|
274
|
+
): PostView {
|
|
275
|
+
return toPostView(
|
|
276
|
+
{ ...post, mediaAttachments: [] },
|
|
277
|
+
ctx,
|
|
278
|
+
undefined,
|
|
279
|
+
threadRootPermalink,
|
|
280
|
+
isLastInThread,
|
|
281
|
+
);
|
|
209
282
|
}
|
|
210
283
|
|
|
211
284
|
/**
|
|
@@ -214,27 +287,54 @@ export function toPostViewFromPost(post: Post, ctx: MediaContext): PostView {
|
|
|
214
287
|
export function toPostViewsFromPosts(
|
|
215
288
|
posts: Post[],
|
|
216
289
|
ctx: MediaContext,
|
|
290
|
+
threadRootPermalinkMap?: Map<string, string>,
|
|
291
|
+
isLastInThreadMap?: Map<string, boolean>,
|
|
217
292
|
): PostView[] {
|
|
218
|
-
return posts.map((p) =>
|
|
293
|
+
return posts.map((p) => {
|
|
294
|
+
const rootPermalink = p.replyToId
|
|
295
|
+
? threadRootPermalinkMap?.get(p.threadId)
|
|
296
|
+
: undefined;
|
|
297
|
+
return toPostViewFromPost(
|
|
298
|
+
p,
|
|
299
|
+
ctx,
|
|
300
|
+
rootPermalink,
|
|
301
|
+
isLastInThreadMap?.get(p.id),
|
|
302
|
+
);
|
|
303
|
+
});
|
|
219
304
|
}
|
|
220
305
|
|
|
221
306
|
// =============================================================================
|
|
222
|
-
//
|
|
307
|
+
// Thread Helpers
|
|
223
308
|
// =============================================================================
|
|
224
309
|
|
|
225
310
|
/**
|
|
226
|
-
*
|
|
311
|
+
* Builds a map of thread root ID → permalink for posts that are thread replies.
|
|
312
|
+
*
|
|
313
|
+
* @param posts - Posts to inspect for thread membership
|
|
314
|
+
* @param getById - Lookup function to fetch a post by ID
|
|
315
|
+
* @returns Map of thread root ID → permalink string (e.g. `/{slug}`)
|
|
316
|
+
*
|
|
317
|
+
* @example
|
|
318
|
+
* ```ts
|
|
319
|
+
* const map = await loadThreadRootPermalinks(posts, services.posts.getById);
|
|
320
|
+
* const views = toPostViews(postsWithMedia, mediaCtx, map);
|
|
321
|
+
* ```
|
|
227
322
|
*/
|
|
228
|
-
export function
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
323
|
+
export async function loadThreadRootPermalinks(
|
|
324
|
+
posts: Post[],
|
|
325
|
+
getById: (id: string) => Promise<Post | null>,
|
|
326
|
+
): Promise<Map<string, string>> {
|
|
327
|
+
const threadRootIds = [
|
|
328
|
+
...new Set(posts.filter((p) => p.replyToId).map((p) => p.threadId)),
|
|
329
|
+
];
|
|
330
|
+
const map = new Map<string, string>();
|
|
331
|
+
if (threadRootIds.length > 0) {
|
|
332
|
+
const roots = await Promise.all(threadRootIds.map(getById));
|
|
333
|
+
for (const root of roots) {
|
|
334
|
+
if (root) map.set(root.id, `/${root.slug}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return map;
|
|
238
338
|
}
|
|
239
339
|
|
|
240
340
|
// =============================================================================
|
|
@@ -246,7 +346,7 @@ export function toPageView(page: Page): PageView {
|
|
|
246
346
|
*
|
|
247
347
|
* @param item - Raw nav item from database
|
|
248
348
|
* @param currentPath - Current URL path for active state
|
|
249
|
-
* @param isAuthenticated - Whether the user is logged in (affects system
|
|
349
|
+
* @param isAuthenticated - Whether the user is logged in (affects system settings item)
|
|
250
350
|
*/
|
|
251
351
|
export function toNavItemView(
|
|
252
352
|
item: NavItem,
|
|
@@ -256,9 +356,13 @@ export function toNavItemView(
|
|
|
256
356
|
let url = item.url;
|
|
257
357
|
let label = item.label;
|
|
258
358
|
|
|
259
|
-
// System
|
|
260
|
-
|
|
261
|
-
|
|
359
|
+
// System settings item: resolve URL and label based on auth
|
|
360
|
+
// Also handles legacy "/dash" URLs from existing DB data
|
|
361
|
+
if (
|
|
362
|
+
item.type === "system" &&
|
|
363
|
+
(item.url === "/settings" || item.url === "/dash")
|
|
364
|
+
) {
|
|
365
|
+
url = isAuthenticated ? "/settings" : "/signin";
|
|
262
366
|
if (!isAuthenticated) {
|
|
263
367
|
label = "Sign in";
|
|
264
368
|
}
|
|
@@ -280,7 +384,6 @@ export function toNavItemView(
|
|
|
280
384
|
type: item.type as NavItemType,
|
|
281
385
|
label,
|
|
282
386
|
url,
|
|
283
|
-
pageId: item.pageId ?? undefined,
|
|
284
387
|
isActive,
|
|
285
388
|
isExternal,
|
|
286
389
|
};
|
|
@@ -307,26 +410,57 @@ export function toNavItemViews(
|
|
|
307
410
|
|
|
308
411
|
/**
|
|
309
412
|
* Converts a SearchResult to a SearchResultView with PostView.
|
|
413
|
+
*
|
|
414
|
+
* @param result - Raw search result with post and FTS metadata
|
|
415
|
+
* @param ctx - Media context for URL computation
|
|
416
|
+
* @param query - Original search query for client-side title/quote highlighting
|
|
310
417
|
*/
|
|
311
418
|
export function toSearchResultView(
|
|
312
419
|
result: SearchResult,
|
|
313
420
|
ctx: MediaContext,
|
|
421
|
+
query?: string,
|
|
314
422
|
): SearchResultView {
|
|
423
|
+
const post = toPostViewFromPost(result.post, ctx);
|
|
424
|
+
|
|
425
|
+
let titleHighlighted: string | undefined;
|
|
426
|
+
let quoteHighlighted: string | undefined;
|
|
427
|
+
|
|
428
|
+
if (query) {
|
|
429
|
+
if (post.title) {
|
|
430
|
+
titleHighlighted = highlightText(post.title, query);
|
|
431
|
+
}
|
|
432
|
+
if (post.quoteText) {
|
|
433
|
+
// Truncate before highlighting to avoid splitting inside <mark> tags
|
|
434
|
+
const truncated =
|
|
435
|
+
post.quoteText.length > 120
|
|
436
|
+
? post.quoteText.slice(0, 120) + "..."
|
|
437
|
+
: post.quoteText;
|
|
438
|
+
quoteHighlighted = highlightText(truncated, query);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
315
442
|
return {
|
|
316
|
-
post
|
|
443
|
+
post,
|
|
317
444
|
rank: result.rank,
|
|
318
445
|
snippet: result.snippet,
|
|
446
|
+
titleHighlighted,
|
|
447
|
+
quoteHighlighted,
|
|
319
448
|
};
|
|
320
449
|
}
|
|
321
450
|
|
|
322
451
|
/**
|
|
323
452
|
* Batch converts SearchResult[] to SearchResultView[].
|
|
453
|
+
*
|
|
454
|
+
* @param results - Raw search results
|
|
455
|
+
* @param ctx - Media context for URL computation
|
|
456
|
+
* @param query - Original search query for title/quote highlighting
|
|
324
457
|
*/
|
|
325
458
|
export function toSearchResultViews(
|
|
326
459
|
results: SearchResult[],
|
|
327
460
|
ctx: MediaContext,
|
|
461
|
+
query?: string,
|
|
328
462
|
): SearchResultView[] {
|
|
329
|
-
return results.map((r) => toSearchResultView(r, ctx));
|
|
463
|
+
return results.map((r) => toSearchResultView(r, ctx, query));
|
|
330
464
|
}
|
|
331
465
|
|
|
332
466
|
// =============================================================================
|
|
@@ -360,3 +494,36 @@ export function toArchiveGroups(
|
|
|
360
494
|
}
|
|
361
495
|
return groups;
|
|
362
496
|
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Converts a grouped PostWithMedia map to typed ArchiveGroup[].
|
|
500
|
+
* Unlike toArchiveGroups, this preserves media attachments on each post.
|
|
501
|
+
*
|
|
502
|
+
* @param grouped - Map of "YYYY-MM" keys to PostWithMedia arrays
|
|
503
|
+
* @param ctx - Media context for URL computation
|
|
504
|
+
* @returns ArchiveGroup[] with full media data on each PostView
|
|
505
|
+
*/
|
|
506
|
+
export function toArchiveGroupsWithMedia(
|
|
507
|
+
grouped: Map<string, PostWithMedia[]>,
|
|
508
|
+
ctx: MediaContext,
|
|
509
|
+
): ArchiveGroup[] {
|
|
510
|
+
const groups: ArchiveGroup[] = [];
|
|
511
|
+
for (const [yearMonth, posts] of grouped) {
|
|
512
|
+
const [year, month] = yearMonth.split("-");
|
|
513
|
+
if (!year || !month) continue;
|
|
514
|
+
|
|
515
|
+
const date = new Date(parseInt(year, 10), parseInt(month, 10) - 1);
|
|
516
|
+
const label = date.toLocaleDateString("en-US", {
|
|
517
|
+
year: "numeric",
|
|
518
|
+
month: "long",
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
groups.push({
|
|
522
|
+
year,
|
|
523
|
+
month,
|
|
524
|
+
label,
|
|
525
|
+
posts: toPostViews(posts, ctx),
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
return groups;
|
|
529
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
2
|
import { Hono } from "hono";
|
|
3
|
-
import { requireAuth, requireAuthApi } from "../auth.js";
|
|
3
|
+
import { requireAuth, requireAuthApi, isLocalHostname } from "../auth.js";
|
|
4
4
|
import { errorHandler } from "../error-handler.js";
|
|
5
5
|
import type { Bindings } from "../../types.js";
|
|
6
6
|
import type { AppVariables } from "../../types/app-context.js";
|
|
@@ -21,6 +21,32 @@ function createMockAuth(authenticated: boolean) {
|
|
|
21
21
|
} as AppVariables["auth"];
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function createMockApiTokenService(validToken?: string) {
|
|
25
|
+
const tokenId = "token-id-1";
|
|
26
|
+
return {
|
|
27
|
+
verify: vi.fn(async (raw: string) => (raw === validToken ? tokenId : null)),
|
|
28
|
+
updateLastUsed: vi.fn(async () => {}),
|
|
29
|
+
create: vi.fn(),
|
|
30
|
+
list: vi.fn(),
|
|
31
|
+
delete: vi.fn(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("isLocalHostname", () => {
|
|
36
|
+
it.each([
|
|
37
|
+
["localhost", true],
|
|
38
|
+
["127.0.0.1", true],
|
|
39
|
+
["::1", true],
|
|
40
|
+
["jant.localtest.me", true],
|
|
41
|
+
["sub.localtest.me", true],
|
|
42
|
+
["myblog.com", false],
|
|
43
|
+
["demo.jant.me", false],
|
|
44
|
+
["localtest.me.evil.com", false],
|
|
45
|
+
])("isLocalHostname(%s) → %s", (hostname, expected) => {
|
|
46
|
+
expect(isLocalHostname(hostname)).toBe(expected);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
24
50
|
describe("requireAuth", () => {
|
|
25
51
|
it("allows authenticated requests", async () => {
|
|
26
52
|
const app = new Hono<Env>();
|
|
@@ -28,11 +54,11 @@ describe("requireAuth", () => {
|
|
|
28
54
|
c.set("auth", createMockAuth(true));
|
|
29
55
|
await next();
|
|
30
56
|
});
|
|
31
|
-
app.get("/
|
|
57
|
+
app.get("/settings", requireAuth(), (c) => c.text("Settings"));
|
|
32
58
|
|
|
33
|
-
const res = await app.request("/
|
|
59
|
+
const res = await app.request("/settings");
|
|
34
60
|
expect(res.status).toBe(200);
|
|
35
|
-
expect(await res.text()).toBe("
|
|
61
|
+
expect(await res.text()).toBe("Settings");
|
|
36
62
|
});
|
|
37
63
|
|
|
38
64
|
it("redirects unauthenticated requests to /signin", async () => {
|
|
@@ -41,9 +67,9 @@ describe("requireAuth", () => {
|
|
|
41
67
|
c.set("auth", createMockAuth(false));
|
|
42
68
|
await next();
|
|
43
69
|
});
|
|
44
|
-
app.get("/
|
|
70
|
+
app.get("/settings", requireAuth(), (c) => c.text("Settings"));
|
|
45
71
|
|
|
46
|
-
const res = await app.request("/
|
|
72
|
+
const res = await app.request("/settings", { redirect: "manual" });
|
|
47
73
|
expect(res.status).toBe(302);
|
|
48
74
|
expect(res.headers.get("Location")).toBe("/signin");
|
|
49
75
|
});
|
|
@@ -54,20 +80,23 @@ describe("requireAuth", () => {
|
|
|
54
80
|
c.set("auth", createMockAuth(false));
|
|
55
81
|
await next();
|
|
56
82
|
});
|
|
57
|
-
app.get("/
|
|
83
|
+
app.get("/settings", requireAuth("/login"), (c) => c.text("Settings"));
|
|
58
84
|
|
|
59
|
-
const res = await app.request("/
|
|
85
|
+
const res = await app.request("/settings", { redirect: "manual" });
|
|
60
86
|
expect(res.status).toBe(302);
|
|
61
87
|
expect(res.headers.get("Location")).toBe("/login");
|
|
62
88
|
});
|
|
63
89
|
});
|
|
64
90
|
|
|
65
91
|
describe("requireAuthApi", () => {
|
|
66
|
-
it("allows authenticated requests", async () => {
|
|
92
|
+
it("allows authenticated requests via session", async () => {
|
|
67
93
|
const app = new Hono<Env>();
|
|
68
94
|
app.onError(errorHandler);
|
|
69
95
|
app.use("*", async (c, next) => {
|
|
70
96
|
c.set("auth", createMockAuth(true));
|
|
97
|
+
c.set("services", {
|
|
98
|
+
apiTokens: createMockApiTokenService(),
|
|
99
|
+
} as AppVariables["services"]);
|
|
71
100
|
await next();
|
|
72
101
|
});
|
|
73
102
|
app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
|
|
@@ -79,11 +108,14 @@ describe("requireAuthApi", () => {
|
|
|
79
108
|
expect(body.data).toBe("secret");
|
|
80
109
|
});
|
|
81
110
|
|
|
82
|
-
it("returns 401 for unauthenticated requests", async () => {
|
|
111
|
+
it("returns 401 for unauthenticated requests without Bearer token", async () => {
|
|
83
112
|
const app = new Hono<Env>();
|
|
84
113
|
app.onError(errorHandler);
|
|
85
114
|
app.use("*", async (c, next) => {
|
|
86
115
|
c.set("auth", createMockAuth(false));
|
|
116
|
+
c.set("services", {
|
|
117
|
+
apiTokens: createMockApiTokenService(),
|
|
118
|
+
} as AppVariables["services"]);
|
|
87
119
|
await next();
|
|
88
120
|
});
|
|
89
121
|
app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
|
|
@@ -107,6 +139,9 @@ describe("requireAuthApi", () => {
|
|
|
107
139
|
},
|
|
108
140
|
},
|
|
109
141
|
} as AppVariables["auth"]);
|
|
142
|
+
c.set("services", {
|
|
143
|
+
apiTokens: createMockApiTokenService(),
|
|
144
|
+
} as AppVariables["services"]);
|
|
110
145
|
await next();
|
|
111
146
|
});
|
|
112
147
|
app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
|
|
@@ -114,4 +149,149 @@ describe("requireAuthApi", () => {
|
|
|
114
149
|
const res = await app.request("/api/data");
|
|
115
150
|
expect(res.status).toBe(401);
|
|
116
151
|
});
|
|
152
|
+
|
|
153
|
+
it("allows requests with valid Bearer token when session auth fails", async () => {
|
|
154
|
+
const validToken = "jnt_abc123";
|
|
155
|
+
const mockApiTokens = createMockApiTokenService(validToken);
|
|
156
|
+
|
|
157
|
+
const app = new Hono<Env>();
|
|
158
|
+
app.onError(errorHandler);
|
|
159
|
+
app.use("*", async (c, next) => {
|
|
160
|
+
c.set("auth", createMockAuth(false));
|
|
161
|
+
c.set("services", {
|
|
162
|
+
apiTokens: mockApiTokens,
|
|
163
|
+
} as AppVariables["services"]);
|
|
164
|
+
await next();
|
|
165
|
+
});
|
|
166
|
+
app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
|
|
167
|
+
|
|
168
|
+
const res = await app.request("/api/data", {
|
|
169
|
+
headers: { Authorization: `Bearer ${validToken}` },
|
|
170
|
+
});
|
|
171
|
+
expect(res.status).toBe(200);
|
|
172
|
+
|
|
173
|
+
const body = await res.json();
|
|
174
|
+
expect(body.data).toBe("secret");
|
|
175
|
+
|
|
176
|
+
expect(mockApiTokens.verify).toHaveBeenCalledWith(validToken);
|
|
177
|
+
expect(mockApiTokens.updateLastUsed).toHaveBeenCalledWith("token-id-1");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("returns 401 for invalid Bearer token", async () => {
|
|
181
|
+
const mockApiTokens = createMockApiTokenService("jnt_valid");
|
|
182
|
+
|
|
183
|
+
const app = new Hono<Env>();
|
|
184
|
+
app.onError(errorHandler);
|
|
185
|
+
app.use("*", async (c, next) => {
|
|
186
|
+
c.set("auth", createMockAuth(false));
|
|
187
|
+
c.set("services", {
|
|
188
|
+
apiTokens: mockApiTokens,
|
|
189
|
+
} as AppVariables["services"]);
|
|
190
|
+
await next();
|
|
191
|
+
});
|
|
192
|
+
app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
|
|
193
|
+
|
|
194
|
+
const res = await app.request("/api/data", {
|
|
195
|
+
headers: { Authorization: "Bearer jnt_invalid" },
|
|
196
|
+
});
|
|
197
|
+
expect(res.status).toBe(401);
|
|
198
|
+
|
|
199
|
+
expect(mockApiTokens.verify).toHaveBeenCalledWith("jnt_invalid");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("prefers session auth over Bearer token", async () => {
|
|
203
|
+
const mockApiTokens = createMockApiTokenService("jnt_valid");
|
|
204
|
+
|
|
205
|
+
const app = new Hono<Env>();
|
|
206
|
+
app.onError(errorHandler);
|
|
207
|
+
app.use("*", async (c, next) => {
|
|
208
|
+
c.set("auth", createMockAuth(true));
|
|
209
|
+
c.set("services", {
|
|
210
|
+
apiTokens: mockApiTokens,
|
|
211
|
+
} as AppVariables["services"]);
|
|
212
|
+
await next();
|
|
213
|
+
});
|
|
214
|
+
app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
|
|
215
|
+
|
|
216
|
+
const res = await app.request("/api/data", {
|
|
217
|
+
headers: { Authorization: "Bearer jnt_valid" },
|
|
218
|
+
});
|
|
219
|
+
expect(res.status).toBe(200);
|
|
220
|
+
|
|
221
|
+
// Should not check the token since session auth succeeded
|
|
222
|
+
expect(mockApiTokens.verify).not.toHaveBeenCalled();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("allows DEV_API_TOKEN on localhost", async () => {
|
|
226
|
+
const devToken = "jnt_dev_test123";
|
|
227
|
+
const mockApiTokens = createMockApiTokenService();
|
|
228
|
+
|
|
229
|
+
const app = new Hono<Env>();
|
|
230
|
+
app.onError(errorHandler);
|
|
231
|
+
app.use("*", async (c, next) => {
|
|
232
|
+
c.env = { ...c.env, DEV_API_TOKEN: devToken } as Bindings;
|
|
233
|
+
c.set("auth", createMockAuth(false));
|
|
234
|
+
c.set("services", {
|
|
235
|
+
apiTokens: mockApiTokens,
|
|
236
|
+
} as AppVariables["services"]);
|
|
237
|
+
await next();
|
|
238
|
+
});
|
|
239
|
+
app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
|
|
240
|
+
|
|
241
|
+
const res = await app.request("http://localhost:9020/api/data", {
|
|
242
|
+
headers: { Authorization: `Bearer ${devToken}` },
|
|
243
|
+
});
|
|
244
|
+
expect(res.status).toBe(200);
|
|
245
|
+
|
|
246
|
+
// Should NOT hit DB verification
|
|
247
|
+
expect(mockApiTokens.verify).not.toHaveBeenCalled();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("rejects DEV_API_TOKEN on non-local hostname", async () => {
|
|
251
|
+
const devToken = "jnt_dev_test123";
|
|
252
|
+
const mockApiTokens = createMockApiTokenService();
|
|
253
|
+
|
|
254
|
+
const app = new Hono<Env>();
|
|
255
|
+
app.onError(errorHandler);
|
|
256
|
+
app.use("*", async (c, next) => {
|
|
257
|
+
c.env = { ...c.env, DEV_API_TOKEN: devToken } as Bindings;
|
|
258
|
+
c.set("auth", createMockAuth(false));
|
|
259
|
+
c.set("services", {
|
|
260
|
+
apiTokens: mockApiTokens,
|
|
261
|
+
} as AppVariables["services"]);
|
|
262
|
+
await next();
|
|
263
|
+
});
|
|
264
|
+
app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
|
|
265
|
+
|
|
266
|
+
const res = await app.request("https://myblog.com/api/data", {
|
|
267
|
+
headers: { Authorization: `Bearer ${devToken}` },
|
|
268
|
+
});
|
|
269
|
+
expect(res.status).toBe(401);
|
|
270
|
+
|
|
271
|
+
// Falls through to normal DB verification (which also fails)
|
|
272
|
+
expect(mockApiTokens.verify).toHaveBeenCalledWith(devToken);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("allows DEV_API_TOKEN on *.localtest.me", async () => {
|
|
276
|
+
const devToken = "jnt_dev_test123";
|
|
277
|
+
const mockApiTokens = createMockApiTokenService();
|
|
278
|
+
|
|
279
|
+
const app = new Hono<Env>();
|
|
280
|
+
app.onError(errorHandler);
|
|
281
|
+
app.use("*", async (c, next) => {
|
|
282
|
+
c.env = { ...c.env, DEV_API_TOKEN: devToken } as Bindings;
|
|
283
|
+
c.set("auth", createMockAuth(false));
|
|
284
|
+
c.set("services", {
|
|
285
|
+
apiTokens: mockApiTokens,
|
|
286
|
+
} as AppVariables["services"]);
|
|
287
|
+
await next();
|
|
288
|
+
});
|
|
289
|
+
app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
|
|
290
|
+
|
|
291
|
+
const res = await app.request("https://jant.localtest.me/api/data", {
|
|
292
|
+
headers: { Authorization: `Bearer ${devToken}` },
|
|
293
|
+
});
|
|
294
|
+
expect(res.status).toBe(200);
|
|
295
|
+
expect(mockApiTokens.verify).not.toHaveBeenCalled();
|
|
296
|
+
});
|
|
117
297
|
});
|