@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.
Files changed (271) hide show
  1. package/bin/commands/export.js +1 -1
  2. package/bin/commands/import-site.js +529 -0
  3. package/bin/commands/reset-password.js +3 -2
  4. package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
  5. package/dist/client/assets/url-FWFqPJPb.js +1 -0
  6. package/dist/client/client.css +1 -1
  7. package/dist/client/client.js +4012 -3276
  8. package/dist/index.js +10285 -5809
  9. package/package.json +11 -3
  10. package/src/__tests__/helpers/app.ts +9 -9
  11. package/src/__tests__/helpers/db.ts +91 -93
  12. package/src/app.tsx +157 -27
  13. package/src/auth.ts +20 -2
  14. package/src/client/archive-nav.js +187 -0
  15. package/src/client/audio-player.ts +478 -0
  16. package/src/client/audio-processor.ts +84 -0
  17. package/src/client/avatar-upload.ts +3 -2
  18. package/src/client/components/__tests__/jant-compose-dialog.test.ts +645 -49
  19. package/src/client/components/__tests__/jant-compose-editor.test.ts +208 -16
  20. package/src/client/components/__tests__/jant-post-form.test.ts +19 -9
  21. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  22. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  23. package/src/client/components/collection-sidebar-types.ts +7 -9
  24. package/src/client/components/compose-types.ts +101 -4
  25. package/src/client/components/jant-collection-form.ts +43 -7
  26. package/src/client/components/jant-collection-sidebar.ts +88 -84
  27. package/src/client/components/jant-compose-dialog.ts +1655 -219
  28. package/src/client/components/jant-compose-editor.ts +732 -168
  29. package/src/client/components/jant-compose-fullscreen.ts +23 -78
  30. package/src/client/components/jant-media-lightbox.ts +2 -0
  31. package/src/client/components/jant-nav-manager.ts +24 -284
  32. package/src/client/components/jant-post-form.ts +89 -9
  33. package/src/client/components/jant-post-menu.ts +1019 -0
  34. package/src/client/components/jant-settings-avatar.ts +3 -3
  35. package/src/client/components/jant-settings-general.ts +38 -4
  36. package/src/client/components/jant-text-preview.ts +232 -0
  37. package/src/client/components/nav-manager-types.ts +4 -19
  38. package/src/client/components/post-form-template.ts +107 -12
  39. package/src/client/components/post-form-types.ts +11 -4
  40. package/src/client/compose-bridge.ts +410 -109
  41. package/src/client/image-processor.ts +26 -8
  42. package/src/client/media-metadata.ts +247 -0
  43. package/src/client/multipart-upload.ts +160 -0
  44. package/src/client/post-form-bridge.ts +52 -1
  45. package/src/client/settings-bridge.ts +0 -12
  46. package/src/client/thread-context.ts +140 -0
  47. package/src/client/tiptap/create-editor.ts +46 -0
  48. package/src/client/tiptap/extensions.ts +5 -0
  49. package/src/client/tiptap/image-node.ts +2 -8
  50. package/src/client/tiptap/paste-image.ts +2 -13
  51. package/src/client/tiptap/slash-commands.ts +173 -63
  52. package/src/client/toast.ts +101 -3
  53. package/src/client/types/sortablejs.d.ts +15 -0
  54. package/src/client/upload-with-metadata.ts +54 -0
  55. package/src/client/video-processor.ts +207 -0
  56. package/src/client.ts +5 -2
  57. package/src/db/__tests__/migrations.test.ts +118 -0
  58. package/src/db/index.ts +52 -0
  59. package/src/db/migrations/0000_baseline.sql +269 -0
  60. package/src/db/migrations/0001_fts_setup.sql +31 -0
  61. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  62. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  63. package/src/db/migrations/meta/_journal.json +4 -39
  64. package/src/db/schema.ts +409 -145
  65. package/src/i18n/__tests__/detect.test.ts +115 -0
  66. package/src/i18n/context.tsx +2 -2
  67. package/src/i18n/detect.ts +85 -1
  68. package/src/i18n/i18n.ts +1 -1
  69. package/src/i18n/index.ts +2 -1
  70. package/src/i18n/locales/en.po +487 -1217
  71. package/src/i18n/locales/en.ts +1 -1
  72. package/src/i18n/locales/zh-Hans.po +613 -996
  73. package/src/i18n/locales/zh-Hans.ts +1 -1
  74. package/src/i18n/locales/zh-Hant.po +624 -1007
  75. package/src/i18n/locales/zh-Hant.ts +1 -1
  76. package/src/i18n/middleware.ts +6 -0
  77. package/src/index.ts +5 -7
  78. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  79. package/src/lib/__tests__/constants.test.ts +0 -1
  80. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  81. package/src/lib/__tests__/nanoid.test.ts +26 -0
  82. package/src/lib/__tests__/schemas.test.ts +181 -63
  83. package/src/lib/__tests__/slug.test.ts +126 -0
  84. package/src/lib/__tests__/sse.test.ts +6 -6
  85. package/src/lib/__tests__/summary.test.ts +264 -0
  86. package/src/lib/__tests__/theme.test.ts +1 -1
  87. package/src/lib/__tests__/timeline.test.ts +33 -30
  88. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  89. package/src/lib/__tests__/view.test.ts +141 -66
  90. package/src/lib/blurhash-placeholder.ts +102 -0
  91. package/src/lib/constants.ts +3 -1
  92. package/src/lib/emoji-catalog.ts +885 -68
  93. package/src/lib/errors.ts +11 -8
  94. package/src/lib/feed.ts +78 -32
  95. package/src/lib/html.ts +2 -1
  96. package/src/lib/icon-catalog.ts +5033 -1
  97. package/src/lib/icons.ts +3 -2
  98. package/src/lib/index.ts +0 -1
  99. package/src/lib/markdown-to-tiptap.ts +286 -0
  100. package/src/lib/media-helpers.ts +12 -3
  101. package/src/lib/nanoid.ts +29 -0
  102. package/src/lib/navigation.ts +1 -1
  103. package/src/lib/render.tsx +20 -2
  104. package/src/lib/resolve-config.ts +6 -2
  105. package/src/lib/schemas.ts +224 -55
  106. package/src/lib/search-snippet.ts +34 -0
  107. package/src/lib/slug.ts +96 -0
  108. package/src/lib/sse.ts +6 -6
  109. package/src/lib/storage.ts +115 -7
  110. package/src/lib/summary.ts +66 -0
  111. package/src/lib/theme.ts +11 -8
  112. package/src/lib/timeline.ts +74 -34
  113. package/src/lib/tiptap-render.ts +5 -10
  114. package/src/lib/tiptap-to-markdown.ts +305 -0
  115. package/src/lib/upload.ts +190 -29
  116. package/src/lib/url.ts +31 -0
  117. package/src/lib/view.ts +204 -37
  118. package/src/middleware/__tests__/auth.test.ts +191 -11
  119. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  120. package/src/middleware/auth.ts +63 -9
  121. package/src/middleware/onboarding.ts +1 -1
  122. package/src/middleware/secure-headers.ts +40 -0
  123. package/src/preset.css +45 -2
  124. package/src/routes/__tests__/compose.test.ts +17 -24
  125. package/src/routes/api/__tests__/collections.test.ts +109 -61
  126. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  127. package/src/routes/api/__tests__/posts.test.ts +132 -68
  128. package/src/routes/api/__tests__/search.test.ts +15 -2
  129. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  130. package/src/routes/api/collections.ts +51 -42
  131. package/src/routes/api/custom-urls.ts +80 -0
  132. package/src/routes/api/export.ts +31 -0
  133. package/src/routes/api/nav-items.ts +23 -19
  134. package/src/routes/api/posts.ts +43 -39
  135. package/src/routes/api/search.ts +3 -4
  136. package/src/routes/api/upload-multipart.ts +245 -0
  137. package/src/routes/api/upload.ts +85 -19
  138. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  139. package/src/routes/auth/setup.tsx +26 -33
  140. package/src/routes/auth/signin.tsx +3 -7
  141. package/src/routes/compose.tsx +10 -55
  142. package/src/routes/dash/__tests__/settings-avatar.test.ts +1 -1
  143. package/src/routes/dash/custom-urls.tsx +414 -0
  144. package/src/routes/dash/settings.tsx +304 -232
  145. package/src/routes/feed/__tests__/rss.test.ts +27 -28
  146. package/src/routes/feed/rss.ts +6 -4
  147. package/src/routes/feed/sitemap.ts +2 -12
  148. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  149. package/src/routes/pages/__tests__/featured.test.ts +41 -22
  150. package/src/routes/pages/archive.tsx +175 -39
  151. package/src/routes/pages/collection.tsx +22 -10
  152. package/src/routes/pages/collections.tsx +3 -3
  153. package/src/routes/pages/featured.tsx +28 -4
  154. package/src/routes/pages/home.tsx +16 -15
  155. package/src/routes/pages/latest.tsx +1 -11
  156. package/src/routes/pages/new.tsx +39 -0
  157. package/src/routes/pages/page.tsx +95 -49
  158. package/src/routes/pages/search.tsx +1 -1
  159. package/src/services/__tests__/api-token.test.ts +135 -0
  160. package/src/services/__tests__/collection.test.ts +275 -227
  161. package/src/services/__tests__/custom-url.test.ts +213 -0
  162. package/src/services/__tests__/media.test.ts +162 -22
  163. package/src/services/__tests__/navigation.test.ts +109 -68
  164. package/src/services/__tests__/post-timeline.test.ts +205 -32
  165. package/src/services/__tests__/post.test.ts +713 -234
  166. package/src/services/__tests__/search.test.ts +67 -10
  167. package/src/services/api-token.ts +166 -0
  168. package/src/services/auth.ts +17 -2
  169. package/src/services/collection.ts +397 -131
  170. package/src/services/custom-url.ts +188 -0
  171. package/src/services/export.ts +802 -0
  172. package/src/services/index.ts +26 -19
  173. package/src/services/media.ts +100 -22
  174. package/src/services/navigation.ts +158 -47
  175. package/src/services/path.ts +339 -0
  176. package/src/services/post.ts +687 -154
  177. package/src/services/search.ts +160 -75
  178. package/src/styles/components.css +58 -7
  179. package/src/styles/tokens.css +84 -6
  180. package/src/styles/ui.css +2964 -457
  181. package/src/types/bindings.ts +4 -1
  182. package/src/types/config.ts +12 -4
  183. package/src/types/constants.ts +15 -3
  184. package/src/types/entities.ts +74 -35
  185. package/src/types/operations.ts +11 -24
  186. package/src/types/props.ts +51 -16
  187. package/src/types/views.ts +45 -22
  188. package/src/ui/color-themes.ts +133 -23
  189. package/src/ui/compose/ComposeDialog.tsx +239 -17
  190. package/src/ui/compose/ComposePrompt.tsx +1 -1
  191. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  192. package/src/ui/dash/ListItemRow.tsx +1 -1
  193. package/src/ui/dash/StatusBadge.tsx +3 -1
  194. package/src/ui/dash/appearance/AdvancedContent.tsx +22 -1
  195. package/src/ui/dash/appearance/ColorThemeContent.tsx +22 -2
  196. package/src/ui/dash/appearance/FontThemeContent.tsx +1 -1
  197. package/src/ui/dash/appearance/NavigationContent.tsx +5 -45
  198. package/src/ui/dash/index.ts +0 -3
  199. package/src/ui/dash/settings/AccountContent.tsx +3 -57
  200. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  201. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  202. package/src/ui/dash/settings/AvatarContent.tsx +8 -0
  203. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  204. package/src/ui/dash/settings/SettingsRootContent.tsx +45 -15
  205. package/src/ui/feed/LinkCard.tsx +89 -40
  206. package/src/ui/feed/NoteCard.tsx +39 -25
  207. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  208. package/src/ui/feed/QuoteCard.tsx +38 -23
  209. package/src/ui/feed/ThreadPreview.tsx +90 -26
  210. package/src/ui/feed/TimelineFeed.tsx +3 -2
  211. package/src/ui/feed/TimelineItem.tsx +15 -6
  212. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  213. package/src/ui/feed/thread-preview-state.ts +61 -0
  214. package/src/ui/font-themes.ts +2 -2
  215. package/src/ui/layouts/BaseLayout.tsx +2 -2
  216. package/src/ui/layouts/SiteLayout.tsx +105 -92
  217. package/src/ui/pages/ArchivePage.tsx +923 -98
  218. package/src/ui/pages/ComposePage.tsx +54 -0
  219. package/src/ui/pages/PostPage.tsx +30 -45
  220. package/src/ui/pages/SearchPage.tsx +181 -37
  221. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  222. package/src/ui/shared/CollectionsSidebar.tsx +47 -37
  223. package/src/ui/shared/MediaGallery.tsx +445 -149
  224. package/src/ui/shared/PostFooter.tsx +204 -0
  225. package/src/ui/shared/StarRating.tsx +27 -0
  226. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  227. package/src/ui/shared/index.ts +0 -1
  228. package/dist/client/assets/url-8Dj-5CLW.js +0 -1
  229. package/src/client/media-upload.ts +0 -161
  230. package/src/client/page-slug-bridge.ts +0 -42
  231. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  232. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  233. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  234. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  235. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  236. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  237. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  238. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  239. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  240. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  241. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  242. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  243. package/src/db/migrations/0012_add_tiptap_columns.sql +0 -2
  244. package/src/db/migrations/0013_replace_featured_with_visibility.sql +0 -8
  245. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  246. package/src/lib/__tests__/sqid.test.ts +0 -65
  247. package/src/lib/sqid.ts +0 -79
  248. package/src/routes/api/__tests__/pages.test.ts +0 -218
  249. package/src/routes/api/pages.ts +0 -73
  250. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  251. package/src/routes/dash/index.tsx +0 -109
  252. package/src/routes/dash/media.tsx +0 -135
  253. package/src/routes/dash/pages.tsx +0 -245
  254. package/src/routes/dash/posts.tsx +0 -338
  255. package/src/routes/dash/redirects.tsx +0 -263
  256. package/src/routes/pages/post.tsx +0 -59
  257. package/src/services/__tests__/page.test.ts +0 -298
  258. package/src/services/__tests__/path-registry.test.ts +0 -165
  259. package/src/services/__tests__/redirect.test.ts +0 -159
  260. package/src/services/page.ts +0 -216
  261. package/src/services/path-registry.ts +0 -160
  262. package/src/services/redirect.ts +0 -97
  263. package/src/ui/dash/PageForm.tsx +0 -187
  264. package/src/ui/dash/PostList.tsx +0 -95
  265. package/src/ui/dash/media/MediaListContent.tsx +0 -206
  266. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  267. package/src/ui/dash/pages/PagesContent.tsx +0 -75
  268. package/src/ui/dash/posts/PostForm.tsx +0 -260
  269. package/src/ui/layouts/DashLayout.tsx +0 -247
  270. package/src/ui/pages/SinglePage.tsx +0 -23
  271. package/src/ui/shared/ThreadView.tsx +0 -136
