@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.
Files changed (307) 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/module-RjUF93sV.js +716 -0
  6. package/dist/client/assets/native-48B9X9Wg.js +1 -0
  7. package/dist/client/assets/url-FWFqPJPb.js +1 -0
  8. package/dist/client/client.css +1 -1
  9. package/dist/client/client.js +4564 -3013
  10. package/dist/index.js +12885 -8161
  11. package/package.json +23 -6
  12. package/src/__tests__/helpers/app.ts +10 -10
  13. package/src/__tests__/helpers/db.ts +91 -87
  14. package/src/app.tsx +157 -31
  15. package/src/auth.ts +20 -2
  16. package/src/client/archive-nav.js +187 -0
  17. package/src/client/audio-player.ts +478 -0
  18. package/src/client/audio-processor.ts +84 -0
  19. package/src/{lib → client}/avatar-upload.ts +4 -3
  20. package/src/{lib → client}/collection-form-bridge.ts +2 -2
  21. package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
  22. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
  23. package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
  24. package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
  25. package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
  26. package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
  27. package/src/client/components/collection-sidebar-types.ts +43 -0
  28. package/src/{ui → client}/components/collection-types.ts +3 -4
  29. package/src/client/components/compose-types.ts +174 -0
  30. package/src/client/components/jant-collection-form.ts +667 -0
  31. package/src/client/components/jant-collection-sidebar.ts +805 -0
  32. package/src/client/components/jant-compose-dialog.ts +2161 -0
  33. package/src/client/components/jant-compose-editor.ts +1813 -0
  34. package/src/client/components/jant-compose-fullscreen.ts +283 -0
  35. package/src/client/components/jant-media-lightbox.ts +259 -0
  36. package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
  37. package/src/{ui → client}/components/jant-post-form.ts +141 -12
  38. package/src/client/components/jant-post-menu.ts +1019 -0
  39. package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
  40. package/src/{ui → client}/components/jant-settings-general.ts +38 -4
  41. package/src/client/components/jant-text-preview.ts +232 -0
  42. package/src/{ui → client}/components/nav-manager-types.ts +6 -18
  43. package/src/{ui → client}/components/post-form-template.ts +137 -38
  44. package/src/{ui → client}/components/post-form-types.ts +15 -4
  45. package/src/client/compose-bridge.ts +583 -0
  46. package/src/{lib → client}/image-processor.ts +26 -8
  47. package/src/client/lazy-slugify.ts +51 -0
  48. package/src/client/media-metadata.ts +247 -0
  49. package/src/client/multipart-upload.ts +160 -0
  50. package/src/{lib → client}/nav-manager-bridge.ts +1 -1
  51. package/src/{lib → client}/post-form-bridge.ts +53 -2
  52. package/src/{lib → client}/settings-bridge.ts +3 -15
  53. package/src/client/thread-context.ts +140 -0
  54. package/src/client/tiptap/bubble-menu.ts +205 -0
  55. package/src/client/tiptap/create-editor.ts +86 -0
  56. package/src/client/tiptap/exitable-marks.ts +73 -0
  57. package/src/client/tiptap/extensions.ts +65 -0
  58. package/src/client/tiptap/image-node.ts +482 -0
  59. package/src/client/tiptap/link-toolbar.ts +371 -0
  60. package/src/client/tiptap/more-break.ts +50 -0
  61. package/src/client/tiptap/paste-image.ts +129 -0
  62. package/src/client/tiptap/slash-commands.ts +438 -0
  63. package/src/{lib → client}/toast.ts +101 -3
  64. package/src/client/types/sortablejs.d.ts +44 -0
  65. package/src/client/upload-with-metadata.ts +54 -0
  66. package/src/client/video-processor.ts +207 -0
  67. package/src/client.ts +27 -17
  68. package/src/db/__tests__/migrations.test.ts +118 -0
  69. package/src/db/index.ts +52 -0
  70. package/src/db/migrations/0000_baseline.sql +269 -0
  71. package/src/db/migrations/0001_fts_setup.sql +31 -0
  72. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  73. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  74. package/src/db/migrations/meta/_journal.json +4 -39
  75. package/src/db/schema.ts +409 -140
  76. package/src/i18n/__tests__/detect.test.ts +115 -0
  77. package/src/i18n/context.tsx +2 -2
  78. package/src/i18n/detect.ts +85 -1
  79. package/src/i18n/i18n.ts +1 -1
  80. package/src/i18n/index.ts +2 -1
  81. package/src/i18n/locales/en.po +783 -1087
  82. package/src/i18n/locales/en.ts +1 -1
  83. package/src/i18n/locales/zh-Hans.po +867 -812
  84. package/src/i18n/locales/zh-Hans.ts +1 -1
  85. package/src/i18n/locales/zh-Hant.po +878 -823
  86. package/src/i18n/locales/zh-Hant.ts +1 -1
  87. package/src/i18n/middleware.ts +6 -0
  88. package/src/index.ts +5 -7
  89. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  90. package/src/lib/__tests__/constants.test.ts +0 -1
  91. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  92. package/src/lib/__tests__/nanoid.test.ts +26 -0
  93. package/src/lib/__tests__/resolve-config.test.ts +2 -2
  94. package/src/lib/__tests__/schemas.test.ts +186 -65
  95. package/src/lib/__tests__/slug.test.ts +126 -0
  96. package/src/lib/__tests__/sse.test.ts +6 -6
  97. package/src/lib/__tests__/summary.test.ts +264 -0
  98. package/src/lib/__tests__/theme.test.ts +1 -1
  99. package/src/lib/__tests__/timeline.test.ts +33 -30
  100. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  101. package/src/lib/__tests__/url.test.ts +2 -2
  102. package/src/lib/__tests__/view.test.ts +140 -65
  103. package/src/lib/blurhash-placeholder.ts +102 -0
  104. package/src/lib/constants.ts +3 -1
  105. package/src/lib/emoji-catalog.ts +963 -0
  106. package/src/lib/errors.ts +11 -8
  107. package/src/lib/feed.ts +77 -31
  108. package/src/lib/html.ts +2 -1
  109. package/src/lib/icon-catalog.ts +5033 -1
  110. package/src/lib/icons.ts +3 -2
  111. package/src/lib/index.ts +0 -1
  112. package/src/lib/markdown-to-tiptap.ts +286 -0
  113. package/src/lib/media-helpers.ts +22 -12
  114. package/src/lib/nanoid.ts +29 -0
  115. package/src/lib/navigation.ts +1 -1
  116. package/src/lib/render.tsx +24 -5
  117. package/src/lib/resolve-config.ts +13 -2
  118. package/src/lib/schemas.ts +226 -58
  119. package/src/lib/search-snippet.ts +34 -0
  120. package/src/lib/slug.ts +96 -0
  121. package/src/lib/sse.ts +6 -6
  122. package/src/lib/storage.ts +115 -7
  123. package/src/lib/summary.ts +158 -0
  124. package/src/lib/theme.ts +11 -8
  125. package/src/lib/timeline.ts +76 -34
  126. package/src/lib/tiptap-render.ts +191 -0
  127. package/src/lib/tiptap-to-markdown.ts +305 -0
  128. package/src/lib/upload.ts +263 -14
  129. package/src/lib/url.ts +37 -22
  130. package/src/lib/view.ts +236 -55
  131. package/src/middleware/__tests__/auth.test.ts +191 -11
  132. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  133. package/src/middleware/auth.ts +63 -9
  134. package/src/middleware/error-handler.ts +3 -3
  135. package/src/middleware/onboarding.ts +1 -1
  136. package/src/middleware/secure-headers.ts +40 -0
  137. package/src/preset.css +83 -2
  138. package/src/routes/__tests__/compose.test.ts +17 -24
  139. package/src/routes/api/__tests__/collections.test.ts +109 -61
  140. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  141. package/src/routes/api/__tests__/posts.test.ts +132 -68
  142. package/src/routes/api/__tests__/search.test.ts +15 -2
  143. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  144. package/src/routes/api/collections.ts +57 -31
  145. package/src/routes/api/custom-urls.ts +80 -0
  146. package/src/routes/api/export.ts +31 -0
  147. package/src/routes/api/nav-items.ts +23 -19
  148. package/src/routes/api/posts.ts +81 -62
  149. package/src/routes/api/search.ts +3 -4
  150. package/src/routes/api/upload-multipart.ts +245 -0
  151. package/src/routes/api/upload.ts +92 -24
  152. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  153. package/src/routes/auth/reset.tsx +5 -4
  154. package/src/routes/auth/setup.tsx +39 -31
  155. package/src/routes/auth/signin.tsx +13 -14
  156. package/src/routes/compose.tsx +27 -63
  157. package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
  158. package/src/routes/dash/custom-urls.tsx +414 -0
  159. package/src/routes/dash/settings.tsx +475 -99
  160. package/src/routes/feed/__tests__/rss.test.ts +22 -23
  161. package/src/routes/feed/rss.ts +6 -2
  162. package/src/routes/feed/sitemap.ts +2 -12
  163. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  164. package/src/routes/pages/__tests__/featured.test.ts +36 -18
  165. package/src/routes/pages/archive.tsx +177 -37
  166. package/src/routes/pages/collection.tsx +43 -14
  167. package/src/routes/pages/collections.tsx +11 -2
  168. package/src/routes/pages/featured.tsx +27 -3
  169. package/src/routes/pages/home.tsx +15 -14
  170. package/src/routes/pages/latest.tsx +1 -11
  171. package/src/routes/pages/new.tsx +39 -0
  172. package/src/routes/pages/page.tsx +95 -49
  173. package/src/routes/pages/search.tsx +1 -1
  174. package/src/services/__tests__/api-token.test.ts +135 -0
  175. package/src/services/__tests__/collection.test.ts +275 -227
  176. package/src/services/__tests__/custom-url.test.ts +213 -0
  177. package/src/services/__tests__/media.test.ts +162 -22
  178. package/src/services/__tests__/navigation.test.ts +109 -68
  179. package/src/services/__tests__/post-timeline.test.ts +205 -32
  180. package/src/services/__tests__/post.test.ts +800 -230
  181. package/src/services/__tests__/search.test.ts +67 -10
  182. package/src/services/__tests__/settings.test.ts +3 -3
  183. package/src/services/api-token.ts +166 -0
  184. package/src/services/auth.ts +17 -2
  185. package/src/services/collection.ts +397 -131
  186. package/src/services/custom-url.ts +188 -0
  187. package/src/services/export.ts +802 -0
  188. package/src/services/index.ts +26 -19
  189. package/src/services/media.ts +100 -22
  190. package/src/services/navigation.ts +158 -47
  191. package/src/services/path.ts +339 -0
  192. package/src/services/post.ts +764 -172
  193. package/src/services/search.ts +161 -74
  194. package/src/services/settings.ts +6 -2
  195. package/src/styles/components.css +293 -62
  196. package/src/styles/tokens.css +93 -5
  197. package/src/styles/ui.css +4349 -766
  198. package/src/types/bindings.ts +8 -0
  199. package/src/types/config.ts +34 -4
  200. package/src/types/constants.ts +17 -2
  201. package/src/types/entities.ts +83 -37
  202. package/src/types/operations.ts +20 -27
  203. package/src/types/props.ts +52 -17
  204. package/src/types/views.ts +48 -24
  205. package/src/ui/color-themes.ts +133 -23
  206. package/src/ui/compose/ComposeDialog.tsx +255 -16
  207. package/src/ui/compose/ComposePrompt.tsx +1 -1
  208. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  209. package/src/ui/dash/ListItemRow.tsx +1 -1
  210. package/src/ui/dash/StatusBadge.tsx +12 -2
  211. package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
  212. package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
  213. package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
  214. package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
  215. package/src/ui/dash/index.ts +0 -3
  216. package/src/ui/dash/settings/AccountContent.tsx +87 -146
  217. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  218. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  219. package/src/ui/dash/settings/AvatarContent.tsx +78 -0
  220. package/src/ui/dash/settings/GeneralContent.tsx +3 -62
  221. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  222. package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
  223. package/src/ui/feed/LinkCard.tsx +89 -40
  224. package/src/ui/feed/NoteCard.tsx +39 -25
  225. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  226. package/src/ui/feed/QuoteCard.tsx +38 -23
  227. package/src/ui/feed/ThreadPreview.tsx +90 -26
  228. package/src/ui/feed/TimelineFeed.tsx +3 -2
  229. package/src/ui/feed/TimelineItem.tsx +15 -6
  230. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  231. package/src/ui/feed/thread-preview-state.ts +61 -0
  232. package/src/ui/font-themes.ts +2 -2
  233. package/src/ui/layouts/BaseLayout.tsx +2 -2
  234. package/src/ui/layouts/SiteLayout.tsx +116 -103
  235. package/src/ui/pages/ArchivePage.tsx +923 -95
  236. package/src/ui/pages/CollectionPage.tsx +6 -35
  237. package/src/ui/pages/CollectionsPage.tsx +2 -1
  238. package/src/ui/pages/ComposePage.tsx +54 -0
  239. package/src/ui/pages/FeaturedPage.tsx +2 -1
  240. package/src/ui/pages/HomePage.tsx +1 -1
  241. package/src/ui/pages/PostPage.tsx +30 -45
  242. package/src/ui/pages/SearchPage.tsx +182 -38
  243. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  244. package/src/ui/shared/CollectionsSidebar.tsx +239 -4
  245. package/src/ui/shared/MediaGallery.tsx +475 -41
  246. package/src/ui/shared/PostFooter.tsx +204 -0
  247. package/src/ui/shared/StarRating.tsx +27 -0
  248. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  249. package/src/ui/shared/index.ts +0 -1
  250. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  251. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  252. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  253. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  254. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  255. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  256. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  257. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  258. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  259. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  260. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  261. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  262. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  263. package/src/lib/__tests__/sqid.test.ts +0 -65
  264. package/src/lib/collections-reorder.ts +0 -28
  265. package/src/lib/compose-bridge.ts +0 -280
  266. package/src/lib/media-upload.ts +0 -148
  267. package/src/lib/sqid.ts +0 -79
  268. package/src/routes/api/__tests__/pages.test.ts +0 -218
  269. package/src/routes/api/pages.ts +0 -73
  270. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  271. package/src/routes/dash/appearance.tsx +0 -240
  272. package/src/routes/dash/collections.tsx +0 -211
  273. package/src/routes/dash/index.tsx +0 -103
  274. package/src/routes/dash/media.tsx +0 -132
  275. package/src/routes/dash/pages.tsx +0 -239
  276. package/src/routes/dash/posts.tsx +0 -334
  277. package/src/routes/dash/redirects.tsx +0 -257
  278. package/src/routes/pages/post.tsx +0 -59
  279. package/src/services/__tests__/page.test.ts +0 -298
  280. package/src/services/__tests__/path-registry.test.ts +0 -165
  281. package/src/services/__tests__/redirect.test.ts +0 -159
  282. package/src/services/page.ts +0 -203
  283. package/src/services/path-registry.ts +0 -160
  284. package/src/services/redirect.ts +0 -97
  285. package/src/types/sortablejs.d.ts +0 -29
  286. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
  287. package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
  288. package/src/ui/components/compose-types.ts +0 -75
  289. package/src/ui/components/jant-collection-form.ts +0 -512
  290. package/src/ui/components/jant-compose-dialog.ts +0 -495
  291. package/src/ui/components/jant-compose-editor.ts +0 -814
  292. package/src/ui/dash/PageForm.tsx +0 -185
  293. package/src/ui/dash/PostList.tsx +0 -95
  294. package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
  295. package/src/ui/dash/collections/CollectionForm.tsx +0 -166
  296. package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
  297. package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
  298. package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
  299. package/src/ui/dash/media/MediaListContent.tsx +0 -201
  300. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  301. package/src/ui/dash/pages/PagesContent.tsx +0 -74
  302. package/src/ui/dash/posts/PostForm.tsx +0 -248
  303. package/src/ui/dash/settings/SettingsNav.tsx +0 -52
  304. package/src/ui/layouts/DashLayout.tsx +0 -165
  305. package/src/ui/pages/SinglePage.tsx +0 -23
  306. package/src/ui/shared/ThreadView.tsx +0 -136
  307. /package/src/{ui → client}/components/settings-types.ts +0 -0
