@jant/core 0.3.24 → 0.3.26

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 (277) hide show
  1. package/dist/app.js +101 -571
  2. package/dist/client.js +1 -0
  3. package/dist/db/schema.js +1 -1
  4. package/dist/i18n/locales/en.js +1 -1
  5. package/dist/i18n/locales/zh-Hans.js +1 -1
  6. package/dist/i18n/locales/zh-Hant.js +1 -1
  7. package/dist/index.js +3 -9
  8. package/dist/lib/avatar-upload.js +134 -0
  9. package/dist/lib/config.js +39 -0
  10. package/dist/lib/constants.js +10 -9
  11. package/dist/lib/favicon.js +102 -0
  12. package/dist/lib/image.js +13 -17
  13. package/dist/lib/media-helpers.js +2 -2
  14. package/dist/lib/nav-reorder.js +1 -1
  15. package/dist/lib/navigation.js +48 -3
  16. package/dist/lib/pagination.js +44 -0
  17. package/dist/lib/render.js +16 -11
  18. package/dist/lib/schemas.js +34 -3
  19. package/dist/lib/theme.js +4 -4
  20. package/dist/lib/timeline.js +24 -48
  21. package/dist/lib/timezones.js +388 -0
  22. package/dist/lib/view.js +3 -3
  23. package/dist/routes/api/collections.js +124 -0
  24. package/dist/routes/api/nav-items.js +104 -0
  25. package/dist/routes/api/pages.js +91 -0
  26. package/dist/routes/api/posts.js +3 -3
  27. package/dist/routes/api/search.js +2 -2
  28. package/dist/routes/api/settings.js +68 -0
  29. package/dist/routes/api/upload.js +3 -3
  30. package/dist/routes/auth/reset.js +221 -0
  31. package/dist/routes/auth/setup.js +194 -0
  32. package/dist/routes/auth/signin.js +176 -0
  33. package/dist/routes/compose.js +48 -0
  34. package/dist/routes/dash/collections.js +24 -416
  35. package/dist/routes/dash/index.js +1 -1
  36. package/dist/routes/dash/media.js +13 -393
  37. package/dist/routes/dash/pages.js +112 -86
  38. package/dist/routes/dash/posts.js +3 -5
  39. package/dist/routes/dash/redirects.js +20 -14
  40. package/dist/routes/dash/settings.js +213 -518
  41. package/dist/routes/feed/rss.js +4 -3
  42. package/dist/routes/feed/sitemap.js +5 -3
  43. package/dist/routes/pages/archive.js +3 -6
  44. package/dist/routes/pages/collection.js +3 -6
  45. package/dist/routes/pages/collections.js +28 -0
  46. package/dist/routes/pages/featured.js +36 -0
  47. package/dist/routes/pages/home.js +33 -49
  48. package/dist/routes/pages/latest.js +45 -0
  49. package/dist/routes/pages/page.js +29 -32
  50. package/dist/routes/pages/post.js +3 -6
  51. package/dist/routes/pages/search.js +3 -6
  52. package/dist/services/page.js +5 -1
  53. package/dist/services/post.js +45 -31
  54. package/dist/services/search.js +1 -1
  55. package/dist/types/bindings.js +3 -0
  56. package/dist/types/config.js +147 -0
  57. package/dist/types/constants.js +27 -0
  58. package/dist/types/entities.js +3 -0
  59. package/dist/types/operations.js +3 -0
  60. package/dist/types/props.js +3 -0
  61. package/dist/types/views.js +5 -0
  62. package/dist/types.js +8 -111
  63. package/dist/{theme → ui}/color-themes.js +33 -33
  64. package/dist/ui/compose/ComposeDialog.js +467 -0
  65. package/dist/ui/compose/ComposePrompt.js +55 -0
  66. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +1 -2
  67. package/dist/{theme/components → ui/dash}/PageForm.js +21 -15
  68. package/dist/{theme/components → ui/dash}/PostForm.js +22 -43
  69. package/dist/{theme/components → ui/dash}/PostList.js +6 -6
  70. package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
  71. package/dist/ui/dash/collections/CollectionForm.js +152 -0
  72. package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
  73. package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
  74. package/dist/{theme/components → ui/dash}/index.js +3 -6
  75. package/dist/ui/dash/media/MediaListContent.js +166 -0
  76. package/dist/ui/dash/media/ViewMediaContent.js +212 -0
  77. package/dist/ui/dash/pages/LinkFormContent.js +130 -0
  78. package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
  79. package/dist/ui/dash/settings/AccountContent.js +209 -0
  80. package/dist/ui/dash/settings/AppearanceContent.js +259 -0
  81. package/dist/ui/dash/settings/GeneralContent.js +536 -0
  82. package/dist/ui/dash/settings/SettingsNav.js +41 -0
  83. package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
  84. package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
  85. package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
  86. package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
  87. package/dist/ui/feed/TimelineFeed.js +41 -0
  88. package/dist/ui/feed/TimelineItem.js +27 -0
  89. package/dist/ui/font-themes.js +36 -0
  90. package/dist/{theme → ui}/layouts/BaseLayout.js +34 -2
  91. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  92. package/dist/ui/layouts/SiteLayout.js +169 -0
  93. package/dist/{themes/threads → ui}/pages/ArchivePage.js +16 -14
  94. package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
  95. package/dist/ui/pages/CollectionsPage.js +76 -0
  96. package/dist/ui/pages/FeaturedPage.js +24 -0
  97. package/dist/ui/pages/HomePage.js +24 -0
  98. package/dist/{themes/threads → ui}/pages/PostPage.js +13 -8
  99. package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
  100. package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
  101. package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
  102. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  103. package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
  104. package/dist/ui/shared/index.js +5 -0
  105. package/package.json +1 -9
  106. package/src/__tests__/helpers/db.ts +3 -0
  107. package/src/app.tsx +131 -561
  108. package/src/client.ts +1 -0
  109. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  110. package/src/db/migrations/meta/_journal.json +7 -0
  111. package/src/db/schema.ts +1 -1
  112. package/src/i18n/locales/en.po +477 -261
  113. package/src/i18n/locales/en.ts +1 -1
  114. package/src/i18n/locales/zh-Hans.po +477 -261
  115. package/src/i18n/locales/zh-Hans.ts +1 -1
  116. package/src/i18n/locales/zh-Hant.po +477 -261
  117. package/src/i18n/locales/zh-Hant.ts +1 -1
  118. package/src/index.ts +7 -36
  119. package/src/lib/__tests__/config.test.ts +192 -0
  120. package/src/lib/__tests__/favicon.test.ts +151 -0
  121. package/src/lib/__tests__/image.test.ts +2 -6
  122. package/src/lib/__tests__/schemas.test.ts +60 -19
  123. package/src/lib/__tests__/timeline.test.ts +45 -81
  124. package/src/lib/__tests__/timezones.test.ts +61 -0
  125. package/src/lib/__tests__/view.test.ts +15 -9
  126. package/src/lib/avatar-upload.ts +165 -0
  127. package/src/lib/config.ts +47 -0
  128. package/src/lib/constants.ts +19 -10
  129. package/src/lib/favicon.ts +115 -0
  130. package/src/lib/image.ts +13 -21
  131. package/src/lib/media-helpers.ts +2 -2
  132. package/src/lib/nav-reorder.ts +1 -1
  133. package/src/lib/navigation.ts +73 -4
  134. package/src/lib/pagination.ts +50 -0
  135. package/src/lib/render.tsx +22 -15
  136. package/src/lib/schemas.ts +47 -6
  137. package/src/lib/theme.ts +5 -5
  138. package/src/lib/timeline.ts +28 -57
  139. package/src/lib/timezones.ts +325 -0
  140. package/src/lib/view.ts +3 -3
  141. package/src/preset.css +2 -1
  142. package/src/routes/__tests__/compose.test.ts +199 -0
  143. package/src/routes/api/__tests__/collections.test.ts +249 -0
  144. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  145. package/src/routes/api/__tests__/pages.test.ts +218 -0
  146. package/src/routes/api/__tests__/settings.test.ts +132 -0
  147. package/src/routes/api/collections.ts +143 -0
  148. package/src/routes/api/nav-items.ts +115 -0
  149. package/src/routes/api/pages.ts +101 -0
  150. package/src/routes/api/posts.ts +3 -3
  151. package/src/routes/api/search.ts +2 -2
  152. package/src/routes/api/settings.ts +91 -0
  153. package/src/routes/api/upload.ts +2 -3
  154. package/src/routes/auth/reset.tsx +239 -0
  155. package/src/routes/auth/setup.tsx +189 -0
  156. package/src/routes/auth/signin.tsx +163 -0
  157. package/src/routes/compose.ts +63 -0
  158. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  159. package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
  160. package/src/routes/dash/collections.tsx +18 -367
  161. package/src/routes/dash/index.tsx +1 -1
  162. package/src/routes/dash/media.tsx +13 -415
  163. package/src/routes/dash/pages.tsx +131 -98
  164. package/src/routes/dash/posts.tsx +3 -7
  165. package/src/routes/dash/redirects.tsx +22 -16
  166. package/src/routes/dash/settings.tsx +265 -478
  167. package/src/routes/feed/__tests__/rss.test.ts +141 -0
  168. package/src/routes/feed/rss.ts +5 -3
  169. package/src/routes/feed/sitemap.ts +5 -3
  170. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  171. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  172. package/src/routes/pages/archive.tsx +2 -6
  173. package/src/routes/pages/collection.tsx +2 -6
  174. package/src/routes/pages/collections.tsx +36 -0
  175. package/src/routes/pages/featured.tsx +44 -0
  176. package/src/routes/pages/home.tsx +30 -53
  177. package/src/routes/pages/latest.tsx +59 -0
  178. package/src/routes/pages/page.tsx +28 -30
  179. package/src/routes/pages/post.tsx +2 -5
  180. package/src/routes/pages/search.tsx +2 -6
  181. package/src/services/__tests__/page.test.ts +106 -0
  182. package/src/services/__tests__/post.test.ts +114 -15
  183. package/src/services/page.ts +13 -1
  184. package/src/services/post.ts +58 -40
  185. package/src/services/search.ts +2 -2
  186. package/src/styles/components.css +0 -65
  187. package/src/styles/tokens.css +47 -0
  188. package/src/styles/ui.css +475 -0
  189. package/src/types/bindings.ts +30 -0
  190. package/src/types/config.ts +183 -0
  191. package/src/types/constants.ts +26 -0
  192. package/src/types/entities.ts +109 -0
  193. package/src/types/operations.ts +88 -0
  194. package/src/types/props.ts +115 -0
  195. package/src/types/views.ts +172 -0
  196. package/src/types.ts +8 -774
  197. package/src/ui/__tests__/font-themes.test.ts +34 -0
  198. package/src/{theme → ui}/color-themes.ts +34 -34
  199. package/src/ui/compose/ComposeDialog.tsx +414 -0
  200. package/src/ui/compose/ComposePrompt.tsx +55 -0
  201. package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
  202. package/src/{theme/components → ui/dash}/PageForm.tsx +25 -19
  203. package/src/{theme/components → ui/dash}/PostForm.tsx +26 -45
  204. package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
  205. package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
  206. package/src/ui/dash/collections/CollectionForm.tsx +153 -0
  207. package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
  208. package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
  209. package/src/ui/dash/index.ts +10 -0
  210. package/src/ui/dash/media/MediaListContent.tsx +201 -0
  211. package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
  212. package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
  213. package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
  214. package/src/ui/dash/settings/AccountContent.tsx +176 -0
  215. package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
  216. package/src/ui/dash/settings/GeneralContent.tsx +533 -0
  217. package/src/ui/dash/settings/SettingsNav.tsx +56 -0
  218. package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
  219. package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
  220. package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
  221. package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
  222. package/src/ui/feed/TimelineFeed.tsx +49 -0
  223. package/src/ui/feed/TimelineItem.tsx +45 -0
  224. package/src/ui/font-themes.ts +54 -0
  225. package/src/{theme → ui}/layouts/BaseLayout.tsx +28 -1
  226. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  227. package/src/ui/layouts/SiteLayout.tsx +164 -0
  228. package/src/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
  229. package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
  230. package/src/ui/pages/CollectionsPage.tsx +73 -0
  231. package/src/ui/pages/FeaturedPage.tsx +31 -0
  232. package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
  233. package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
  234. package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
  235. package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
  236. package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
  237. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  238. package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
  239. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  240. package/src/ui/shared/index.ts +12 -0
  241. package/bin/jant.js +0 -185
  242. package/dist/lib/theme-components.js +0 -46
  243. package/dist/routes/dash/navigation.js +0 -289
  244. package/dist/theme/index.js +0 -18
  245. package/dist/theme/layouts/index.js +0 -2
  246. package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
  247. package/dist/themes/threads/index.js +0 -81
  248. package/dist/themes/threads/pages/HomePage.js +0 -25
  249. package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
  250. package/dist/themes/threads/timeline/TimelineItem.js +0 -36
  251. package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
  252. package/dist/themes/threads/timeline/groupByDate.js +0 -22
  253. package/dist/themes/threads/timeline/timelineMore.js +0 -107
  254. package/src/lib/__tests__/theme-components.test.ts +0 -105
  255. package/src/lib/theme-components.ts +0 -65
  256. package/src/routes/dash/navigation.tsx +0 -317
  257. package/src/theme/components/index.ts +0 -23
  258. package/src/theme/index.ts +0 -22
  259. package/src/theme/layouts/index.ts +0 -7
  260. package/src/themes/threads/ThreadsSiteLayout.tsx +0 -194
  261. package/src/themes/threads/index.ts +0 -100
  262. package/src/themes/threads/style.css +0 -336
  263. package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
  264. package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
  265. package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
  266. package/src/themes/threads/timeline/groupByDate.ts +0 -30
  267. package/src/themes/threads/timeline/timelineMore.tsx +0 -130
  268. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  269. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  270. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  271. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  272. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  273. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  274. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  275. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  276. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  277. /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Pages API Routes