@@ -1,18 +1,20 @@
1
1
  /**
2
- * Dashboard Settings Routes
2
+ * Settings Routes
3
3
  *
4
4
  * Unified settings hub — root page with iOS-style grouped list,
5
5
  * plus sub-pages for General, Avatar, Navigation, Color Theme,
6
- * Font Theme, Custom CSS, and Account.
6
+ * Font Theme, Custom CSS, Account (Sessions + Password), and API Tokens.
7
7
  */
8
8
 
9
9
  import { Hono } from "hono";
10
10
  import { msg } from "@lingui/core/macro";
11
11
  import type { Bindings } from "../../types.js";
12
12
  import type { AppVariables } from "../../types/app-context.js";
13
- import { DashLayout } from "../../ui/layouts/DashLayout.js";
14
13
  import { sse, dsRedirect, dsToast } from "../../lib/sse.js";
15
14
  import { getI18n } from "../../i18n/index.js";
15
+ import { renderPublicPage } from "../../lib/render.js";
16
+ import { getNavigationData } from "../../lib/navigation.js";
17
+ import { AdminBreadcrumb } from "../../ui/shared/AdminBreadcrumb.js";
16
18
  import { TIMEZONES } from "../../lib/timezones.js";
17
19
  import { escapeHtml } from "../../lib/html.js";