@@ -1,64 +1,100 @@
1
1
  /**
2
- * Dashboard Settings Routes
2
+ * Settings Routes
3
3
  *
4
- * Sub-pages: General, Account
4
+ * Unified settings hub — root page with iOS-style grouped list,
5
+ * plus sub-pages for General, Avatar, Navigation, Color Theme,
6
+ * Font Theme, Custom CSS, Account (Sessions + Password), and API Tokens.
5
7
  */
6
8
 
7
9
  import { Hono } from "hono";
8
10
  import { msg } from "@lingui/core/macro";
9
11
  import type { Bindings } from "../../types.js";
10
12
  import type { AppVariables } from "../../types/app-context.js";
11
- import { DashLayout } from "../../ui/layouts/DashLayout.js";
12
13
  import { sse, dsRedirect, dsToast } from "../../lib/sse.js";
13
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";
14
18
  import { TIMEZONES } from "../../lib/timezones.js";
15
19
  import { escapeHtml } from "../../lib/html.js";
16
20
  import { ValidationError } from "../../lib/errors.js";
21
+ import { SETTINGS_KEYS } from "../../lib/constants.js";
22
+ import { getAvailableThemes } from "../../lib/theme.js";
23
+ import { BUILTIN_FONT_THEMES } from "../../ui/font-themes.js";
24
+ import { SettingsRootContent } from "../../ui/dash/settings/SettingsRootContent.js";
17
25
  import { GeneralContent } from "../../ui/dash/settings/GeneralContent.js";
