@jant/core 0.3.25 → 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 (131) hide show
  1. package/dist/app.js +67 -562
  2. package/dist/client.js +1 -0
  3. package/dist/i18n/locales/en.js +1 -1
  4. package/dist/i18n/locales/zh-Hans.js +1 -1
  5. package/dist/i18n/locales/zh-Hant.js +1 -1
  6. package/dist/lib/avatar-upload.js +134 -0
  7. package/dist/lib/config.js +39 -0
  8. package/dist/lib/constants.js +10 -10
  9. package/dist/lib/favicon.js +102 -0
  10. package/dist/lib/image.js +13 -17
  11. package/dist/lib/media-helpers.js +2 -2
  12. package/dist/lib/navigation.js +23 -3
  13. package/dist/lib/render.js +10 -1
  14. package/dist/lib/schemas.js +31 -0
  15. package/dist/lib/timezones.js +388 -0
  16. package/dist/lib/view.js +1 -1
  17. package/dist/routes/api/posts.js +1 -1
  18. package/dist/routes/api/upload.js +3 -3
  19. package/dist/routes/auth/reset.js +221 -0
  20. package/dist/routes/auth/setup.js +194 -0
  21. package/dist/routes/auth/signin.js +176 -0
  22. package/dist/routes/dash/collections.js +23 -415
  23. package/dist/routes/dash/media.js +12 -392
  24. package/dist/routes/dash/pages.js +7 -330
  25. package/dist/routes/dash/redirects.js +18 -12
  26. package/dist/routes/dash/settings.js +198 -577
  27. package/dist/routes/feed/rss.js +2 -1
  28. package/dist/routes/feed/sitemap.js +4 -2
  29. package/dist/routes/pages/featured.js +5 -1
  30. package/dist/routes/pages/home.js +26 -1
  31. package/dist/routes/pages/latest.js +45 -0
  32. package/dist/services/post.js +30 -50
  33. package/dist/types/bindings.js +3 -0
  34. package/dist/types/config.js +147 -0
  35. package/dist/types/constants.js +27 -0
  36. package/dist/types/entities.js +3 -0
  37. package/dist/types/operations.js +3 -0
  38. package/dist/types/props.js +3 -0
  39. package/dist/types/views.js +5 -0
  40. package/dist/types.js +8 -111
  41. package/dist/ui/color-themes.js +33 -33
  42. package/dist/ui/compose/ComposeDialog.js +36 -21
  43. package/dist/ui/dash/PageForm.js +21 -15
  44. package/dist/ui/dash/PostForm.js +22 -16
  45. package/dist/ui/dash/collections/CollectionForm.js +152 -0
  46. package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
  47. package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
  48. package/dist/ui/dash/media/MediaListContent.js +166 -0
  49. package/dist/ui/dash/media/ViewMediaContent.js +212 -0
  50. package/dist/ui/dash/pages/LinkFormContent.js +130 -0
  51. package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
  52. package/dist/ui/dash/settings/AccountContent.js +209 -0
  53. package/dist/ui/dash/settings/AppearanceContent.js +259 -0
  54. package/dist/ui/dash/settings/GeneralContent.js +536 -0
  55. package/dist/ui/dash/settings/SettingsNav.js +41 -0
  56. package/dist/ui/font-themes.js +36 -0
  57. package/dist/ui/layouts/BaseLayout.js +24 -2
  58. package/dist/ui/layouts/SiteLayout.js +47 -19
  59. package/package.json +1 -1
  60. package/src/app.tsx +93 -553
  61. package/src/client.ts +1 -0
  62. package/src/i18n/locales/en.po +240 -175
  63. package/src/i18n/locales/en.ts +1 -1
  64. package/src/i18n/locales/zh-Hans.po +240 -175
  65. package/src/i18n/locales/zh-Hans.ts +1 -1
  66. package/src/i18n/locales/zh-Hant.po +240 -175
  67. package/src/i18n/locales/zh-Hant.ts +1 -1
  68. package/src/lib/__tests__/config.test.ts +192 -0
  69. package/src/lib/__tests__/favicon.test.ts +151 -0
  70. package/src/lib/__tests__/image.test.ts +2 -6
  71. package/src/lib/__tests__/timezones.test.ts +61 -0
  72. package/src/lib/__tests__/view.test.ts +2 -2
  73. package/src/lib/avatar-upload.ts +165 -0
  74. package/src/lib/config.ts +47 -0
  75. package/src/lib/constants.ts +19 -11
  76. package/src/lib/favicon.ts +115 -0
  77. package/src/lib/image.ts +13 -21
  78. package/src/lib/media-helpers.ts +2 -2
  79. package/src/lib/navigation.ts +33 -2
  80. package/src/lib/render.tsx +15 -1
  81. package/src/lib/schemas.ts +39 -0
  82. package/src/lib/timezones.ts +325 -0
  83. package/src/lib/view.ts +1 -1
  84. package/src/routes/api/posts.ts +1 -1
  85. package/src/routes/api/upload.ts +2 -3
  86. package/src/routes/auth/reset.tsx +239 -0
  87. package/src/routes/auth/setup.tsx +189 -0
  88. package/src/routes/auth/signin.tsx +163 -0
  89. package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
  90. package/src/routes/dash/collections.tsx +17 -366
  91. package/src/routes/dash/media.tsx +12 -414
  92. package/src/routes/dash/pages.tsx +8 -348
  93. package/src/routes/dash/redirects.tsx +20 -14
  94. package/src/routes/dash/settings.tsx +243 -534
  95. package/src/routes/feed/__tests__/rss.test.ts +141 -0
  96. package/src/routes/feed/rss.ts +3 -1
  97. package/src/routes/feed/sitemap.ts +4 -2
  98. package/src/routes/pages/featured.tsx +7 -1
  99. package/src/routes/pages/home.tsx +25 -2
  100. package/src/routes/pages/latest.tsx +59 -0
  101. package/src/services/post.ts +34 -66
  102. package/src/styles/components.css +0 -65
  103. package/src/styles/tokens.css +1 -1
  104. package/src/styles/ui.css +24 -40
  105. package/src/types/bindings.ts +30 -0
  106. package/src/types/config.ts +183 -0
  107. package/src/types/constants.ts +26 -0
  108. package/src/types/entities.ts +109 -0
  109. package/src/types/operations.ts +88 -0
  110. package/src/types/props.ts +115 -0
  111. package/src/types/views.ts +172 -0
  112. package/src/types.ts +8 -644
  113. package/src/ui/__tests__/font-themes.test.ts +34 -0
  114. package/src/ui/color-themes.ts +34 -34
  115. package/src/ui/compose/ComposeDialog.tsx +40 -21
  116. package/src/ui/dash/PageForm.tsx +25 -19
  117. package/src/ui/dash/PostForm.tsx +26 -20
  118. package/src/ui/dash/collections/CollectionForm.tsx +153 -0
  119. package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
  120. package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
  121. package/src/ui/dash/media/MediaListContent.tsx +201 -0
  122. package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
  123. package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
  124. package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
  125. package/src/ui/dash/settings/AccountContent.tsx +176 -0
  126. package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
  127. package/src/ui/dash/settings/GeneralContent.tsx +533 -0
  128. package/src/ui/dash/settings/SettingsNav.tsx +56 -0
  129. package/src/ui/font-themes.ts +54 -0
  130. package/src/ui/layouts/BaseLayout.tsx +17 -0
  131. package/src/ui/layouts/SiteLayout.tsx +45 -31
