@jant/core 0.3.36 → 0.3.38
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
|
@@ -1,80 +1,126 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Catch-all Route
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* The path registry eliminates ambiguity: each path maps to exactly
|
|
7
|
-
* one entity (page, post, or redirect).
|
|
4
|
+
* Resolves post slugs, aliases, redirects, and collection aliases.
|
|
5
|
+
* Must be registered last.
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
|
-
import { Hono } from "hono";
|
|
8
|
+
import { Hono, type Context } from "hono";
|
|
11
9
|
import type { Bindings } from "../../types.js";
|
|
12
10
|
import type { AppVariables } from "../../types/app-context.js";
|
|
13
|
-
import { SinglePage } from "../../ui/pages/SinglePage.js";
|
|
14
11
|
import { PostPage } from "../../ui/pages/PostPage.js";
|
|
15
12
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
16
13
|
import { renderPublicPage } from "../../lib/render.js";
|
|
17
14
|
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
18
|
-
import { createMediaContext,
|
|
15
|
+
import { createMediaContext, toPostView } from "../../lib/view.js";
|
|
16
|
+
import type { Post } from "../../types.js";
|
|
19
17
|
|
|
20
18
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
21
19
|
|
|
22
20
|
export const pageRoutes = new Hono<Env>();
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
async function renderPost(c: Context<Env>, post: Post) {
|
|
23
|
+
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
24
|
+
|
|
25
|
+
// Load the full thread if this post is part of one
|
|
26
|
+
const threadRootId = post.threadId;
|
|
27
|
+
const threadPosts = (
|
|
28
|
+
await c.var.services.posts.getThread(threadRootId)
|
|
29
|
+
).filter((threadPost) => threadPost.status === "published");
|
|
30
|
+
|
|
31
|
+
// Batch load media for all thread posts (or just this post if solo)
|
|
32
|
+
const allPostIds =
|
|
33
|
+
threadPosts.length > 1 ? threadPosts.map((p) => p.id) : [post.id];
|
|
34
|
+
const rawMediaMap = await c.var.services.media.getByPostIds(allPostIds);
|
|
35
|
+
const mediaMap = buildMediaMap(
|
|
36
|
+
rawMediaMap,
|
|
37
|
+
mediaCtx.r2PublicUrl,
|
|
38
|
+
mediaCtx.imageTransformUrl,
|
|
39
|
+
mediaCtx.s3PublicUrl,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Batch load collections for all posts
|
|
43
|
+
const collectionsMap =
|
|
44
|
+
await c.var.services.collections.getCollectionsByPostIds(allPostIds);
|
|
45
|
+
|
|
46
|
+
const postView = toPostView(
|
|
47
|
+
{ ...post, mediaAttachments: mediaMap.get(post.id) ?? [] },
|
|
48
|
+
mediaCtx,
|
|
49
|
+
collectionsMap.get(post.id),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Build thread post views if this is a multi-post thread
|
|
53
|
+
const threadPostViews =
|
|
54
|
+
threadPosts.length > 1
|
|
55
|
+
? threadPosts.map((tp, i) =>
|
|
56
|
+
toPostView(
|
|
57
|
+
{ ...tp, mediaAttachments: mediaMap.get(tp.id) ?? [] },
|
|
58
|
+
mediaCtx,
|
|
59
|
+
collectionsMap.get(tp.id),
|
|
60
|
+
undefined,
|
|
61
|
+
i === threadPosts.length - 1,
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
: undefined;
|
|
65
|
+
|
|
66
|
+
const navData = await getNavigationData(c);
|
|
67
|
+
const title = post.title || navData.siteName;
|
|
68
|
+
|
|
69
|
+
return renderPublicPage(c, {
|
|
70
|
+
title,
|
|
71
|
+
description: post.body?.slice(0, 160),
|
|
72
|
+
navData,
|
|
73
|
+
content: <PostPage post={postView} threadPosts={threadPostViews} />,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Catch-all for path-registry backed post URLs, aliases, and redirects
|
|
25
78
|
pageRoutes.get("/*", async (c) => {
|
|
26
79
|
const fullPath = c.req.path.slice(1); // Remove leading /
|
|
27
80
|
if (!fullPath) return c.notFound();
|
|
28
81
|
|
|
29
|
-
const
|
|
82
|
+
const resolved = await c.var.services.paths.resolve(fullPath);
|
|
83
|
+
if (!resolved) return c.notFound();
|
|
30
84
|
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
85
|
+
if (resolved.kind === "redirect" && resolved.redirectToPath) {
|
|
86
|
+
return c.redirect(
|
|
87
|
+
`/${resolved.redirectToPath}`,
|
|
88
|
+
resolved.redirectType ?? 301,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
36
91
|
|
|
37
|
-
|
|
38
|
-
const
|
|
92
|
+
if (resolved.postId) {
|
|
93
|
+
const post = await c.var.services.posts.getById(resolved.postId);
|
|
94
|
+
if (!post || post.status === "draft") return c.notFound();
|
|
39
95
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
content: <SinglePage page={pageView} />,
|
|
45
|
-
});
|
|
46
|
-
}
|
|
96
|
+
if (post.visibility === "private") {
|
|
97
|
+
const navData = await getNavigationData(c);
|
|
98
|
+
if (!navData.isAuthenticated) return c.notFound();
|
|
99
|
+
}
|
|
47
100
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
101
|
+
// If accessed via slug but an alias exists, redirect to the alias
|
|
102
|
+
if (resolved.kind === "slug") {
|
|
103
|
+
const alias = await c.var.services.customUrls.getByTarget(
|
|
104
|
+
"post",
|
|
105
|
+
post.id,
|
|
106
|
+
);
|
|
107
|
+
if (alias) {
|
|
108
|
+
return c.redirect(`/${alias.path}`, 301);
|
|
109
|
+
}
|
|
52
110
|
}
|
|
53
111
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
57
|
-
const mediaMap = buildMediaMap(
|
|
58
|
-
rawMediaMap,
|
|
59
|
-
mediaCtx.r2PublicUrl,
|
|
60
|
-
mediaCtx.imageTransformUrl,
|
|
61
|
-
mediaCtx.s3PublicUrl,
|
|
62
|
-
);
|
|
112
|
+
return renderPost(c, post);
|
|
113
|
+
}
|
|
63
114
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
115
|
+
if (resolved.collectionId) {
|
|
116
|
+
const collection = await c.var.services.collections.getById(
|
|
117
|
+
resolved.collectionId,
|
|
67
118
|
);
|
|
119
|
+
if (!collection) return c.notFound();
|
|
68
120
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return renderPublicPage(c, {
|
|
73
|
-
title,
|
|
74
|
-
description: post.body?.slice(0, 160),
|
|
75
|
-
navData,
|
|
76
|
-
content: <PostPage post={postView} />,
|
|
77
|
-
});
|
|
121
|
+
if (resolved.kind === "alias") {
|
|
122
|
+
return c.redirect(`/c/${collection.slug}`, 301);
|
|
123
|
+
}
|
|
78
124
|
}
|
|
79
125
|
|
|
80
126
|
return c.notFound();
|
|
@@ -50,7 +50,7 @@ searchRoutes.get("/", async (c) => {
|
|
|
50
50
|
|
|
51
51
|
// Transform to View Models
|
|
52
52
|
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
53
|
-
const resultViews = toSearchResultViews(results, mediaCtx);
|
|
53
|
+
const resultViews = toSearchResultViews(results, mediaCtx, query);
|
|
54
54
|
|
|
55
55
|
return renderPublicPage(c, {
|
|
56
56
|
title: query
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { createTestDatabase } from "../../__tests__/helpers/db.js";
|
|
3
|
+
import { createApiTokenService } from "../api-token.js";
|
|
4
|
+
import type { Database } from "../../db/index.js";
|
|
5
|
+
|
|
6
|
+
describe("ApiTokenService", () => {
|
|
7
|
+
let db: Database;
|
|
8
|
+
let service: ReturnType<typeof createApiTokenService>;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
const testDb = createTestDatabase();
|
|
12
|
+
db = testDb.db as unknown as Database;
|
|
13
|
+
service = createApiTokenService(db);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("create", () => {
|
|
17
|
+
it("returns a token with jnt_ prefix and metadata", async () => {
|
|
18
|
+
const { token, plaintext } = await service.create("Test Token");
|
|
19
|
+
|
|
20
|
+
expect(plaintext).toMatch(/^jnt_[0-9a-f]{64}$/);
|
|
21
|
+
expect(token.name).toBe("Test Token");
|
|
22
|
+
expect(token.prefix).toHaveLength(8);
|
|
23
|
+
expect(token.lastUsedAt).toBeNull();
|
|
24
|
+
expect(token.createdAt).toBeGreaterThan(0);
|
|
25
|
+
expect(token.updatedAt).toBeGreaterThan(0);
|
|
26
|
+
expect(token.id).toBeTruthy();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("does not expose tokenHash in the returned entity", async () => {
|
|
30
|
+
const { token } = await service.create("Test");
|
|
31
|
+
|
|
32
|
+
expect(token).not.toHaveProperty("tokenHash");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("generates unique tokens each time", async () => {
|
|
36
|
+
const { plaintext: t1 } = await service.create("Token 1");
|
|
37
|
+
const { plaintext: t2 } = await service.create("Token 2");
|
|
38
|
+
|
|
39
|
+
expect(t1).not.toBe(t2);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("list", () => {
|
|
44
|
+
it("returns empty array when no tokens exist", async () => {
|
|
45
|
+
const tokens = await service.list();
|
|
46
|
+
expect(tokens).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("returns all created tokens", async () => {
|
|
50
|
+
await service.create("Token A");
|
|
51
|
+
await service.create("Token B");
|
|
52
|
+
|
|
53
|
+
const tokens = await service.list();
|
|
54
|
+
expect(tokens).toHaveLength(2);
|
|
55
|
+
expect(tokens[0]?.name).toBe("Token A");
|
|
56
|
+
expect(tokens[1]?.name).toBe("Token B");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("does not include tokenHash in listed tokens", async () => {
|
|
60
|
+
await service.create("Token");
|
|
61
|
+
|
|
62
|
+
const tokens = await service.list();
|
|
63
|
+
expect(tokens[0]).not.toHaveProperty("tokenHash");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("verify", () => {
|
|
68
|
+
it("returns token ID for valid token", async () => {
|
|
69
|
+
const { token, plaintext } = await service.create("Test");
|
|
70
|
+
|
|
71
|
+
const result = await service.verify(plaintext);
|
|
72
|
+
expect(result).toBe(token.id);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("returns null for invalid token", async () => {
|
|
76
|
+
await service.create("Test");
|
|
77
|
+
|
|
78
|
+
const result = await service.verify("jnt_" + "0".repeat(64));
|
|
79
|
+
expect(result).toBeNull();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("returns null for token without jnt_ prefix", async () => {
|
|
83
|
+
const result = await service.verify("invalid_token");
|
|
84
|
+
expect(result).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("returns null for empty string", async () => {
|
|
88
|
+
const result = await service.verify("");
|
|
89
|
+
expect(result).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("delete", () => {
|
|
94
|
+
it("returns true when token exists", async () => {
|
|
95
|
+
const { token } = await service.create("Test");
|
|
96
|
+
|
|
97
|
+
const result = await service.delete(token.id);
|
|
98
|
+
expect(result).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("returns false when token does not exist", async () => {
|
|
102
|
+
const result = await service.delete("nonexistent-id");
|
|
103
|
+
expect(result).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("prevents verification of deleted token", async () => {
|
|
107
|
+
const { token, plaintext } = await service.create("Test");
|
|
108
|
+
await service.delete(token.id);
|
|
109
|
+
|
|
110
|
+
const result = await service.verify(plaintext);
|
|
111
|
+
expect(result).toBeNull();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("removes token from list", async () => {
|
|
115
|
+
const { token } = await service.create("Test");
|
|
116
|
+
await service.delete(token.id);
|
|
117
|
+
|
|
118
|
+
const tokens = await service.list();
|
|
119
|
+
expect(tokens).toHaveLength(0);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("updateLastUsed", () => {
|
|
124
|
+
it("updates the lastUsedAt timestamp", async () => {
|
|
125
|
+
const { token } = await service.create("Test");
|
|
126
|
+
expect(token.lastUsedAt).toBeNull();
|
|
127
|
+
|
|
128
|
+
await service.updateLastUsed(token.id);
|
|
129
|
+
|
|
130
|
+
const tokens = await service.list();
|
|
131
|
+
const updated = tokens.find((t) => t.id === token.id);
|
|
132
|
+
expect(updated?.lastUsedAt).toBeGreaterThan(0);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|