26
+ import { AvatarContent } from "../../ui/dash/settings/AvatarContent.js";
27
+ import { AccountMenuContent } from "../../ui/dash/settings/AccountMenuContent.js";
18
28
  import { AccountContent } from "../../ui/dash/settings/AccountContent.js";
29
+ import {
30
+ SessionsContent,
31
+ type SessionInfo,
32
+ } from "../../ui/dash/settings/SessionsContent.js";
33
+ import { NavigationContent } from "../../ui/dash/appearance/NavigationContent.js";
34
+ import { ColorThemeContent } from "../../ui/dash/appearance/ColorThemeContent.js";
35
+ import { FontThemeContent } from "../../ui/dash/appearance/FontThemeContent.js";
36
+ import { AdvancedContent } from "../../ui/dash/appearance/AdvancedContent.js";
37
+ import { ApiTokensContent } from "../../ui/dash/settings/ApiTokensContent.js";
19
38
 
20
39
  type Env = { Bindings: Bindings; Variables: AppVariables };
21
40
 
22
41
  export const settingsRoutes = new Hono<Env>();
23
42
 
24
43
  // ===========================================================================
25
- // General settings
44
+ // Settings root — iOS-style grouped list
26
45
  // ===========================================================================
27
46
 
28
47
  settingsRoutes.get("/", async (c) => {
48
+ const navData = await getNavigationData(c);
49
+
50
+ return renderPublicPage(c, {
51
+ title: `Settings - ${navData.siteName}`,
52
+ navData,
53
+ content: <SettingsRootContent />,
54
+ });
55
+ });
56
+
57
+ // ===========================================================================
58
+ // General settings
59
+ // ===========================================================================
60
+
61
+ settingsRoutes.get("/general", async (c) => {
29
62
  const { allSettings, appConfig } = c.var;
30
63
 
31
64
  const dbSiteName = allSettings["SITE_NAME"] ?? "";
32
65
  const dbSiteDescription = allSettings["SITE_DESCRIPTION"] ?? "";
33
66
 
34
67
  const saved = c.req.query("saved") !== undefined;
35
-
36
- return c.html(
37
- <DashLayout
38
- c={c}
39
- title="Settings"
40
- siteName={dbSiteName || appConfig.fallbacks.siteName}
41
- currentPath="/dash/settings"
42
- toast={saved ? { message: "Settings saved successfully." } : undefined}
43
- >
44
- <GeneralContent
45
- siteName={dbSiteName || ""}
46
- siteDescription={dbSiteDescription || ""}
47
- siteLanguage={appConfig.siteLanguage}
48
- siteNameFallback={appConfig.fallbacks.siteName}
49
- siteDescriptionFallback={appConfig.fallbacks.siteDescription}
50
- siteAvatarUrl={appConfig.siteAvatarUrl}
51
- showHeaderAvatar={appConfig.showHeaderAvatar}
52
- timeZone={appConfig.timeZone}
53
- siteFooter={appConfig.siteFooter}
54
- noindex={appConfig.noindex}
55
- timezones={TIMEZONES}
56
- />
57
- </DashLayout>,
58
- );
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
+ });
59
95
  });