3
+ */ import { Hono } from "hono";
4
+ import { requireAuthApi } from "../../middleware/auth.js";
5
+ import { z } from "zod";
6
+ import { StatusSchema } from "../../lib/schemas.js";
7
+ export const pagesApiRoutes = new Hono();
8
+ const CreatePageSchema = z.object({
9
+ slug: z.string().min(1),
10
+ title: z.string().optional(),
11
+ body: z.string().optional(),
12
+ status: StatusSchema.optional()
13
+ });
14
+ const UpdatePageSchema = z.object({
15
+ slug: z.string().min(1).optional(),
16
+ title: z.string().nullable().optional(),
17
+ body: z.string().nullable().optional(),
18
+ status: StatusSchema.optional()
19
+ });
20
+ // List pages
21
+ pagesApiRoutes.get("/", async (c)=>{
22
+ const pages = await c.var.services.pages.list();
23
+ return c.json({
24
+ pages
25
+ });
26
+ });
27
+ // Get single page
28
+ pagesApiRoutes.get("/:id", async (c)=>{
29
+ const id = parseInt(c.req.param("id"), 10);
30
+ if (isNaN(id)) return c.json({
31
+ error: "Invalid ID"
32
+ }, 400);
33
+ const page = await c.var.services.pages.getById(id);
34
+ if (!page) return c.json({
35
+ error: "Not found"
36
+ }, 404);
37
+ return c.json(page);
38
+ });
39
+ // Create page (requires auth)
40
+ pagesApiRoutes.post("/", requireAuthApi(), async (c)=>{
41
+ const rawBody = await c.req.json();
42
+ const parseResult = CreatePageSchema.safeParse(rawBody);
43
+ if (!parseResult.success) {
44
+ return c.json({
45
+ error: "Validation failed",
46
+ details: parseResult.error.flatten()
47
+ }, 400);
48
+ }
49
+ const body = parseResult.data;
50
+ const page = await c.var.services.pages.create({
51
+ slug: body.slug,
52
+ title: body.title,
53
+ body: body.body,
54
+ status: body.status
55
+ });
56
+ return c.json(page, 201);
57
+ });
58
+ // Update page (requires auth)
59
+ pagesApiRoutes.put("/:id", requireAuthApi(), async (c)=>{
60
+ const id = parseInt(c.req.param("id"), 10);
61
+ if (isNaN(id)) return c.json({
62
+ error: "Invalid ID"
63
+ }, 400);
64
+ const rawBody = await c.req.json();
65
+ const parseResult = UpdatePageSchema.safeParse(rawBody);
66
+ if (!parseResult.success) {
67
+ return c.json({
68
+ error: "Validation failed",
69
+ details: parseResult.error.flatten()
70
+ }, 400);
71
+ }
72
+ const page = await c.var.services.pages.update(id, parseResult.data);
73
+ if (!page) return c.json({
74
+ error: "Not found"
75
+ }, 404);
76
+ return c.json(page);
77
+ });
78
+ // Delete page (requires auth)
79
+ pagesApiRoutes.delete("/:id", requireAuthApi(), async (c)=>{
80
+ const id = parseInt(c.req.param("id"), 10);
81
+ if (isNaN(id)) return c.json({
82
+ error: "Invalid ID"
83
+ }, 400);
84
+ const success = await c.var.services.pages.delete(id);
85
+ if (!success) return c.json({
86
+ error: "Not found"
87
+ }, 404);
88
+ return c.json({
89
+ success: true
90
+ });
91
+ });
@@ -10,7 +10,7 @@ export const postsApiRoutes = new Hono();
10
10
  * Converts a Media record to a MediaAttachment API response shape.