18
20
  import { ValidationError } from "../../lib/errors.js";
@@ -22,11 +24,17 @@ import { BUILTIN_FONT_THEMES } from "../../ui/font-themes.js";
22
24
  import { SettingsRootContent } from "../../ui/dash/settings/SettingsRootContent.js";
23
25
  import { GeneralContent } from "../../ui/dash/settings/GeneralContent.js";
24
26
  import { AvatarContent } from "../../ui/dash/settings/AvatarContent.js";
27
+ import { AccountMenuContent } from "../../ui/dash/settings/AccountMenuContent.js";
25
28
  import { AccountContent } from "../../ui/dash/settings/AccountContent.js";
29
+ import {
30
+ SessionsContent,
31
+ type SessionInfo,
32
+ } from "../../ui/dash/settings/SessionsContent.js";
26
33
  import { NavigationContent } from "../../ui/dash/appearance/NavigationContent.js";
27
34
  import { ColorThemeContent } from "../../ui/dash/appearance/ColorThemeContent.js";
28
35
  import { FontThemeContent } from "../../ui/dash/appearance/FontThemeContent.js";
29
36
  import { AdvancedContent } from "../../ui/dash/appearance/AdvancedContent.js";
37
+ import { ApiTokensContent } from "../../ui/dash/settings/ApiTokensContent.js";
30
38
 