60
96
 
61
- settingsRoutes.post("/", async (c) => {
97
+ settingsRoutes.post("/general", async (c) => {
62
98
  const i18n = getI18n(c);
63
99
  const body = await c.req.json<{
64
100
  siteName: string;
@@ -76,43 +112,36 @@ settingsRoutes.post("/", async (c) => {
76
112
  fallbackSiteName: c.var.appConfig.fallbacks.siteName,
77
113
  });
78
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
+
79
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.
80
124
  const wantsJson = c.req.header("accept")?.includes("application/json");
81
125
  if (wantsJson) {
82
- if (languageChanged) {
83
- return c.json({
84
- status: "redirect" as const,
85
- url: "/dash/settings?saved",
86
- });
87
- }
88
126
  return c.json({
89
- status: "ok" as const,
90
- toast: i18n._(
91
- msg({
92
- message: "Settings saved successfully.",
93
- comment: "@context: Toast after saving general settings",
94
- }),
95
- ),
96
- siteName: displayName,
127
+ status: "redirect" as const,
128
+ url: "/settings/general?saved",
97
129
  });
98
130
  }
99
131
 
100
132
  return sse(c, async (stream) => {
101
133
  if (languageChanged) {
102
- await stream.redirect("/dash/settings?saved");
134
+ await stream.redirect("/settings/general?saved");
103
135
  } else {
104
136
  const escaped = escapeHtml(displayName);
105
- await stream.patchElements(
106
- `<a id="site-name" href="/dash" class="font-semibold">${escaped}</a>`,
107
- );
108
- await stream.patchElements(`Settings - ${escaped}`, {
137
+ await stream.patchElements(`General - ${escaped}`, {
109
138
  mode: "inner",
110
139
  selector: "title",
111
140
  });
112
141
  await stream.toast(
113
142
  i18n._(
114
143
  msg({
115
- message: "Settings saved successfully.",
144
+ message: "Settings updated.",
116
145
  comment: "@context: Toast after saving general settings",
117
146
  }),
118
147
  ),
@@ -129,7 +158,7 @@ settingsRoutes.post("/", async (c) => {
129
158
  });
130
159
  });
131
160
 
132
- settingsRoutes.post("/seo", async (c) => {
161
+ settingsRoutes.post("/general/seo", async (c) => {
133
162
  const i18n = getI18n(c);
134
163
  const body = await c.req.json<{ noindex: string }>();
135
164
  const { settings } = c.var.services;
@@ -150,7 +179,7 @@ settingsRoutes.post("/seo", async (c) => {
150
179
  status: "ok" as const,
151
180
  toast: i18n._(
152
181
  msg({
153
- message: "SEO settings saved successfully.",
182
+ message: "SEO settings updated.",
154
183
  comment: "@context: Toast after saving SEO settings",
155
184
  }),
156
185
  ),
@@ -161,7 +190,7 @@ settingsRoutes.post("/seo", async (c) => {
161
190
  await stream.toast(
162
191
  i18n._(
163
192
  msg({
164
- message: "SEO settings saved successfully.",
193
+ message: "SEO settings updated.",
165
194
  comment: "@context: Toast after saving SEO settings",
166
195
  }),
167
196
  ),
@@ -174,9 +203,33 @@ settingsRoutes.post("/seo", async (c) => {
174
203
  });
175
204
 
176
205
  // ===========================================================================
177
- // Avatar upload & removal
206
+ // Avatar
178
207
  // ===========================================================================
179
208
 
209
+ settingsRoutes.get("/avatar", async (c) => {
210
+ const saved = c.req.query("saved") !== undefined;
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
+ });
231
+ });
232
+
180
233
  settingsRoutes.post("/avatar", async (c) => {
181
234
  const i18n = getI18n(c);
182
235
  const storage = c.var.storage;
@@ -184,7 +237,7 @@ settingsRoutes.post("/avatar", async (c) => {
184
237
  return dsToast(
185
238
  i18n._(
186
239
  msg({
187
- message: "Storage not configured.",
240
+ message: "File storage isn't set up. Check your server config.",
188
241
  comment: "@context: Error toast when file storage is not set up",
189
242
  }),
190
243
  ),
@@ -198,7 +251,7 @@ settingsRoutes.post("/avatar", async (c) => {
198
251
  return dsToast(
199
252
  i18n._(
200
253
  msg({
201
- message: "No file provided.",
254
+ message: "No file selected. Choose a file to upload.",
202
255
  comment: "@context: Error toast when no file was selected for upload",
203
256
  }),
204
257
  ),
@@ -222,10 +275,11 @@ settingsRoutes.post("/avatar", async (c) => {
222
275
  media: c.var.services.media,
223
276
  storage,
224
277
  storageProvider: c.var.appConfig.storageDriver,
278
+ maxFileSizeMB: c.var.appConfig.uploadMaxFileSize,
225
279
  },
226
280
  );
227
281
 
228
- return dsRedirect("/dash/settings?saved");
282
+ return dsRedirect("/settings/avatar?saved");
229
283
  } catch (e) {
230
284
  if (e instanceof ValidationError) {
231
285
  return dsToast(e.message, "error");
@@ -233,7 +287,7 @@ settingsRoutes.post("/avatar", async (c) => {
233
287
  return dsToast(
234
288
  i18n._(
235
289
  msg({
236
- message: "Upload failed. Please try again.",
290
+ message: "Upload didn't go through. Try again in a moment.",
237
291
  comment: "@context: Error toast when avatar upload fails",
238
292
  }),
239
293
  ),
@@ -248,10 +302,13 @@ settingsRoutes.post("/avatar/remove", async (c) => {
248
302
  // ── JSON response mode (used by Lit settings bridge) ──────────────
249
303
  const wantsJson = c.req.header("accept")?.includes("application/json");
250
304
  if (wantsJson) {
251
- return c.json({ status: "redirect" as const, url: "/dash/settings?saved" });
305
+ return c.json({
306
+ status: "redirect" as const,
307
+ url: "/settings/avatar?saved",
308
+ });
252
309
  }
253
310
 
254
- return dsRedirect("/dash/settings?saved");
311
+ return dsRedirect("/settings/avatar?saved");
255
312
  });
256
313
 
257
314
  settingsRoutes.post("/avatar/display", async (c) => {
@@ -272,7 +329,7 @@ settingsRoutes.post("/avatar/display", async (c) => {
272
329
  status: "ok" as const,
273
330
  toast: i18n._(
274
331
  msg({
275
- message: "Avatar display setting saved successfully.",
332
+ message: "Avatar display updated.",
276
333
  comment: "@context: Toast after saving avatar display preference",
277
334
  }),
278
335
  ),
@@ -283,7 +340,7 @@ settingsRoutes.post("/avatar/display", async (c) => {
283
340
  await stream.toast(
284
341
  i18n._(
285
342
  msg({
286
- message: "Avatar display setting saved successfully.",
343
+ message: "Avatar display updated.",
287
344
  comment: "@context: Toast after saving avatar display preference",
288
345
  }),
289
346
  ),
@@ -296,75 +353,343 @@ settingsRoutes.post("/avatar/display", async (c) => {
296
353
  });
297
354
 
298
355
  // ===========================================================================
299
- // Account
356
+ // Navigation (moved from appearance routes)
300
357
  // ===========================================================================
301
358
 
302
- settingsRoutes.get("/account", async (c) => {
303
- const siteName = c.var.appConfig.siteName;
304
- const session = await c.var.auth.api.getSession({
305
- headers: c.req.raw.headers,
359
+ settingsRoutes.get("/navigation", async (c) => {
360
+ const navItems = await c.var.services.navItems.list();
361
+ const headerNavMaxVisible = c.var.appConfig.headerNavMaxVisible;
362
+ const homeDefaultView = c.var.appConfig.homeDefaultView;
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
+ ),
306
383
  });
307
- const userName = session?.user?.name ?? "";
308
- const saved = c.req.query("saved") !== undefined;
384
+ });
309
385
 
310
- return c.html(
311
- <DashLayout
312
- c={c}
313
- title="Settings"
314
- siteName={siteName}
315
- currentPath="/dash/settings"
316
- toast={saved ? { message: "Profile saved successfully." } : undefined}
317
- >
318
- <AccountContent userName={userName} />
319
- </DashLayout>,
320
- );
386
+ settingsRoutes.post("/navigation/nav-max-visible", async (c) => {
387
+ const body = await c.req.json<{ value: number }>();
388
+ const { settings } = c.var.services;
389
+
390
+ const navMax = Math.max(0, Math.min(5, body.value ?? 3));
391
+ if (navMax !== 3) {
392
+ await settings.set("HEADER_NAV_MAX_VISIBLE", String(navMax));
393
+ } else {
394
+ await settings.remove("HEADER_NAV_MAX_VISIBLE");
395
+ }
396
+
397
+ return c.json({ ok: true });
321
398
  });
322
399
 
323
- settingsRoutes.post("/account", async (c) => {
400
+ settingsRoutes.post("/navigation/home-default-view", async (c) => {
401
+ const body = await c.req.json<{ value: string }>();
402
+ const { settings } = c.var.services;
403
+
404
+ if (body.value === "featured") {
405
+ await settings.set("HOME_DEFAULT_VIEW", "featured");
406
+ } else {
407
+ await settings.remove("HOME_DEFAULT_VIEW");
408
+ }
409
+
410
+ return c.json({ ok: true });
411
+ });
412
+
413
+ // ===========================================================================
414
+ // Color Theme (moved from appearance routes)
415
+ // ===========================================================================
416
+
417
+ settingsRoutes.get("/color-theme", async (c) => {
418
+ const defaultThemeId = c.var.appConfig.fallbacks.defaultTheme;
419
+ const currentThemeId =
420
+ c.var.allSettings[SETTINGS_KEYS.THEME] ?? defaultThemeId;
421
+ const themes = getAvailableThemes();
422
+ const saved = c.req.query("saved") !== undefined;
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
+ });
440
+ });
441
+
442
+ settingsRoutes.post("/color-theme", async (c) => {
324
443
  const i18n = getI18n(c);
325
- const body = await c.req.json<{ userName: string }>();
326
- const name = body.userName?.trim();
444
+ const body = await c.req.json<{ theme: string }>();
445
+ const { settings } = c.var.services;
446
+ const themes = getAvailableThemes();
327
447
 
328
- if (!name) {
448
+ const validTheme = themes.find((t) => t.id === body.theme);
449
+ if (!validTheme) {
329
450
  return dsToast(
330
451
  i18n._(
331
452
  msg({
332
- message: "Name is required.",
333
- comment: "@context: Error toast when display name is empty",
453
+ message: "That theme isn't available. Pick another one.",
454
+ comment: "@context: Error toast when selected theme is not valid",
334
455
  }),
335
456
  ),
336
457
  "error",
337
458
  );
338
459
  }
339
460
 
340
- try {
341
- await c.var.auth.api.updateUser({
342
- body: { name },
343
- headers: c.req.raw.headers,
344
- });
345
- } catch {
461
+ const defaultThemeId = c.var.appConfig.fallbacks.defaultTheme;
462
+ if (validTheme.id === defaultThemeId) {
463
+ await settings.remove(SETTINGS_KEYS.THEME);
464
+ } else {
465
+ await settings.set(SETTINGS_KEYS.THEME, validTheme.id);
466
+ }
467
+
468
+ return dsRedirect("/settings/color-theme?saved");
469
+ });
470
+
471
+ // ===========================================================================
472
+ // Font Theme (moved from appearance routes)
473
+ // ===========================================================================
474
+
475
+ settingsRoutes.get("/font-theme", async (c) => {
476
+ const currentFontThemeId = c.var.allSettings["FONT_THEME"] ?? "default";
477
+ const saved = c.req.query("saved") !== undefined;
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
+ });
498
+ });
499
+
500
+ settingsRoutes.post("/font-theme", async (c) => {
501
+ const i18n = getI18n(c);
502
+ const body = await c.req.json<{ fontTheme: string }>();
503
+ const { settings } = c.var.services;
504
+
505
+ const validFont = BUILTIN_FONT_THEMES.find((f) => f.id === body.fontTheme);
506
+ if (!validFont) {
346
507
  return dsToast(
347
508
  i18n._(
348
509
  msg({
349
- message: "Failed to update profile.",
350
- comment: "@context: Error toast when profile update fails",
510
+ message: "That font theme isn't available. Pick another one.",
511
+ comment:
512
+ "@context: Error toast when selected font theme is not valid",
351
513
  }),
352
514
  ),
353
515
  "error",
354
516
  );
355
517
  }
356
518
 
519
+ if (validFont.id === "default") {
520
+ await settings.remove("FONT_THEME");
521
+ } else {
522
+ await settings.set("FONT_THEME", validFont.id);
523
+ }
524
+
525
+ return dsRedirect("/settings/font-theme?saved");
526
+ });
527
+
528
+ // ===========================================================================
529
+ // Custom CSS (moved from appearance routes)
530
+ // ===========================================================================
531
+
532
+ settingsRoutes.get("/custom-css", async (c) => {
533
+ const customCSS = c.var.allSettings[SETTINGS_KEYS.CUSTOM_CSS] ?? "";
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
+ });
550
+ });
551
+
552
+ settingsRoutes.post("/custom-css", async (c) => {
553
+ const i18n = getI18n(c);
554
+ const body = await c.req.json<{ customCSS: string }>();
555
+ const { settings } = c.var.services;
556
+
557
+ const css = body.customCSS?.trim() ?? "";
558
+
559
+ if (css) {
560
+ await settings.set(SETTINGS_KEYS.CUSTOM_CSS, css);
561
+ } else {
562
+ await settings.remove(SETTINGS_KEYS.CUSTOM_CSS);
563
+ }
564
+
357
565
  return dsToast(
358
566
  i18n._(
359
567
  msg({
360
- message: "Profile saved successfully.",
361
- comment: "@context: Toast after saving user profile",
568
+ message: "Custom CSS updated.",
569
+ comment: "@context: Toast after saving custom CSS",
362
570
  }),
363
571
  ),
364
572
  );
365
573
  });
366
574
 
367
- settingsRoutes.post("/password", async (c) => {
575
+ // ===========================================================================
576
+ // Account sub-menu
577
+ // ===========================================================================
578
+
579
+ settingsRoutes.get("/account", async (c) => {
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({
613
+ headers: c.req.raw.headers,
614
+ });
615
+
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
+ }),
629
+ );
630
+
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
+ });
637
+
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");
656
+
657
+ try {
658
+ await c.var.auth.api.revokeSession({
659
+ body: { token },
660
+ headers: c.req.raw.headers,
661
+ });
662
+ } catch {
663
+ // Session may already be expired/revoked — still redirect
664
+ }
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
+ </>
688
+ ),
689
+ });
690
+ });
691
+
692
+ settingsRoutes.post("/account/password", async (c) => {
368
693
  const i18n = getI18n(c);
369
694
  const body = await c.req.json<{
370
695
  currentPassword: string;
@@ -376,7 +701,8 @@ settingsRoutes.post("/password", async (c) => {
376
701
  return dsToast(
377
702
  i18n._(
378
703
  msg({
379
- message: "Passwords do not match.",
704
+ message:
705
+ "Passwords don't match. Make sure both fields are identical.",
380
706
  comment:
381
707
  "@context: Error toast when new password and confirmation differ",
382
708
  }),
@@ -398,7 +724,7 @@ settingsRoutes.post("/password", async (c) => {
398
724
  return dsToast(
399
725
  i18n._(
400
726
  msg({
401
- message: "Current password is incorrect.",
727
+ message: "Current password doesn't match. Try again.",
402
728
  comment:
403
729
  "@context: Error toast when current password verification fails",
404
730
  }),
@@ -411,7 +737,7 @@ settingsRoutes.post("/password", async (c) => {
411
737
  await stream.toast(
412
738
  i18n._(
413
739
  msg({
414
- message: "Password changed successfully.",
740
+ message: "Password changed.",
415
741
  comment: "@context: Toast after changing account password",
416
742
  }),
417
743
  ),
@@ -423,3 +749,53 @@ settingsRoutes.post("/password", async (c) => {
423
749
  });
424
750
  });
425
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
+ });