11
11
  */ function toMediaAttachment(m, r2PublicUrl, imageTransformUrl, s3PublicUrl) {
12
12
  const publicUrl = getPublicUrlForProvider(m.provider, r2PublicUrl, s3PublicUrl);
13
- const url = getMediaUrl(m.id, m.storageKey, publicUrl);
13
+ const url = getMediaUrl(m.storageKey, publicUrl);
14
14
  const previewUrl = getImageUrl(url, imageTransformUrl, {
15
15
  width: 400,
16
16
  quality: 80,
@@ -110,7 +110,7 @@ postsApiRoutes.post("/", requireAuthApi(), async (c)=>{
110
110
  format: body.format,
111
111
  title: body.title,
112
112
  body: body.body,
113
- slug: body.slug || undefined,
113
+ path: body.path || undefined,
114
114
  status: body.status,
115
115
  featured: body.featured,
116
116
  pinned: body.pinned,
@@ -173,7 +173,7 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c)=>{
173
173
  format: body.format,
174
174
  title: body.title,
175
175
  body: body.body,
176
- slug: body.slug,
176
+ path: body.path,
177
177
  status: body.status,
178
178
  featured: body.featured,
179
179
  pinned: body.pinned,
@@ -31,10 +31,10 @@ searchApiRoutes.get("/", async (c)=>{
31
31
  id: sqid.encode(r.post.id),
32
32
  format: r.post.format,
33
33
  title: r.post.title,
34
- slug: r.post.slug,
34
+ path: r.post.path,
35
35
  snippet: r.snippet,
36
36
  publishedAt: r.post.publishedAt,
37
- url: r.post.slug ? `/${r.post.slug}` : `/p/${sqid.encode(r.post.id)}`
37
+ url: r.post.path ? `/${r.post.path}` : `/p/${sqid.encode(r.post.id)}`
38
38
  })),
39
39
  count: results.length
40
40
  });
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Settings API Routes
3
+ */ import { Hono } from "hono";
4
+ import { requireAuthApi } from "../../middleware/auth.js";
5
+ import { CONFIG_FIELDS } from "../../types.js";
6
+ import { z } from "zod";
7
+ export const settingsApiRoutes = new Hono();
8
+ /** Config keys that can be modified via the settings API */ const editableKeys = Object.entries(CONFIG_FIELDS).filter(([, field])=>!field.envOnly).map(([key])=>key);
9
+ const UpdateSettingsSchema = z.record(z.string(), z.string());
10
+ // Get all settings (requires auth)
11
+ settingsApiRoutes.get("/", requireAuthApi(), async (c)=>{
12
+ const allSettings = await c.var.services.settings.getAll();
13
+ // Include default values for editable keys not yet stored in DB
14
+ const result = {};
15
+ for (const key of editableKeys){
16
+ result[key] = allSettings[key] ?? CONFIG_FIELDS[key].defaultValue;
17
+ }
18
+ return c.json({
19
+ settings: result
20
+ });
21
+ });
22
+ // Update settings (requires auth)
23
+ settingsApiRoutes.put("/", requireAuthApi(), async (c)=>{
24
+ const rawBody = await c.req.json();
25
+ const parseResult = UpdateSettingsSchema.safeParse(rawBody);
26
+ if (!parseResult.success) {
27
+ return c.json({
28
+ error: "Validation failed",
29
+ details: parseResult.error.flatten()
30
+ }, 400);
31
+ }
32
+ const updates = parseResult.data;
33
+ // Filter to only editable keys
34
+ const filteredUpdates = {};
35
+ const rejectedKeys = [];
36
+ for (const [key, value] of Object.entries(updates)){
37
+ if (editableKeys.includes(key)) {
38
+ filteredUpdates[key] = value;
39
+ } else {
40
+ rejectedKeys.push(key);
41
+ }
42
+ }
43
+ if (rejectedKeys.length > 0 && Object.keys(filteredUpdates).length === 0) {
44
+ return c.json({
45
+ error: "None of the provided keys are editable",
46
+ rejectedKeys
47
+ }, 400);
48
+ }
49
+ if (Object.keys(filteredUpdates).length > 0) {
50
+ // Settings service expects SettingsKey, but our ConfigKeys that are
51
+ // editable (SITE_NAME, SITE_DESCRIPTION, SITE_LANGUAGE) are valid SettingsKeys
52
+ for (const [key, value] of Object.entries(filteredUpdates)){
53
+ await c.var.services.settings.set(key, value);
54
+ }
55
+ }
56
+ // Return updated state
57
+ const allSettings = await c.var.services.settings.getAll();
58
+ const result = {};
59
+ for (const key of editableKeys){
60
+ result[key] = allSettings[key] ?? CONFIG_FIELDS[key].defaultValue;
61
+ }
62
+ return c.json({
63
+ settings: result,
64
+ ...rejectedKeys.length > 0 && {
65
+ rejectedKeys
66
+ }
67
+ });
68
+ });
@@ -15,7 +15,7 @@ uploadApiRoutes.use("*", requireAuthApi());
15
15
  /**
16
16
  * Render a media card HTML string for SSE response
17
17
  */ function renderMediaCard(media, publicUrl, imageTransformUrl) {
18
- const fullUrl = getMediaUrl(media.id, media.storageKey, publicUrl);
18
+ const fullUrl = getMediaUrl(media.storageKey, publicUrl);
19
19
  const thumbnailUrl = getImageUrl(fullUrl, imageTransformUrl, {
20
20
  width: 300,
21
21
  quality: 80,
@@ -180,7 +180,7 @@ uploadApiRoutes.post("/", async (c)=>{
180
180
  // JSON response for API clients
181
181
  const provider = c.env.STORAGE_DRIVER || "r2";
182
182
  const mediaPublicUrl = getPublicUrlForProvider(provider, c.env.R2_PUBLIC_URL, c.env.S3_PUBLIC_URL);
183
- const publicUrl = getMediaUrl(media.id, storageKey, mediaPublicUrl);
183
+ const publicUrl = getMediaUrl(storageKey, mediaPublicUrl);
184
184
  return c.json({
185
185
  id: media.id,
186
186
  filename: media.filename,
@@ -212,7 +212,7 @@ uploadApiRoutes.get("/", async (c)=>{
212
212
  media: mediaList.map((m)=>({
213
213
  id: m.id,
214
214
  filename: m.filename,
215
- url: getMediaUrl(m.id, m.storageKey, getPublicUrlForProvider(m.provider, r2PublicUrl, s3PublicUrl)),
215
+ url: getMediaUrl(m.storageKey, getPublicUrlForProvider(m.provider, r2PublicUrl, s3PublicUrl)),
216
216
  mimeType: m.mimeType,
217
217
  size: m.size,
218
218
  createdAt: m.createdAt
@@ -0,0 +1,221 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
2
+ /**
3
+ * Password Reset Routes
4
+ *
5
+ * One-time token-based password reset flow.
6
+ */ import { Hono } from "hono";
7
+ import { useLingui as $_useLingui } from "@jant/core/i18n";
8
+ import { hashPassword } from "better-auth/crypto";
9
+ import { BaseLayout } from "../../ui/layouts/BaseLayout.js";
10
+ import { dsRedirect, dsToast } from "../../lib/sse.js";
11
+ import { SETTINGS_KEYS } from "../../lib/constants.js";
12
+ import { ResetPasswordSchema } from "../../lib/schemas.js";
13
+ const ResetContent = ({ token })=>{
14
+ const { i18n: $__i18n, _: $__ } = $_useLingui();
15
+ const signals = JSON.stringify({
16
+ password: "",
17
+ confirmPassword: "",
18
+ token
19
+ }).replace(/</g, "\\u003c");
20
+ return /*#__PURE__*/ _jsx("div", {
21
+ class: "min-h-screen flex items-center justify-center",
22
+ children: /*#__PURE__*/ _jsxs("div", {
23
+ class: "card max-w-md w-full",
24
+ children: [
25
+ /*#__PURE__*/ _jsxs("header", {
26
+ children: [
27
+ /*#__PURE__*/ _jsx("h2", {
28
+ children: $__i18n._({
29
+ id: "KbS2K9",
30
+ message: "Reset Password"
31
+ })
32
+ }),
33
+ /*#__PURE__*/ _jsx("p", {
34
+ children: $__i18n._({
35
+ id: "hWOZIv",
36
+ message: "Enter your new password."
37
+ })
38
+ })
39
+ ]
40
+ }),
41
+ /*#__PURE__*/ _jsx("section", {
42
+ children: /*#__PURE__*/ _jsxs("form", {
43
+ "data-signals": signals,
44
+ "data-on:submit__prevent": "@post('/reset')",
45
+ "data-indicator": "_loading",
46
+ class: "flex flex-col gap-4",
47
+ children: [
48
+ /*#__PURE__*/ _jsxs("div", {
49
+ class: "field",
50
+ children: [
51
+ /*#__PURE__*/ _jsx("label", {
52
+ class: "label",
53
+ children: $__i18n._({
54
+ id: "7vhWI8",
55
+ message: "New Password"
56
+ })
57
+ }),
58
+ /*#__PURE__*/ _jsx("input", {
59
+ type: "password",
60
+ "data-bind": "password",
61
+ class: "input",
62
+ required: true,
63
+ minLength: 8,
64
+ autocomplete: "new-password"
65
+ })
66
+ ]
67
+ }),
68
+ /*#__PURE__*/ _jsxs("div", {
69
+ class: "field",
70
+ children: [
71
+ /*#__PURE__*/ _jsx("label", {
72
+ class: "label",
73
+ children: $__i18n._({
74
+ id: "p2/GCq",
75
+ message: "Confirm Password"
76
+ })
77
+ }),
78
+ /*#__PURE__*/ _jsx("input", {
79
+ type: "password",
80
+ "data-bind": "confirmPassword",
81
+ class: "input",
82
+ required: true,
83
+ minLength: 8,
84
+ autocomplete: "new-password"
85
+ })
86
+ ]
87
+ }),
88
+ /*#__PURE__*/ _jsxs("button", {
89
+ type: "submit",
90
+ class: "btn",
91
+ "data-attr:disabled": "$_loading",
92
+ children: [
93
+ /*#__PURE__*/ _jsx("svg", {
94
+ "data-show": "$_loading",
95
+ style: "display:none",
96
+ class: "animate-spin size-4",
97
+ xmlns: "http://www.w3.org/2000/svg",
98
+ viewBox: "0 0 24 24",
99
+ fill: "none",
100
+ stroke: "currentColor",
101
+ "stroke-width": "2",
102
+ "stroke-linecap": "round",
103
+ "stroke-linejoin": "round",
104
+ role: "status",
105
+ children: /*#__PURE__*/ _jsx("path", {
106
+ d: "M21 12a9 9 0 1 1-6.219-8.56"
107
+ })
108
+ }),
109
+ $__i18n._({
110
+ id: "KbS2K9",
111
+ message: "Reset Password"
112
+ })
113
+ ]
114
+ })
115
+ ]
116
+ })
117
+ })
118
+ ]
119
+ })
120
+ });
121
+ };
122
+ const ResetErrorContent = ()=>{
123
+ const { i18n: $__i18n, _: $__ } = $_useLingui();
124
+ return /*#__PURE__*/ _jsx("div", {
125
+ class: "min-h-screen flex items-center justify-center",
126
+ children: /*#__PURE__*/ _jsxs("div", {
127
+ class: "card max-w-md w-full",
128
+ children: [
129
+ /*#__PURE__*/ _jsx("header", {
130
+ children: /*#__PURE__*/ _jsx("h2", {
131
+ children: $__i18n._({
132
+ id: "7aECQB",
133
+ message: "Invalid or Expired Link"
134
+ })
135
+ })
136
+ }),
137
+ /*#__PURE__*/ _jsx("section", {
138
+ children: /*#__PURE__*/ _jsx("p", {
139
+ class: "text-muted-foreground",
140
+ children: $__i18n._({
141
+ id: "GbVAnd",
142
+ message: "This password reset link is invalid or has expired. Please generate a new one."
143
+ })
144
+ })
145
+ })
146
+ ]
147
+ })
148
+ });
149
+ };
150
+ /**
151
+ * Validate a password reset token against the stored value.
152
+ * Returns true if the token is valid and not expired.
153
+ */ async function validateResetToken(settings, token) {
154
+ const stored = await settings.get(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
155
+ if (!stored) return false;
156
+ const separatorIndex = stored.lastIndexOf(":");
157
+ const storedToken = stored.substring(0, separatorIndex);
158
+ const expiry = parseInt(stored.substring(separatorIndex + 1), 10);
159
+ const now = Math.floor(Date.now() / 1000);
160
+ return token === storedToken && now <= expiry;
161
+ }
162
+ export const resetRoutes = new Hono();
163
+ resetRoutes.get("/reset", async (c)=>{
164
+ const token = c.req.query("token");
165
+ if (!token) {
166
+ return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
167
+ title: "Reset Password - Jant",
168
+ c: c,
169
+ children: /*#__PURE__*/ _jsx(ResetErrorContent, {})
170
+ }));
171
+ }
172
+ const isValid = await validateResetToken(c.var.services.settings, token);
173
+ if (!isValid) {
174
+ return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
175
+ title: "Reset Password - Jant",
176
+ c: c,
177
+ children: /*#__PURE__*/ _jsx(ResetErrorContent, {})
178
+ }));
179
+ }
180
+ return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
181
+ title: "Reset Password - Jant",
182
+ c: c,
183
+ children: /*#__PURE__*/ _jsx(ResetContent, {
184
+ token: token
185
+ })
186
+ }));
187
+ });
188
+ resetRoutes.post("/reset", async (c)=>{
189
+ const body = await c.req.json();
190
+ const parsed = ResetPasswordSchema.safeParse(body);
191
+ if (!parsed.success) {
192
+ const msg = parsed.error.errors[0]?.message ?? "Invalid input";
193
+ return dsToast(msg, "error");
194
+ }
195
+ const { password, token } = parsed.data;
196
+ // Validate token
197
+ const isValid = await validateResetToken(c.var.services.settings, token);
198
+ if (!isValid) {
199
+ return dsToast("Invalid or expired reset link.", "error");
200
+ }
201
+ try {
202
+ const hashedPassword = await hashPassword(password);
203
+ const db = c.env.DB.withSession();
204
+ // Get admin user
205
+ const userResult = await db.prepare("SELECT id FROM user LIMIT 1").first();
206
+ if (!userResult) {
207
+ return dsToast("No user account found.", "error");
208
+ }
209
+ // Update password
210
+ await db.prepare("UPDATE account SET password = ? WHERE user_id = ? AND provider_id = 'credential'").bind(hashedPassword, userResult.id).run();
211
+ // Delete all sessions
212
+ await db.prepare("DELETE FROM session WHERE user_id = ?").bind(userResult.id).run();
213
+ // Delete the reset token
214
+ await c.var.services.settings.remove(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
215
+ return dsRedirect("/signin?reset");
216
+ } catch (err) {
217
+ // eslint-disable-next-line no-console -- Error logging is intentional
218
+ console.error("Password reset error:", err);
219
+ return dsToast("Failed to reset password.", "error");
220
+ }
221
+ });
@@ -0,0 +1,194 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
2
+ /**
3
+ * Setup Routes
4
+ *
5
+ * Initial admin account creation during first-time setup.
6
+ */ import { Hono } from "hono";
7
+ import { useLingui as $_useLingui } from "@jant/core/i18n";
8
+ import { BaseLayout } from "../../ui/layouts/BaseLayout.js";
9
+ import { dsRedirect, dsToast } from "../../lib/sse.js";
10
+ import { SetupSchema } from "../../lib/schemas.js";
11
+ import { mapIanaToTimezone } from "../../lib/timezones.js";
12
+ const SetupContent = ()=>{
13
+ const { i18n: $__i18n, _: $__ } = $_useLingui();
14
+ return /*#__PURE__*/ _jsx("div", {
15
+ class: "min-h-screen flex items-center justify-center",
16
+ children: /*#__PURE__*/ _jsxs("div", {
17
+ class: "card max-w-md w-full",
18
+ children: [
19
+ /*#__PURE__*/ _jsxs("header", {
20
+ children: [
21
+ /*#__PURE__*/ _jsx("h2", {
22
+ children: $__i18n._({
23
+ id: "GorKul",
24
+ message: "Welcome to Jant"
25
+ })
26
+ }),
27
+ /*#__PURE__*/ _jsx("p", {
28
+ children: $__i18n._({
29
+ id: "GX2VMa",
30
+ message: "Create your admin account."
31
+ })
32
+ })
33
+ ]
34
+ }),
35
+ /*#__PURE__*/ _jsx("section", {
36
+ children: /*#__PURE__*/ _jsxs("form", {
37
+ "data-signals": "{name: '', email: '', password: '', _timezone: ''}",
38
+ "data-init": "$_timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || ''",
39
+ "data-on:submit__prevent": "@post('/setup')",
40
+ "data-indicator": "_loading",
41
+ class: "flex flex-col gap-4",
42
+ children: [
43
+ /*#__PURE__*/ _jsxs("div", {
44
+ class: "field",
45
+ children: [
46
+ /*#__PURE__*/ _jsx("label", {
47
+ class: "label",
48
+ children: $__i18n._({
49
+ id: "/Rj5P4",
50
+ message: "Your Name"
51
+ })
52
+ }),
53
+ /*#__PURE__*/ _jsx("input", {
54
+ type: "text",
55
+ "data-bind": "name",
56
+ class: "input",
57
+ required: true,
58
+ placeholder: "John Doe"
59
+ })
60
+ ]
61
+ }),
62
+ /*#__PURE__*/ _jsxs("div", {
63
+ class: "field",
64
+ children: [
65
+ /*#__PURE__*/ _jsx("label", {
66
+ class: "label",
67
+ children: $__i18n._({
68
+ id: "O3oNi5",
69
+ message: "Email"
70
+ })
71
+ }),
72
+ /*#__PURE__*/ _jsx("input", {
73
+ type: "email",
74
+ "data-bind": "email",
75
+ class: "input",
76
+ required: true,
77
+ placeholder: "you@example.com"
78
+ })
79
+ ]
80
+ }),
81
+ /*#__PURE__*/ _jsxs("div", {
82
+ class: "field",
83
+ children: [
84
+ /*#__PURE__*/ _jsx("label", {
85
+ class: "label",
86
+ children: $__i18n._({
87
+ id: "8ZsakT",
88
+ message: "Password"
89
+ })
90
+ }),
91
+ /*#__PURE__*/ _jsx("input", {
92
+ type: "password",
93
+ "data-bind": "password",
94
+ class: "input",
95
+ required: true,
96
+ minLength: 8
97
+ })
98
+ ]
99
+ }),
100
+ /*#__PURE__*/ _jsxs("button", {
101
+ type: "submit",
102
+ class: "btn",
103
+ "data-attr:disabled": "$_loading",
104
+ children: [
105
+ /*#__PURE__*/ _jsx("svg", {
106
+ "data-show": "$_loading",
107
+ style: "display:none",
108
+ class: "animate-spin size-4",
109
+ xmlns: "http://www.w3.org/2000/svg",
110
+ viewBox: "0 0 24 24",
111
+ fill: "none",
112
+ stroke: "currentColor",
113
+ "stroke-width": "2",
114
+ "stroke-linecap": "round",
115
+ "stroke-linejoin": "round",
116
+ role: "status",
117
+ children: /*#__PURE__*/ _jsx("path", {
118
+ d: "M21 12a9 9 0 1 1-6.219-8.56"
119
+ })
120
+ }),
121
+ $__i18n._({
122
+ id: "EGwzOK",
123
+ message: "Complete Setup"
124
+ })
125
+ ]
126
+ })
127
+ ]
128
+ })
129
+ })
130
+ ]
131
+ })
132
+ });
133
+ };
134
+ export const setupRoutes = new Hono();
135
+ setupRoutes.get("/setup", async (c)=>{
136
+ const isComplete = await c.var.services.settings.isOnboardingComplete();
137
+ if (isComplete) return c.redirect("/");
138
+ return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
139
+ title: "Setup - Jant",
140
+ c: c,
141
+ children: /*#__PURE__*/ _jsx(SetupContent, {})
142
+ }));
143
+ });
144
+ setupRoutes.post("/setup", async (c)=>{
145
+ const isComplete = await c.var.services.settings.isOnboardingComplete();
146
+ if (isComplete) return c.redirect("/");
147
+ const body = await c.req.json();
148
+ const parsed = SetupSchema.safeParse(body);
149
+ const browserTimezone = body._timezone;
150
+ if (!parsed.success) {
151
+ const msg = parsed.error.errors[0]?.message ?? "Invalid input";
152
+ return dsToast(msg, "error");
153
+ }
154
+ const { name, email, password } = parsed.data;
155
+ if (!c.var.auth) {
156
+ return dsToast("AUTH_SECRET not configured", "error");
157
+ }
158
+ try {
159
+ const signUpResponse = await c.var.auth.api.signUpEmail({
160
+ body: {
161
+ name,
162
+ email,
163
+ password
164
+ }
165
+ });
166
+ if (!signUpResponse || "error" in signUpResponse) {
167
+ return dsToast("Failed to create account", "error");
168
+ }
169
+ await c.var.services.settings.completeOnboarding();
170
+ // Save auto-detected timezone
171
+ if (browserTimezone) {
172
+ const tz = mapIanaToTimezone(browserTimezone);
173
+ if (tz !== "UTC") {
174
+ await c.var.services.settings.set("TIME_ZONE", tz);
175
+ }
176
+ }
177
+ // Seed default navigation items
178
+ await c.var.services.navItems.create({
179
+ type: "link",
180
+ label: "Featured",
181
+ url: "/featured"
182
+ });
183
+ await c.var.services.navItems.create({
184
+ type: "link",
185
+ label: "Collections",
186
+ url: "/collections"
187
+ });
188
+ return dsRedirect("/signin?setup");
189
+ } catch (err) {
190
+ // eslint-disable-next-line no-console -- Error logging is intentional
191
+ console.error("Setup error:", err);
192
+ return dsToast("Failed to create account", "error");
193
+ }
194
+ });