@@ -99,6 +99,37 @@ import { FORMATS, STATUSES, SORT_ORDERS, NAV_ITEM_TYPES, MAX_MEDIA_ATTACHMENTS }
99
99
  /**
100
100
  * API request body schema for updating a collection
101
101
  */ export const UpdateCollectionSchema = CreateCollectionSchema.partial();
102
+ // =============================================================================
103
+ // Auth Schemas
104
+ // =============================================================================
105
+ /**
106
+ * Setup form validation schema
107
+ */ export const SetupSchema = z.object({
108
+ name: z.string().min(1, "Name is required"),
109
+ email: z.string().email("Invalid email address"),
110
+ password: z.string().min(8, "Password must be at least 8 characters")
111
+ });
112
+ /**
113
+ * Sign-in form validation schema
114
+ */ export const SigninSchema = z.object({
115
+ email: z.string().email("Invalid email address"),
116
+ password: z.string().min(1, "Password is required")
117
+ });
118
+ /**
119
+ * Password reset form validation schema
120
+ */ export const ResetPasswordSchema = z.object({
121
+ password: z.string().min(8, "Password must be at least 8 characters"),
122
+ confirmPassword: z.string().min(1),
123
+ token: z.string().min(1)
124
+ }).refine((d)=>d.password === d.confirmPassword, {
125
+ message: "Passwords do not match",
126
+ path: [
127
+ "confirmPassword"
128
+ ]
129
+ });
130
+ // =============================================================================
131
+ // Form Data Helpers
132
+ // =============================================================================
102
133
  /**
103
134
  * Form data helper: safely parse a FormData value with a schema
104
135
  *
@@ -0,0 +1,388 @@
1
+ /**
2
+ * Timezone List
3
+ *
4
+ * Provides a curated list of timezones for the settings UI
5
+ * and a helper to map IANA timezone names to our list entries.
6
+ */ export const TIMEZONES = [
7
+ {
8
+ value: "International Date Line West",
9
+ label: "(UTC-12:00) International Date Line West",
10
+ offset: "-12:00",
11
+ iana: [
12
+ "Etc/GMT+12"
13
+ ]
14
+ },
15
+ {
16
+ value: "American Samoa",
17
+ label: "(UTC-11:00) American Samoa",
18
+ offset: "-11:00",
19
+ iana: [
20
+ "Pacific/Pago_Pago",
21
+ "Pacific/Midway",
22
+ "Etc/GMT+11"
23
+ ]
24
+ },
25
+ {
26
+ value: "Hawaii",
27
+ label: "(UTC-10:00) Hawaii",
28
+ offset: "-10:00",
29
+ iana: [
30
+ "Pacific/Honolulu",
31
+ "Etc/GMT+10"
32
+ ]
33
+ },
34
+ {
35
+ value: "Alaska",
36
+ label: "(UTC-09:00) Alaska",
37
+ offset: "-09:00",
38
+ iana: [
39
+ "America/Anchorage",
40
+ "America/Juneau",
41
+ "America/Nome"
42
+ ]
43
+ },
44
+ {
45
+ value: "Pacific Time (US & Canada)",
46
+ label: "(UTC-08:00) Pacific Time (US & Canada)",
47
+ offset: "-08:00",
48
+ iana: [
49
+ "America/Los_Angeles",
50
+ "America/Vancouver",
51
+ "America/Tijuana"
52
+ ]
53
+ },
54
+ {
55
+ value: "Mountain Time (US & Canada)",
56
+ label: "(UTC-07:00) Mountain Time (US & Canada)",
57
+ offset: "-07:00",
58
+ iana: [
59
+ "America/Denver",
60
+ "America/Edmonton",
61
+ "America/Phoenix",
62
+ "America/Boise"
63
+ ]
64
+ },
65
+ {
66
+ value: "Central Time (US & Canada)",
67
+ label: "(UTC-06:00) Central Time (US & Canada)",
68
+ offset: "-06:00",
69
+ iana: [
70
+ "America/Chicago",
71
+ "America/Winnipeg",
72
+ "America/Mexico_City",
73
+ "America/Guatemala"
74
+ ]
75
+ },
76
+ {
77
+ value: "Eastern Time (US & Canada)",
78
+ label: "(UTC-05:00) Eastern Time (US & Canada)",
79
+ offset: "-05:00",
80
+ iana: [
81
+ "America/New_York",
82
+ "America/Toronto",
83
+ "America/Detroit",
84
+ "America/Indiana/Indianapolis",
85
+ "America/Bogota",
86
+ "America/Lima"
87
+ ]
88
+ },
89
+ {
90
+ value: "Atlantic Time (Canada)",
91
+ label: "(UTC-04:00) Atlantic Time (Canada)",
92
+ offset: "-04:00",
93
+ iana: [
94
+ "America/Halifax",
95
+ "America/Caracas",
96
+ "America/Santiago",
97
+ "America/La_Paz"
98
+ ]
99
+ },
100
+ {
101
+ value: "Newfoundland",
102
+ label: "(UTC-03:30) Newfoundland",
103
+ offset: "-03:30",
104
+ iana: [
105
+ "America/St_Johns"
106
+ ]
107
+ },
108
+ {
109
+ value: "Buenos Aires",
110
+ label: "(UTC-03:00) Buenos Aires",
111
+ offset: "-03:00",
112
+ iana: [
113
+ "America/Argentina/Buenos_Aires",
114
+ "America/Sao_Paulo",
115
+ "America/Montevideo"
116
+ ]
117
+ },
118
+ {
119
+ value: "Mid-Atlantic",
120
+ label: "(UTC-02:00) Mid-Atlantic",
121
+ offset: "-02:00",
122
+ iana: [
123
+ "Atlantic/South_Georgia",
124
+ "Etc/GMT+2"
125
+ ]
126
+ },
127
+ {
128
+ value: "Azores",
129
+ label: "(UTC-01:00) Azores",
130
+ offset: "-01:00",
131
+ iana: [
132
+ "Atlantic/Azores",
133
+ "Atlantic/Cape_Verde"
134
+ ]
135
+ },
136
+ {
137
+ value: "UTC",
138
+ label: "(UTC+00:00) UTC",
139
+ offset: "+00:00",
140
+ iana: [
141
+ "Etc/UTC",
142
+ "UTC",
143
+ "Etc/GMT",
144
+ "Africa/Accra"
145
+ ]
146
+ },
147
+ {
148
+ value: "London",
149
+ label: "(UTC+00:00) London",
150
+ offset: "+00:00",
151
+ iana: [
152
+ "Europe/London",
153
+ "Europe/Dublin",
154
+ "Europe/Lisbon"
155
+ ]
156
+ },
157
+ {
158
+ value: "Central European Time",
159
+ label: "(UTC+01:00) Central European Time",
160
+ offset: "+01:00",
161
+ iana: [
162
+ "Europe/Paris",
163
+ "Europe/Berlin",
164
+ "Europe/Amsterdam",
165
+ "Europe/Rome",
166
+ "Europe/Madrid",
167
+ "Europe/Brussels",
168
+ "Europe/Vienna",
169
+ "Europe/Warsaw",
170
+ "Europe/Prague",
171
+ "Europe/Stockholm",
172
+ "Europe/Oslo",
173
+ "Europe/Copenhagen",
174
+ "Europe/Zurich",
175
+ "Africa/Lagos"
176
+ ]
177
+ },
178
+ {
179
+ value: "Eastern European Time",
180
+ label: "(UTC+02:00) Eastern European Time",
181
+ offset: "+02:00",
182
+ iana: [
183
+ "Europe/Helsinki",
184
+ "Europe/Athens",
185
+ "Europe/Bucharest",
186
+ "Europe/Istanbul",
187
+ "Africa/Cairo",
188
+ "Africa/Johannesburg",
189
+ "Asia/Jerusalem",
190
+ "Asia/Beirut"
191
+ ]
192
+ },
193
+ {
194
+ value: "Moscow",
195
+ label: "(UTC+03:00) Moscow",
196
+ offset: "+03:00",
197
+ iana: [
198
+ "Europe/Moscow",
199
+ "Europe/Minsk",
200
+ "Asia/Baghdad",
201
+ "Asia/Riyadh",
202
+ "Africa/Nairobi",
203
+ "Asia/Kuwait"
204
+ ]
205
+ },
206
+ {
207
+ value: "Tehran",
208
+ label: "(UTC+03:30) Tehran",
209
+ offset: "+03:30",
210
+ iana: [
211
+ "Asia/Tehran"
212
+ ]
213
+ },
214
+ {
215
+ value: "Dubai",
216
+ label: "(UTC+04:00) Dubai",
217
+ offset: "+04:00",
218
+ iana: [
219
+ "Asia/Dubai",
220
+ "Asia/Muscat",
221
+ "Asia/Baku",
222
+ "Asia/Tbilisi"
223
+ ]
224
+ },
225
+ {
226
+ value: "Kabul",
227
+ label: "(UTC+04:30) Kabul",
228
+ offset: "+04:30",
229
+ iana: [
230
+ "Asia/Kabul"
231
+ ]
232
+ },
233
+ {
234
+ value: "Karachi",
235
+ label: "(UTC+05:00) Karachi",
236
+ offset: "+05:00",
237
+ iana: [
238
+ "Asia/Karachi",
239
+ "Asia/Tashkent",
240
+ "Asia/Yekaterinburg"
241
+ ]
242
+ },
243
+ {
244
+ value: "Mumbai",
245
+ label: "(UTC+05:30) Mumbai",
246
+ offset: "+05:30",
247
+ iana: [
248
+ "Asia/Kolkata",
249
+ "Asia/Calcutta",
250
+ "Asia/Colombo"
251
+ ]
252
+ },
253
+ {
254
+ value: "Kathmandu",
255
+ label: "(UTC+05:45) Kathmandu",
256
+ offset: "+05:45",
257
+ iana: [
258
+ "Asia/Kathmandu"
259
+ ]
260
+ },
261
+ {
262
+ value: "Dhaka",
263
+ label: "(UTC+06:00) Dhaka",
264
+ offset: "+06:00",
265
+ iana: [
266
+ "Asia/Dhaka",
267
+ "Asia/Almaty",
268
+ "Asia/Omsk"
269
+ ]
270
+ },
271
+ {
272
+ value: "Yangon",
273
+ label: "(UTC+06:30) Yangon",
274
+ offset: "+06:30",
275
+ iana: [
276
+ "Asia/Yangon",
277
+ "Asia/Rangoon"
278
+ ]
279
+ },
280
+ {
281
+ value: "Bangkok",
282
+ label: "(UTC+07:00) Bangkok",
283
+ offset: "+07:00",
284
+ iana: [
285
+ "Asia/Bangkok",
286
+ "Asia/Jakarta",
287
+ "Asia/Ho_Chi_Minh",
288
+ "Asia/Krasnoyarsk"
289
+ ]
290
+ },
291
+ {
292
+ value: "Beijing",
293
+ label: "(UTC+08:00) Beijing",
294
+ offset: "+08:00",
295
+ iana: [
296
+ "Asia/Shanghai",
297
+ "Asia/Hong_Kong",
298
+ "Asia/Taipei",
299
+ "Asia/Singapore",
300
+ "Asia/Kuala_Lumpur",
301
+ "Asia/Makassar",
302
+ "Asia/Irkutsk",
303
+ "Australia/Perth"
304
+ ]
305
+ },
306
+ {
307
+ value: "Tokyo",
308
+ label: "(UTC+09:00) Tokyo",
309
+ offset: "+09:00",
310
+ iana: [
311
+ "Asia/Tokyo",
312
+ "Asia/Seoul",
313
+ "Asia/Yakutsk"
314
+ ]
315
+ },
316
+ {
317
+ value: "Adelaide",
318
+ label: "(UTC+09:30) Adelaide",
319
+ offset: "+09:30",
320
+ iana: [
321
+ "Australia/Adelaide",
322
+ "Australia/Darwin"
323
+ ]
324
+ },
325
+ {
326
+ value: "Sydney",
327
+ label: "(UTC+10:00) Sydney",
328
+ offset: "+10:00",
329
+ iana: [
330
+ "Australia/Sydney",
331
+ "Australia/Melbourne",
332
+ "Australia/Brisbane",
333
+ "Australia/Hobart",
334
+ "Pacific/Guam",
335
+ "Asia/Vladivostok"
336
+ ]
337
+ },
338
+ {
339
+ value: "Noumea",
340
+ label: "(UTC+11:00) Noumea",
341
+ offset: "+11:00",
342
+ iana: [
343
+ "Pacific/Noumea",
344
+ "Asia/Magadan",
345
+ "Pacific/Guadalcanal"
346
+ ]
347
+ },
348
+ {
349
+ value: "Auckland",
350
+ label: "(UTC+12:00) Auckland",
351
+ offset: "+12:00",
352
+ iana: [
353
+ "Pacific/Auckland",
354
+ "Pacific/Fiji",
355
+ "Asia/Kamchatka"
356
+ ]
357
+ },
358
+ {
359
+ value: "Nuku'alofa",
360
+ label: "(UTC+13:00) Nuku'alofa",
361
+ offset: "+13:00",
362
+ iana: [
363
+ "Pacific/Tongatapu",
364
+ "Pacific/Apia"
365
+ ]
366
+ }
367
+ ];
368
+ /**
369
+ * Maps an IANA timezone name (e.g. "Asia/Shanghai") to our timezone list value
370
+ * (e.g. "Beijing"). Returns "UTC" if no match found.
371
+ *
372
+ * @param iana - IANA timezone identifier from Intl.DateTimeFormat
373
+ * @returns The matching timezone value from TIMEZONES
374
+ *
375
+ * @example
376
+ * ```ts
377
+ * mapIanaToTimezone("Asia/Shanghai"); // "Beijing"
378
+ * mapIanaToTimezone("America/New_York"); // "Eastern Time (US & Canada)"
379
+ * mapIanaToTimezone("Unknown/Zone"); // "UTC"
380
+ * ```
381
+ */ export function mapIanaToTimezone(iana) {
382
+ for (const tz of TIMEZONES){
383
+ if (tz.iana.includes(iana)) {
384
+ return tz.value;
385
+ }
386
+ }
387
+ return "UTC";
388
+ }
package/dist/lib/view.js CHANGED
@@ -30,7 +30,7 @@ import { getHtmlExcerpt } from "./excerpt.js";
30
30
  * @returns Render-ready MediaView with pre-computed URLs
31
31
  */ export function toMediaView(media, ctx) {
32
32
  const publicUrl = getPublicUrlForProvider(media.provider, ctx.r2PublicUrl, ctx.s3PublicUrl);
33
- const url = getMediaUrl(media.id, media.storageKey, publicUrl);
33
+ const url = getMediaUrl(media.storageKey, publicUrl);
34
34
  const thumbnailUrl = getImageUrl(url, ctx.imageTransformUrl, {
35
35
  width: 400,
36
36
  quality: 80,
@@ -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,
@@ -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
+ });