31
39
  type Env = { Bindings: Bindings; Variables: AppVariables };
32
40
 
@@ -37,19 +45,13 @@ export const settingsRoutes = new Hono<Env>();
37
45
  // ===========================================================================
38
46
 
39
47
  settingsRoutes.get("/", async (c) => {
40
- const siteName = c.var.appConfig.siteName;
41
-
42
- return c.html(
43
- <DashLayout
44
- c={c}
45
- title="Settings"
46
- siteName={siteName}
47
- siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
48
- currentPath="/dash/settings"
49
- >
50
- <SettingsRootContent />
51
- </DashLayout>,
52
- );
48
+ const navData = await getNavigationData(c);
49
+
50
+ return renderPublicPage(c, {
51
+ title: `Settings - ${navData.siteName}`,
52
+ navData,
53
+ content: <SettingsRootContent />,
54
+ });
53
55
  });
54
56
 
55
57
  // ===========================================================================
@@ -63,34 +65,33 @@ settingsRoutes.get("/general", async (c) => {
63
65
  const dbSiteDescription = allSettings["SITE_DESCRIPTION"] ?? "";
64
66
 
65
67
  const saved = c.req.query("saved") !== undefined;
66
-
67
- return c.html(
68
- <DashLayout
69
- c={c}
70
- title="General"
71
- siteName={dbSiteName || appConfig.fallbacks.siteName}
72
- siteAvatarUrl={appConfig.siteAvatarUrl}
73
- currentPath="/dash/settings"
74
- breadcrumb={{
75
- parent: "Settings",
76
- parentHref: "/dash/settings",
77
- current: "General",
78
- }}
79
- toast={saved ? { message: "Settings updated." } : undefined}
80
- >
81
- <GeneralContent
82
- siteName={dbSiteName || ""}
83
- siteDescription={dbSiteDescription || ""}
84
- siteLanguage={appConfig.siteLanguage}
85
- siteNameFallback={appConfig.fallbacks.siteName}
86
- siteDescriptionFallback={appConfig.fallbacks.siteDescription}
87
- timeZone={appConfig.timeZone}
88
- siteFooter={appConfig.siteFooter}
89
- noindex={appConfig.noindex}
90
- timezones={TIMEZONES}
91
- />
92
- </DashLayout>,
93
- );
68
+ const navData = await getNavigationData(c);
69
+
70
+ return renderPublicPage(c, {
71
+ title: `General - ${navData.siteName}`,
72
+ navData,
73
+ toast: saved ? { message: "Settings updated." } : undefined,
74
+ content: (
75
+ <>
76
+ <AdminBreadcrumb
77
+ parent="Settings"
78
+ parentHref="/settings"
79
+ current="General"
80
+ />
81
+ <GeneralContent
82
+ siteName={dbSiteName || ""}
83
+ siteDescription={dbSiteDescription || ""}
84
+ siteLanguage={appConfig.siteLanguage}
85
+ siteNameFallback={appConfig.fallbacks.siteName}
86
+ siteDescriptionFallback={appConfig.fallbacks.siteDescription}
87
+ timeZone={appConfig.timeZone}
88
+ siteFooter={appConfig.siteFooter}
89
+ noindex={appConfig.noindex}
90
+ timezones={TIMEZONES}
91
+ />
92
+ </>
93
+ ),
94
+ });
94
95
  });
