@jant/core 0.3.35 → 0.3.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/commands/export.js +1 -1
- package/bin/commands/import-site.js +529 -0
- package/bin/commands/reset-password.js +3 -2
- package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
- package/dist/client/assets/module-RjUF93sV.js +716 -0
- package/dist/client/assets/native-48B9X9Wg.js +1 -0
- package/dist/client/assets/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4564 -3013
- package/dist/index.js +12885 -8161
- package/package.json +23 -6
- package/src/__tests__/helpers/app.ts +10 -10
- package/src/__tests__/helpers/db.ts +91 -87
- package/src/app.tsx +157 -31
- package/src/auth.ts +20 -2
- package/src/client/archive-nav.js +187 -0
- package/src/client/audio-player.ts +478 -0
- package/src/client/audio-processor.ts +84 -0
- package/src/{lib → client}/avatar-upload.ts +4 -3
- package/src/{lib → client}/collection-form-bridge.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
- package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
- package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
- package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +43 -0
- package/src/{ui → client}/components/collection-types.ts +3 -4
- package/src/client/components/compose-types.ts +174 -0
- package/src/client/components/jant-collection-form.ts +667 -0
- package/src/client/components/jant-collection-sidebar.ts +805 -0
- package/src/client/components/jant-compose-dialog.ts +2161 -0
- package/src/client/components/jant-compose-editor.ts +1813 -0
- package/src/client/components/jant-compose-fullscreen.ts +283 -0
- package/src/client/components/jant-media-lightbox.ts +259 -0
- package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
- package/src/{ui → client}/components/jant-post-form.ts +141 -12
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
- package/src/{ui → client}/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/{ui → client}/components/nav-manager-types.ts +6 -18
- package/src/{ui → client}/components/post-form-template.ts +137 -38
- package/src/{ui → client}/components/post-form-types.ts +15 -4
- package/src/client/compose-bridge.ts +583 -0
- package/src/{lib → client}/image-processor.ts +26 -8
- package/src/client/lazy-slugify.ts +51 -0
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/{lib → client}/nav-manager-bridge.ts +1 -1
- package/src/{lib → client}/post-form-bridge.ts +53 -2
- package/src/{lib → client}/settings-bridge.ts +3 -15
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/bubble-menu.ts +205 -0
- package/src/client/tiptap/create-editor.ts +86 -0
- package/src/client/tiptap/exitable-marks.ts +73 -0
- package/src/client/tiptap/extensions.ts +65 -0
- package/src/client/tiptap/image-node.ts +482 -0
- package/src/client/tiptap/link-toolbar.ts +371 -0
- package/src/client/tiptap/more-break.ts +50 -0
- package/src/client/tiptap/paste-image.ts +129 -0
- package/src/client/tiptap/slash-commands.ts +438 -0
- package/src/{lib → client}/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +44 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +27 -17
- package/src/db/__tests__/migrations.test.ts +118 -0
- package/src/db/index.ts +52 -0
- package/src/db/migrations/0000_baseline.sql +269 -0
- package/src/db/migrations/0001_fts_setup.sql +31 -0
- package/src/db/migrations/meta/0000_snapshot.json +703 -119
- package/src/db/migrations/meta/0001_snapshot.json +1337 -0
- package/src/db/migrations/meta/_journal.json +4 -39
- package/src/db/schema.ts +409 -140
- package/src/i18n/__tests__/detect.test.ts +115 -0
- package/src/i18n/context.tsx +2 -2
- package/src/i18n/detect.ts +85 -1
- package/src/i18n/i18n.ts +1 -1
- package/src/i18n/index.ts +2 -1
- package/src/i18n/locales/en.po +783 -1087
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +867 -812
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +878 -823
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +6 -0
- package/src/index.ts +5 -7
- package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
- package/src/lib/__tests__/constants.test.ts +0 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
- package/src/lib/__tests__/nanoid.test.ts +26 -0
- package/src/lib/__tests__/resolve-config.test.ts +2 -2
- package/src/lib/__tests__/schemas.test.ts +186 -65
- package/src/lib/__tests__/slug.test.ts +126 -0
- package/src/lib/__tests__/sse.test.ts +6 -6
- package/src/lib/__tests__/summary.test.ts +264 -0
- package/src/lib/__tests__/theme.test.ts +1 -1
- package/src/lib/__tests__/timeline.test.ts +33 -30
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
- package/src/lib/__tests__/url.test.ts +2 -2
- package/src/lib/__tests__/view.test.ts +140 -65
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +963 -0
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +77 -31
- package/src/lib/html.ts +2 -1
- package/src/lib/icon-catalog.ts +5033 -1
- package/src/lib/icons.ts +3 -2
- package/src/lib/index.ts +0 -1
- package/src/lib/markdown-to-tiptap.ts +286 -0
- package/src/lib/media-helpers.ts +22 -12
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +24 -5
- package/src/lib/resolve-config.ts +13 -2
- package/src/lib/schemas.ts +226 -58
- package/src/lib/search-snippet.ts +34 -0
- package/src/lib/slug.ts +96 -0
- package/src/lib/sse.ts +6 -6
- package/src/lib/storage.ts +115 -7
- package/src/lib/summary.ts +158 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +76 -34
- package/src/lib/tiptap-render.ts +191 -0
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +263 -14
- package/src/lib/url.ts +37 -22
- package/src/lib/view.ts +236 -55
- package/src/middleware/__tests__/auth.test.ts +191 -11
- package/src/middleware/__tests__/onboarding.test.ts +12 -10
- package/src/middleware/auth.ts +63 -9
- package/src/middleware/error-handler.ts +3 -3
- package/src/middleware/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +83 -2
- package/src/routes/__tests__/compose.test.ts +17 -24
- package/src/routes/api/__tests__/collections.test.ts +109 -61
- package/src/routes/api/__tests__/nav-items.test.ts +46 -29
- package/src/routes/api/__tests__/posts.test.ts +132 -68
- package/src/routes/api/__tests__/search.test.ts +15 -2
- package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
- package/src/routes/api/collections.ts +57 -31
- package/src/routes/api/custom-urls.ts +80 -0
- package/src/routes/api/export.ts +31 -0
- package/src/routes/api/nav-items.ts +23 -19
- package/src/routes/api/posts.ts +81 -62
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +92 -24
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/reset.tsx +5 -4
- package/src/routes/auth/setup.tsx +39 -31
- package/src/routes/auth/signin.tsx +13 -14
- package/src/routes/compose.tsx +27 -63
- package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +475 -99
- package/src/routes/feed/__tests__/rss.test.ts +22 -23
- package/src/routes/feed/rss.ts +6 -2
- package/src/routes/feed/sitemap.ts +2 -12
- package/src/routes/pages/__tests__/collections.test.ts +5 -6
- package/src/routes/pages/__tests__/featured.test.ts +36 -18
- package/src/routes/pages/archive.tsx +177 -37
- package/src/routes/pages/collection.tsx +43 -14
- package/src/routes/pages/collections.tsx +11 -2
- package/src/routes/pages/featured.tsx +27 -3
- package/src/routes/pages/home.tsx +15 -14
- package/src/routes/pages/latest.tsx +1 -11
- package/src/routes/pages/new.tsx +39 -0
- package/src/routes/pages/page.tsx +95 -49
- package/src/routes/pages/search.tsx +1 -1
- package/src/services/__tests__/api-token.test.ts +135 -0
- package/src/services/__tests__/collection.test.ts +275 -227
- package/src/services/__tests__/custom-url.test.ts +213 -0
- package/src/services/__tests__/media.test.ts +162 -22
- package/src/services/__tests__/navigation.test.ts +109 -68
- package/src/services/__tests__/post-timeline.test.ts +205 -32
- package/src/services/__tests__/post.test.ts +800 -230
- package/src/services/__tests__/search.test.ts +67 -10
- package/src/services/__tests__/settings.test.ts +3 -3
- package/src/services/api-token.ts +166 -0
- package/src/services/auth.ts +17 -2
- package/src/services/collection.ts +397 -131
- package/src/services/custom-url.ts +188 -0
- package/src/services/export.ts +802 -0
- package/src/services/index.ts +26 -19
- package/src/services/media.ts +100 -22
- package/src/services/navigation.ts +158 -47
- package/src/services/path.ts +339 -0
- package/src/services/post.ts +764 -172
- package/src/services/search.ts +161 -74
- package/src/services/settings.ts +6 -2
- package/src/styles/components.css +293 -62
- package/src/styles/tokens.css +93 -5
- package/src/styles/ui.css +4349 -766
- package/src/types/bindings.ts +8 -0
- package/src/types/config.ts +34 -4
- package/src/types/constants.ts +17 -2
- package/src/types/entities.ts +83 -37
- package/src/types/operations.ts +20 -27
- package/src/types/props.ts +52 -17
- package/src/types/views.ts +48 -24
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +255 -16
- package/src/ui/compose/ComposePrompt.tsx +1 -1
- package/src/ui/dash/CrudPageHeader.tsx +1 -1
- package/src/ui/dash/ListItemRow.tsx +1 -1
- package/src/ui/dash/StatusBadge.tsx +12 -2
- package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
- package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
- package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
- package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +87 -146
- package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
- package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
- package/src/ui/dash/settings/AvatarContent.tsx +78 -0
- package/src/ui/dash/settings/GeneralContent.tsx +3 -62
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
- package/src/ui/feed/LinkCard.tsx +89 -40
- package/src/ui/feed/NoteCard.tsx +39 -25
- package/src/ui/feed/PostStatusBadges.tsx +67 -0
- package/src/ui/feed/QuoteCard.tsx +38 -23
- package/src/ui/feed/ThreadPreview.tsx +90 -26
- package/src/ui/feed/TimelineFeed.tsx +3 -2
- package/src/ui/feed/TimelineItem.tsx +15 -6
- package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
- package/src/ui/feed/thread-preview-state.ts +61 -0
- package/src/ui/font-themes.ts +2 -2
- package/src/ui/layouts/BaseLayout.tsx +2 -2
- package/src/ui/layouts/SiteLayout.tsx +116 -103
- package/src/ui/pages/ArchivePage.tsx +923 -95
- package/src/ui/pages/CollectionPage.tsx +6 -35
- package/src/ui/pages/CollectionsPage.tsx +2 -1
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/FeaturedPage.tsx +2 -1
- package/src/ui/pages/HomePage.tsx +1 -1
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +182 -38
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +239 -4
- package/src/ui/shared/MediaGallery.tsx +475 -41
- package/src/ui/shared/PostFooter.tsx +204 -0
- package/src/ui/shared/StarRating.tsx +27 -0
- package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
- package/src/ui/shared/index.ts +0 -1
- package/src/db/migrations/0000_square_wallflower.sql +0 -118
- package/src/db/migrations/0001_add_search_fts.sql +0 -34
- package/src/db/migrations/0002_add_media_attachments.sql +0 -3
- package/src/db/migrations/0003_add_navigation_links.sql +0 -8
- package/src/db/migrations/0004_add_storage_provider.sql +0 -3
- package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
- package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
- package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
- package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
- package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
- package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
- package/src/db/migrations/0011_add_path_registry.sql +0 -23
- package/src/db/migrations/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- package/src/lib/collections-reorder.ts +0 -28
- package/src/lib/compose-bridge.ts +0 -280
- package/src/lib/media-upload.ts +0 -148
- package/src/lib/sqid.ts +0 -79
- package/src/routes/api/__tests__/pages.test.ts +0 -218
- package/src/routes/api/pages.ts +0 -73
- package/src/routes/dash/__tests__/pages.test.ts +0 -226
- package/src/routes/dash/appearance.tsx +0 -240
- package/src/routes/dash/collections.tsx +0 -211
- package/src/routes/dash/index.tsx +0 -103
- package/src/routes/dash/media.tsx +0 -132
- package/src/routes/dash/pages.tsx +0 -239
- package/src/routes/dash/posts.tsx +0 -334
- package/src/routes/dash/redirects.tsx +0 -257
- package/src/routes/pages/post.tsx +0 -59
- package/src/services/__tests__/page.test.ts +0 -298
- package/src/services/__tests__/path-registry.test.ts +0 -165
- package/src/services/__tests__/redirect.test.ts +0 -159
- package/src/services/page.ts +0 -203
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/types/sortablejs.d.ts +0 -29
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
- package/src/ui/components/compose-types.ts +0 -75
- package/src/ui/components/jant-collection-form.ts +0 -512
- package/src/ui/components/jant-compose-dialog.ts +0 -495
- package/src/ui/components/jant-compose-editor.ts +0 -814
- package/src/ui/dash/PageForm.tsx +0 -185
- package/src/ui/dash/PostList.tsx +0 -95
- package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
- package/src/ui/dash/collections/CollectionForm.tsx +0 -166
- package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
- package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
- package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
- package/src/ui/dash/media/MediaListContent.tsx +0 -201
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -74
- package/src/ui/dash/posts/PostForm.tsx +0 -248
- package/src/ui/dash/settings/SettingsNav.tsx +0 -52
- package/src/ui/layouts/DashLayout.tsx +0 -165
- package/src/ui/pages/SinglePage.tsx +0 -23
- package/src/ui/shared/ThreadView.tsx +0 -136
- /package/src/{ui → client}/components/settings-types.ts +0 -0
|
@@ -10,8 +10,8 @@ import { getNavigationData } from "../../lib/navigation.js";
|
|
|
10
10
|
import { renderPublicPage } from "../../lib/render.js";
|
|
11
11
|
import {
|
|
12
12
|
createMediaContext,
|
|
13
|
-
toPostViewsFromPosts,
|
|
14
13
|
toPostViews,
|
|
14
|
+
loadThreadRootPermalinks,
|
|
15
15
|
} from "../../lib/view.js";
|
|
16
16
|
import { defaultRssRenderer } from "../../lib/feed.js";
|
|
17
17
|
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
@@ -27,35 +27,63 @@ collectionRoutes.get("/:slug", async (c) => {
|
|
|
27
27
|
const collection = await c.var.services.collections.getBySlug(slug);
|
|
28
28
|
if (!collection) return c.notFound();
|
|
29
29
|
|
|
30
|
-
// Fetch posts
|
|
31
|
-
const
|
|
30
|
+
// Fetch posts, all collections, sidebar items, and post counts in parallel
|
|
31
|
+
const navData = await getNavigationData(c);
|
|
32
|
+
|
|
33
|
+
const [posts, allCollections, sidebarItems, postCounts] = await Promise.all([
|
|
32
34
|
c.var.services.posts.list({
|
|
33
35
|
collectionId: collection.id,
|
|
34
36
|
status: "published",
|
|
35
|
-
|
|
37
|
+
excludePrivate: !navData.isAuthenticated,
|
|
36
38
|
}),
|
|
37
39
|
c.var.services.collections.list(),
|
|
40
|
+
c.var.services.collections.listSidebarItems(),
|
|
41
|
+
c.var.services.collections.getPostCounts(),
|
|
38
42
|
]);
|
|
39
43
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
// Batch-load media and thread root permalinks in parallel
|
|
45
|
+
const postIds = posts.map((p) => p.id);
|
|
46
|
+
const [rawMediaMap, rootPermalinkMap] = await Promise.all([
|
|
47
|
+
c.var.services.media.getByPostIds(postIds),
|
|
48
|
+
loadThreadRootPermalinks(
|
|
49
|
+
posts,
|
|
50
|
+
c.var.services.posts.getById.bind(c.var.services.posts),
|
|
51
|
+
),
|
|
52
|
+
]);
|
|
43
53
|
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
44
|
-
const
|
|
54
|
+
const mediaMap = buildMediaMap(
|
|
55
|
+
rawMediaMap,
|
|
56
|
+
mediaCtx.r2PublicUrl,
|
|
57
|
+
mediaCtx.imageTransformUrl,
|
|
58
|
+
mediaCtx.s3PublicUrl,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const postViews = toPostViews(
|
|
62
|
+
posts.map((p) => ({
|
|
63
|
+
...p,
|
|
64
|
+
mediaAttachments: mediaMap.get(p.id) ?? [],
|
|
65
|
+
})),
|
|
66
|
+
mediaCtx,
|
|
67
|
+
rootPermalinkMap,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const items = postViews.map((post) => ({ post }));
|
|
45
71
|
|
|
46
72
|
return renderPublicPage(c, {
|
|
47
73
|
title: `${collection.title} - ${navData.siteName}`,
|
|
48
74
|
description: collection.description ?? undefined,
|
|
49
75
|
navData,
|
|
50
76
|
sidebar: (
|
|
51
|
-
<CollectionsSidebar
|
|
77
|
+
<CollectionsSidebar
|
|
78
|
+
collections={allCollections}
|
|
79
|
+
sidebarItems={sidebarItems}
|
|
80
|
+
activeSlug={slug}
|
|
81
|
+
isAuthenticated={navData.isAuthenticated}
|
|
82
|
+
postCounts={postCounts}
|
|
83
|
+
/>
|
|
52
84
|
),
|
|
53
85
|
content: (
|
|
54
|
-
<CollectionPage
|
|
55
|
-
collection={collection}
|
|
56
|
-
posts={postViews}
|
|
57
|
-
hasMore={false}
|
|
58
|
-
/>
|
|
86
|
+
<CollectionPage collection={collection} items={items} hasMore={false} />
|
|
59
87
|
),
|
|
60
88
|
});
|
|
61
89
|
});
|
|
@@ -77,6 +105,7 @@ collectionRoutes.get("/:slug/feed", async (c) => {
|
|
|
77
105
|
collectionId: collection.id,
|
|
78
106
|
status: "published",
|
|
79
107
|
excludeReplies: true,
|
|
108
|
+
excludePrivate: true,
|
|
80
109
|
limit: feedLimit,
|
|
81
110
|
});
|
|
82
111
|
|
|
@@ -17,8 +17,9 @@ type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
|
17
17
|
export const collectionsPageRoutes = new Hono<Env>();
|
|
18
18
|
|
|
19
19
|
collectionsPageRoutes.get("/", async (c) => {
|
|
20
|
-
const [allCollections, postCounts] = await Promise.all([
|
|
20
|
+
const [allCollections, sidebarItems, postCounts] = await Promise.all([
|
|
21
21
|
c.var.services.collections.list(),
|
|
22
|
+
c.var.services.collections.listSidebarItems(),
|
|
22
23
|
c.var.services.collections.getPostCounts(),
|
|
23
24
|
]);
|
|
24
25
|
|
|
@@ -32,7 +33,15 @@ collectionsPageRoutes.get("/", async (c) => {
|
|
|
32
33
|
return renderPublicPage(c, {
|
|
33
34
|
title: `Collections - ${navData.siteName}`,
|
|
34
35
|
navData,
|
|
35
|
-
sidebar:
|
|
36
|
+
sidebar: (
|
|
37
|
+
<CollectionsSidebar
|
|
38
|
+
collections={allCollections}
|
|
39
|
+
sidebarItems={sidebarItems}
|
|
40
|
+
activeSlug={undefined}
|
|
41
|
+
isAuthenticated={navData.isAuthenticated}
|
|
42
|
+
postCounts={postCounts}
|
|
43
|
+
/>
|
|
44
|
+
),
|
|
36
45
|
content: <CollectionsPage collections={collections} />,
|
|
37
46
|
});
|
|
38
47
|
});
|
|
@@ -9,7 +9,11 @@ import type { Bindings } from "../../types.js";
|
|
|
9
9
|
import type { AppVariables } from "../../types/app-context.js";
|
|
10
10
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
11
11
|
import { renderPublicPage } from "../../lib/render.js";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
createMediaContext,
|
|
14
|
+
toPostViewsFromPosts,
|
|
15
|
+
loadThreadRootPermalinks,
|
|
16
|
+
} from "../../lib/view.js";
|
|
13
17
|
import { FeaturedPage } from "../../ui/pages/FeaturedPage.js";
|
|
14
18
|
|
|
15
19
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
@@ -27,11 +31,31 @@ featuredRoutes.get("/", async (c) => {
|
|
|
27
31
|
const posts = await c.var.services.posts.list({
|
|
28
32
|
featured: true,
|
|
29
33
|
status: "published",
|
|
30
|
-
|
|
34
|
+
excludePrivate: !navData.isAuthenticated,
|
|
31
35
|
});
|
|
32
36
|
|
|
33
37
|
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
34
|
-
|
|
38
|
+
|
|
39
|
+
const rootPermalinkMap = await loadThreadRootPermalinks(
|
|
40
|
+
posts,
|
|
41
|
+
c.var.services.posts.getById.bind(c.var.services.posts),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Determine which posts are last in their thread for reply button visibility
|
|
45
|
+
const threadIds = [...new Set(posts.map((p) => p.threadId))];
|
|
46
|
+
const lastPostMap =
|
|
47
|
+
await c.var.services.posts.getLastPostIdsByThread(threadIds);
|
|
48
|
+
const isLastInThreadMap = new Map<string, boolean>();
|
|
49
|
+
for (const p of posts) {
|
|
50
|
+
isLastInThreadMap.set(p.id, lastPostMap.get(p.threadId) === p.id);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const postViews = toPostViewsFromPosts(
|
|
54
|
+
posts,
|
|
55
|
+
mediaCtx,
|
|
56
|
+
rootPermalinkMap,
|
|
57
|
+
isLastInThreadMap,
|
|
58
|
+
);
|
|
35
59
|
|
|
36
60
|
// Convert to timeline items (simple — no thread previews)
|
|
37
61
|
const items = postViews.map((post) => ({ post }));
|
|
@@ -14,7 +14,11 @@ import type { AppVariables } from "../../types/app-context.js";
|
|
|
14
14
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
15
15
|
import { renderPublicPage } from "../../lib/render.js";
|
|
16
16
|
import { assembleTimeline } from "../../lib/timeline.js";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
createMediaContext,
|
|
19
|
+
toPostViewsFromPosts,
|
|
20
|
+
loadThreadRootPermalinks,
|
|
21
|
+
} from "../../lib/view.js";
|
|
18
22
|
import { HomePage } from "../../ui/pages/HomePage.js";
|
|
19
23
|
import { FeaturedPage } from "../../ui/pages/FeaturedPage.js";
|
|
20
24
|
|
|
@@ -30,10 +34,16 @@ homeRoutes.get("/", async (c) => {
|
|
|
30
34
|
const posts = await c.var.services.posts.list({
|
|
31
35
|
featured: true,
|
|
32
36
|
status: "published",
|
|
33
|
-
|
|
37
|
+
excludePrivate: !navData.isAuthenticated,
|
|
34
38
|
});
|
|
35
39
|
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
36
|
-
|
|
40
|
+
|
|
41
|
+
const rootPermalinkMap = await loadThreadRootPermalinks(
|
|
42
|
+
posts,
|
|
43
|
+
c.var.services.posts.getById.bind(c.var.services.posts),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const postViews = toPostViewsFromPosts(posts, mediaCtx, rootPermalinkMap);
|
|
37
47
|
const items = postViews.map((post) => ({ post }));
|
|
38
48
|
|
|
39
49
|
return renderPublicPage(c, {
|
|
@@ -43,30 +53,21 @@ homeRoutes.get("/", async (c) => {
|
|
|
43
53
|
});
|
|
44
54
|
}
|
|
45
55
|
|
|
46
|
-
// Default: show latest posts
|
|
56
|
+
// Default: show latest posts (pinned posts sort to top via service layer)
|
|
47
57
|
const pageParam = c.req.query("page");
|
|
48
58
|
const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
|
|
49
59
|
|
|
50
60
|
const { items, currentPage, totalPages } = await assembleTimeline(c, {
|
|
51
61
|
page,
|
|
62
|
+
isAuthenticated: navData.isAuthenticated,
|
|
52
63
|
});
|
|
53
64
|
|
|
54
|
-
// Fetch pinned posts
|
|
55
|
-
const pinnedPosts = await c.var.services.posts.list({
|
|
56
|
-
pinned: true,
|
|
57
|
-
status: "published",
|
|
58
|
-
excludeReplies: true,
|
|
59
|
-
});
|
|
60
|
-
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
61
|
-
const pinnedItems = toPostViewsFromPosts(pinnedPosts, mediaCtx);
|
|
62
|
-
|
|
63
65
|
return renderPublicPage(c, {
|
|
64
66
|
title: navData.siteName,
|
|
65
67
|
navData,
|
|
66
68
|
content: (
|
|
67
69
|
<HomePage
|
|
68
70
|
items={items}
|
|
69
|
-
pinnedItems={pinnedItems}
|
|
70
71
|
currentPage={currentPage}
|
|
71
72
|
totalPages={totalPages}
|
|
72
73
|
/>
|
|
@@ -13,7 +13,6 @@ import type { AppVariables } from "../../types/app-context.js";
|
|
|
13
13
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
14
14
|
import { renderPublicPage } from "../../lib/render.js";
|
|
15
15
|
import { assembleTimeline } from "../../lib/timeline.js";
|
|
16
|
-
import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
|
|
17
16
|
import { HomePage } from "../../ui/pages/HomePage.js";
|
|
18
17
|
|
|
19
18
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
@@ -33,24 +32,15 @@ latestRoutes.get("/", async (c) => {
|
|
|
33
32
|
|
|
34
33
|
const { items, currentPage, totalPages } = await assembleTimeline(c, {
|
|
35
34
|
page,
|
|
35
|
+
isAuthenticated: navData.isAuthenticated,
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
// Fetch pinned posts
|
|
39
|
-
const pinnedPosts = await c.var.services.posts.list({
|
|
40
|
-
pinned: true,
|
|
41
|
-
status: "published",
|
|
42
|
-
excludeReplies: true,
|
|
43
|
-
});
|
|
44
|
-
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
45
|
-
const pinnedItems = toPostViewsFromPosts(pinnedPosts, mediaCtx);
|
|
46
|
-
|
|
47
38
|
return renderPublicPage(c, {
|
|
48
39
|
title: `Latest - ${navData.siteName}`,
|
|
49
40
|
navData,
|
|
50
41
|
content: (
|
|
51
42
|
<HomePage
|
|
52
43
|
items={items}
|
|
53
|
-
pinnedItems={pinnedItems}
|
|
54
44
|
currentPage={currentPage}
|
|
55
45
|
totalPages={totalPages}
|
|
56
46
|
/>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { msg } from "@lingui/core/macro";
|
|
3
|
+
import type { Bindings } from "../../types.js";
|
|
4
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
5
|
+
import { requireAuth } from "../../middleware/auth.js";
|
|
6
|
+
import { getNavigationData } from "../../lib/navigation.js";
|
|
7
|
+
import { renderPublicPage } from "../../lib/render.js";
|
|
8
|
+
import { getI18n } from "../../i18n/index.js";
|
|
9
|
+
import { ComposePage } from "../../ui/pages/ComposePage.js";
|
|
10
|
+
|
|
11
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
12
|
+
|
|
13
|
+
export const newPostRoutes = new Hono<Env>();
|
|
14
|
+
|
|
15
|
+
newPostRoutes.use("/new", requireAuth());
|
|
16
|
+
|
|
17
|
+
newPostRoutes.get("/new", async (c) => {
|
|
18
|
+
const navData = await getNavigationData(c);
|
|
19
|
+
const i18n = getI18n(c);
|
|
20
|
+
|
|
21
|
+
return renderPublicPage(c, {
|
|
22
|
+
title: `${i18n._(
|
|
23
|
+
msg({
|
|
24
|
+
message: "New post",
|
|
25
|
+
comment: "@context: Browser page title for the new post page",
|
|
26
|
+
}),
|
|
27
|
+
)} - ${navData.siteName}`,
|
|
28
|
+
navData,
|
|
29
|
+
showComposeDialog: false,
|
|
30
|
+
showHeader: false,
|
|
31
|
+
content: (
|
|
32
|
+
<ComposePage
|
|
33
|
+
collections={navData.collections}
|
|
34
|
+
uploadMaxFileSize={c.var.appConfig.uploadMaxFileSize}
|
|
35
|
+
closeHref="/"
|
|
36
|
+
/>
|
|
37
|
+
),
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -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
|
+
});
|