95
96
 
96
97
  settingsRoutes.post("/general", async (c) => {
@@ -111,35 +112,28 @@ settingsRoutes.post("/general", async (c) => {
111
112
  fallbackSiteName: c.var.appConfig.fallbacks.siteName,
112
113
  });
113
114
 
115
+ // Sync user.name with site name (better-auth requires this field)
116
+ await c.var.auth.api.updateUser({
117
+ body: { name: displayName },
118
+ headers: c.req.raw.headers,
119
+ });
120
+
114
121
  // ── JSON response mode (used by Lit settings bridge) ──────────────
122
+ // Always redirect — site name appears in the header/title and a full
123
+ // reload is the simplest way to keep everything in sync.
115
124
  const wantsJson = c.req.header("accept")?.includes("application/json");
116
125
  if (wantsJson) {
117
- if (languageChanged) {
118
- return c.json({
119
- status: "redirect" as const,
120
- url: "/dash/settings/general?saved",
121
- });
122
- }
123
126
  return c.json({
124
- status: "ok" as const,
125
- toast: i18n._(
126
- msg({
127
- message: "Settings updated.",
128
- comment: "@context: Toast after saving general settings",
129
- }),
130
- ),
131
- siteName: displayName,
127
+ status: "redirect" as const,
128
+ url: "/settings/general?saved",
132
129
  });
133
130
  }
134
131
 
135
132
  return sse(c, async (stream) => {
136
133
  if (languageChanged) {
137
- await stream.redirect("/dash/settings/general?saved");
134
+ await stream.redirect("/settings/general?saved");
138
135
  } else {
139
136
  const escaped = escapeHtml(displayName);
140
- await stream.patchElements(
141
- `<a id="site-name" href="/dash" class="font-semibold">${escaped}</a>`,
142
- );
143
137
  await stream.patchElements(`General - ${escaped}`, {
144
138
  mode: "inner",
145
139
  selector: "title",
@@ -213,29 +207,27 @@ settingsRoutes.post("/general/seo", async (c) => {
213
207
  // ===========================================================================
214
208
 
215
209
  settingsRoutes.get("/avatar", async (c) => {
216
- const siteName = c.var.appConfig.siteName;
217
210
  const saved = c.req.query("saved") !== undefined;
218
-
219
- return c.html(
220
- <DashLayout
221
- c={c}
222
- title="Avatar"
223
- siteName={siteName}
224
- siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
225
- currentPath="/dash/settings"
226
- breadcrumb={{
227
- parent: "Settings",
228
- parentHref: "/dash/settings",
229
- current: "Avatar",
230
- }}
231
- toast={saved ? { message: "Avatar updated." } : undefined}
232
- >
233
- <AvatarContent
234
- siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
235
- showHeaderAvatar={c.var.appConfig.showHeaderAvatar}
236
- />
237
- </DashLayout>,
238
- );
211
+ const navData = await getNavigationData(c);
212
+
213
+ return renderPublicPage(c, {
214
+ title: `Avatar - ${navData.siteName}`,
215
+ navData,
216
+ toast: saved ? { message: "Avatar updated." } : undefined,
217
+ content: (
218
+ <>
219
+ <AdminBreadcrumb
220
+ parent="Settings"
221
+ parentHref="/settings"
222
+ current="Avatar"
223
+ />
224
+ <AvatarContent
225
+ siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
226
+ showHeaderAvatar={c.var.appConfig.showHeaderAvatar}
227
+ />
228
+ </>
229
+ ),
230
+ });
239
231
  });
240
232
 
241
233
  settingsRoutes.post("/avatar", async (c) => {
@@ -287,7 +279,7 @@ settingsRoutes.post("/avatar", async (c) => {
287
279
  },
288
280
  );
289
281
 
290
- return dsRedirect("/dash/settings/avatar?saved");
282
+ return dsRedirect("/settings/avatar?saved");
291
283
  } catch (e) {
292
284
  if (e instanceof ValidationError) {
293
285
  return dsToast(e.message, "error");
@@ -312,11 +304,11 @@ settingsRoutes.post("/avatar/remove", async (c) => {
312
304
  if (wantsJson) {
313
305
  return c.json({
314
306
  status: "redirect" as const,
315
- url: "/dash/settings/avatar?saved",
307
+ url: "/settings/avatar?saved",
316
308
  });
317
309
  }
318
310
 
319
- return dsRedirect("/dash/settings/avatar?saved");
311
+ return dsRedirect("/settings/avatar?saved");
320
312
  });
321
313
 
322
314
  settingsRoutes.post("/avatar/display", async (c) => {
@@ -365,36 +357,30 @@ settingsRoutes.post("/avatar/display", async (c) => {
365
357
  // ===========================================================================
366
358
 
367
359
  settingsRoutes.get("/navigation", async (c) => {
368
- const [navItems, availablePages] = await Promise.all([
369
- c.var.services.navItems.list(),
370
- c.var.services.pages.listNotInNav(),
371
- ]);
372
- const siteName = c.var.appConfig.siteName;
360
+ const navItems = await c.var.services.navItems.list();
373
361
  const headerNavMaxVisible = c.var.appConfig.headerNavMaxVisible;
374
362
  const homeDefaultView = c.var.appConfig.homeDefaultView;
375
-
376
- return c.html(
377
- <DashLayout
378
- c={c}
379
- title="Navigation"
380
- siteName={siteName}
381
- siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
382
- currentPath="/dash/settings"
383
- breadcrumb={{
384
- parent: "Settings",
385
- parentHref: "/dash/settings",
386
- current: "Navigation",
387
- }}
388
- >
389
- <NavigationContent
390
- navItems={navItems}
391
- availablePages={availablePages}
392
- headerNavMaxVisible={headerNavMaxVisible}
393
- homeDefaultView={homeDefaultView}
394
- siteName={siteName}
395
- />
396
- </DashLayout>,
397
- );
363
+ const navData = await getNavigationData(c);
364
+
365
+ return renderPublicPage(c, {
366
+ title: `Navigation - ${navData.siteName}`,
367
+ navData,
368
+ content: (
369
+ <>
370
+ <AdminBreadcrumb
371
+ parent="Settings"
372
+ parentHref="/settings"
373
+ current="Navigation"
374
+ />
375
+ <NavigationContent
376
+ navItems={navItems}
377
+ headerNavMaxVisible={headerNavMaxVisible}
378
+ homeDefaultView={homeDefaultView}
379
+ siteName={navData.siteName}
380
+ />
381
+ </>
382
+ ),
383
+ });
398
384
  });
399
385
 
400
386
  settingsRoutes.post("/navigation/nav-max-visible", async (c) => {
@@ -429,30 +415,28 @@ settingsRoutes.post("/navigation/home-default-view", async (c) => {
429
415
  // ===========================================================================
430
416
 
431
417
  settingsRoutes.get("/color-theme", async (c) => {
432
- const siteName = c.var.appConfig.siteName;
433
418
  const defaultThemeId = c.var.appConfig.fallbacks.defaultTheme;
434
419
  const currentThemeId =
435
420
  c.var.allSettings[SETTINGS_KEYS.THEME] ?? defaultThemeId;
436
421
  const themes = getAvailableThemes();
437
422
  const saved = c.req.query("saved") !== undefined;
438
-
439
- return c.html(
440
- <DashLayout
441
- c={c}
442
- title="Color Theme"
443
- siteName={siteName}
444
- siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
445
- currentPath="/dash/settings"
446
- breadcrumb={{
447
- parent: "Settings",
448
- parentHref: "/dash/settings",
449
- current: "Color Theme",
450
- }}
451
- toast={saved ? { message: "Theme updated." } : undefined}
452
- >
453
- <ColorThemeContent themes={themes} currentThemeId={currentThemeId} />
454
- </DashLayout>,
455
- );
423
+ const navData = await getNavigationData(c);
424
+
425
+ return renderPublicPage(c, {
426
+ title: `Color Theme - ${navData.siteName}`,
427
+ navData,
428
+ toast: saved ? { message: "Theme updated." } : undefined,
429
+ content: (
430
+ <>
431
+ <AdminBreadcrumb
432
+ parent="Settings"
433
+ parentHref="/settings"
434
+ current="Color Theme"
435
+ />
436
+ <ColorThemeContent themes={themes} currentThemeId={currentThemeId} />
437
+ </>
438
+ ),
439
+ });
456
440
  });
457
441
 
458
442
  settingsRoutes.post("/color-theme", async (c) => {
@@ -481,7 +465,7 @@ settingsRoutes.post("/color-theme", async (c) => {
481
465
  await settings.set(SETTINGS_KEYS.THEME, validTheme.id);
482
466
  }
483
467
 
484
- return dsRedirect("/dash/settings/color-theme?saved");
468
+ return dsRedirect("/settings/color-theme?saved");
485
469
  });
486
470
 
487
471
  // ===========================================================================
@@ -489,30 +473,28 @@ settingsRoutes.post("/color-theme", async (c) => {
489
473
  // ===========================================================================
490
474
 
491
475
  settingsRoutes.get("/font-theme", async (c) => {
492
- const siteName = c.var.appConfig.siteName;
493
476
  const currentFontThemeId = c.var.allSettings["FONT_THEME"] ?? "default";
494
477
  const saved = c.req.query("saved") !== undefined;
495
-
496
- return c.html(
497
- <DashLayout
498
- c={c}
499
- title="Font Theme"
500
- siteName={siteName}
501
- siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
502
- currentPath="/dash/settings"
503
- breadcrumb={{
504
- parent: "Settings",
505
- parentHref: "/dash/settings",
506
- current: "Font Theme",
507
- }}
508
- toast={saved ? { message: "Font theme updated." } : undefined}
509
- >
510
- <FontThemeContent
511
- fontThemes={BUILTIN_FONT_THEMES}
512
- currentFontThemeId={currentFontThemeId}
513
- />
514
- </DashLayout>,
515
- );
478
+ const navData = await getNavigationData(c);
479
+
480
+ return renderPublicPage(c, {
481
+ title: `Font Theme - ${navData.siteName}`,
482
+ navData,
483
+ toast: saved ? { message: "Font theme updated." } : undefined,
484
+ content: (
485
+ <>
486
+ <AdminBreadcrumb
487
+ parent="Settings"
488
+ parentHref="/settings"
489
+ current="Font Theme"
490
+ />
491
+ <FontThemeContent
492
+ fontThemes={BUILTIN_FONT_THEMES}
493
+ currentFontThemeId={currentFontThemeId}
494
+ />
495
+ </>
496
+ ),
497
+ });
516
498
  });
517
499
 
518
500
  settingsRoutes.post("/font-theme", async (c) => {
@@ -540,7 +522,7 @@ settingsRoutes.post("/font-theme", async (c) => {
540
522
  await settings.set("FONT_THEME", validFont.id);
541
523
  }
542
524
 
543
- return dsRedirect("/dash/settings/font-theme?saved");
525
+ return dsRedirect("/settings/font-theme?saved");
544
526
  });
545
527
 
546
528
  // ===========================================================================
@@ -548,25 +530,23 @@ settingsRoutes.post("/font-theme", async (c) => {
548
530
  // ===========================================================================
549
531
 
550
532
  settingsRoutes.get("/custom-css", async (c) => {
551
- const siteName = c.var.appConfig.siteName;
552
533
  const customCSS = c.var.allSettings[SETTINGS_KEYS.CUSTOM_CSS] ?? "";
553
-
554
- return c.html(
555
- <DashLayout
556
- c={c}
557
- title="Custom CSS"
558
- siteName={siteName}
559
- siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
560
- currentPath="/dash/settings"
561
- breadcrumb={{
562
- parent: "Settings",
563
- parentHref: "/dash/settings",
564
- current: "Custom CSS",
565
- }}
566
- >
567
- <AdvancedContent customCSS={customCSS} />
568
- </DashLayout>,
569
- );
534
+ const navData = await getNavigationData(c);
535
+
536
+ return renderPublicPage(c, {
537
+ title: `Custom CSS - ${navData.siteName}`,
538
+ navData,
539
+ content: (
540
+ <>
541
+ <AdminBreadcrumb
542
+ parent="Settings"
543
+ parentHref="/settings"
544
+ current="Custom CSS"
545
+ />
546
+ <AdvancedContent customCSS={customCSS} />
547
+ </>
548
+ ),
549
+ });
570
550
  });
571
551
 
572
552
  settingsRoutes.post("/custom-css", async (c) => {
@@ -593,81 +573,123 @@ settingsRoutes.post("/custom-css", async (c) => {
593
573
  });
594
574
 
595
575
  // ===========================================================================
596
- // Account
576
+ // Account sub-menu
597
577
  // ===========================================================================
598
578
 
599
579
  settingsRoutes.get("/account", async (c) => {
600
- const siteName = c.var.appConfig.siteName;
601
- const session = await c.var.auth.api.getSession({
580
+ const navData = await getNavigationData(c);
581
+
582
+ return renderPublicPage(c, {
583
+ title: `Account - ${navData.siteName}`,
584
+ navData,
585
+ content: (
586
+ <>
587
+ <AdminBreadcrumb
588
+ parent="Settings"
589
+ parentHref="/settings"
590
+ current="Account"
591
+ />
592
+ <AccountMenuContent />
593
+ </>
594
+ ),
595
+ });
596
+ });
597
+
598
+ // ===========================================================================
599
+ // Sessions
600
+ // ===========================================================================
601
+
602
+ settingsRoutes.get("/account/sessions", async (c) => {
603
+ const navData = await getNavigationData(c);
604
+
605
+ // Get current session to mark it
606
+ const currentSession = await c.var.auth.api.getSession({
607
+ headers: c.req.raw.headers,
608
+ });
609
+ const currentToken = currentSession?.session?.token ?? "";
610
+
611
+ // List all active sessions
612
+ const rawSessions = await c.var.auth.api.listSessions({
602
613
  headers: c.req.raw.headers,
603
614
  });
604
- const userName = session?.user?.name ?? "";
605
- const saved = c.req.query("saved") !== undefined;
606
615
 
607
- return c.html(
608
- <DashLayout
609
- c={c}
610
- title="Account"
611
- siteName={siteName}
612
- siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
613
- currentPath="/dash/settings"
614
- breadcrumb={{
615
- parent: "Settings",
616
- parentHref: "/dash/settings",
617
- current: "Account",
618
- }}
619
- toast={saved ? { message: "Profile updated." } : undefined}
620
- >
621
- <AccountContent userName={userName} />
622
- </DashLayout>,
616
+ const sessions: SessionInfo[] = (rawSessions ?? []).map(
617
+ (s: {
618
+ token: string;
619
+ ipAddress?: string | null;
620
+ userAgent?: string | null;
621
+ createdAt: Date;
622
+ }) => ({
623
+ token: s.token,
624
+ ipAddress: s.ipAddress ?? null,
625
+ userAgent: s.userAgent ?? null,
626
+ createdAt: Math.floor(new Date(s.createdAt).getTime() / 1000),
627
+ isCurrent: s.token === currentToken,
628
+ }),
623
629
  );
624
- });
625
630
 
626
- settingsRoutes.post("/account", async (c) => {
627
- const i18n = getI18n(c);
628
- const body = await c.req.json<{ userName: string }>();
629
- const name = body.userName?.trim();
631
+ // Sort: current session first, then by creation date descending
632
+ sessions.sort((a, b) => {
633
+ if (a.isCurrent) return -1;
634
+ if (b.isCurrent) return 1;
635
+ return b.createdAt - a.createdAt;
636
+ });
630
637
 
631
- if (!name) {
632
- return dsToast(
633
- i18n._(
634
- msg({
635
- message: "A display name is required.",
636
- comment: "@context: Error toast when display name is empty",
637
- }),
638
- ),
639
- "error",
640
- );
641
- }
638
+ return renderPublicPage(c, {
639
+ title: `Sessions - ${navData.siteName}`,
640
+ navData,
641
+ content: (
642
+ <>
643
+ <AdminBreadcrumb
644
+ parent="Account"
645
+ parentHref="/settings/account"
646
+ current="Sessions"
647
+ />
648
+ <SessionsContent sessions={sessions} />
649
+ </>
650
+ ),
651
+ });
652
+ });
653
+
654
+ settingsRoutes.post("/account/sessions/:token/revoke", async (c) => {
655
+ const token = c.req.param("token");
642
656
 
643
657
  try {
644
- await c.var.auth.api.updateUser({
645
- body: { name },
658
+ await c.var.auth.api.revokeSession({
659
+ body: { token },
646
660
  headers: c.req.raw.headers,
647
661
  });
648
662
  } catch {
649
- return dsToast(
650
- i18n._(
651
- msg({
652
- message: "Couldn't update your profile. Try again in a moment.",
653
- comment: "@context: Error toast when profile update fails",
654
- }),
655
- ),
656
- "error",
657
- );
663
+ // Session may already be expired/revoked — still redirect
658
664
  }
659
665
 
660
- return dsToast(
661
- i18n._(
662
- msg({
663
- message: "Profile updated.",
664
- comment: "@context: Toast after saving user profile",
665
- }),
666
+ return dsRedirect("/settings/account/sessions");
667
+ });
668
+
669
+ // ===========================================================================
670
+ // Password
671
+ // ===========================================================================
672
+
673
+ settingsRoutes.get("/account/password", async (c) => {
674
+ const navData = await getNavigationData(c);
675
+
676
+ return renderPublicPage(c, {
677
+ title: `Password - ${navData.siteName}`,
678
+ navData,
679
+ content: (
680
+ <>
681
+ <AdminBreadcrumb
682
+ parent="Account"
683
+ parentHref="/settings/account"
684
+ current="Password"
685
+ />
686
+ <AccountContent />
687
+ </>
666
688
  ),
667
- );
689
+ });
668
690
  });
669
691
 
670
- settingsRoutes.post("/password", async (c) => {
692
+ settingsRoutes.post("/account/password", async (c) => {
671
693
  const i18n = getI18n(c);
672
694
  const body = await c.req.json<{
673
695
  currentPassword: string;
@@ -727,3 +749,53 @@ settingsRoutes.post("/password", async (c) => {
727
749
  });
728
750
  });
729
751
  });
752
+
753
+ // ===========================================================================
754
+ // API Tokens
755
+ // ===========================================================================
756
+
757
+ settingsRoutes.get("/api-tokens", async (c) => {
758
+ const tokens = await c.var.services.apiTokens.list();
759
+ const navData = await getNavigationData(c);
760
+ const siteUrl = c.env.SITE_URL;
761
+
762
+ return renderPublicPage(c, {
763
+ title: `API Tokens - ${navData.siteName}`,
764
+ navData,
765
+ content: (
766
+ <>
767
+ <AdminBreadcrumb
768
+ parent="Settings"
769
+ parentHref="/settings"
770
+ current="API Tokens"
771
+ />
772
+ <ApiTokensContent tokens={tokens} siteUrl={siteUrl} />
773
+ </>
774
+ ),
775
+ });
776
+ });
777
+
778
+ settingsRoutes.post("/api-tokens", async (c) => {
779
+ const body = await c.req.json<{ tokenName: string }>();
780
+ const name = body.tokenName?.trim();
781
+
782
+ if (!name) {
783
+ return dsToast("Token name is required.", "error");
784
+ }
785
+
786
+ const { plaintext } = await c.var.services.apiTokens.create(name);
787
+
788
+ return sse(c, async (stream) => {
789
+ await stream.patchSignals({
790
+ _newPlaintext: plaintext,
791
+ tokenName: "",
792
+ });
793
+ });
794
+ });
795
+
796
+ settingsRoutes.post("/api-tokens/:id/delete", async (c) => {
797
+ const id = c.req.param("id");
798
+ await c.var.services.apiTokens.delete(id);
799
+
800
+ return dsRedirect("/settings/api-tokens");
801
+ });