@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.
- package/dist/app.js +67 -562
- package/dist/client.js +1 -0
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/lib/avatar-upload.js +134 -0
- package/dist/lib/config.js +39 -0
- package/dist/lib/constants.js +10 -10
- package/dist/lib/favicon.js +102 -0
- package/dist/lib/image.js +13 -17
- package/dist/lib/media-helpers.js +2 -2
- package/dist/lib/navigation.js +23 -3
- package/dist/lib/render.js +10 -1
- package/dist/lib/schemas.js +31 -0
- package/dist/lib/timezones.js +388 -0
- package/dist/lib/view.js +1 -1
- package/dist/routes/api/posts.js +1 -1
- package/dist/routes/api/upload.js +3 -3
- package/dist/routes/auth/reset.js +221 -0
- package/dist/routes/auth/setup.js +194 -0
- package/dist/routes/auth/signin.js +176 -0
- package/dist/routes/dash/collections.js +23 -415
- package/dist/routes/dash/media.js +12 -392
- package/dist/routes/dash/pages.js +7 -330
- package/dist/routes/dash/redirects.js +18 -12
- package/dist/routes/dash/settings.js +198 -577
- package/dist/routes/feed/rss.js +2 -1
- package/dist/routes/feed/sitemap.js +4 -2
- package/dist/routes/pages/featured.js +5 -1
- package/dist/routes/pages/home.js +26 -1
- package/dist/routes/pages/latest.js +45 -0
- package/dist/services/post.js +30 -50
- package/dist/types/bindings.js +3 -0
- package/dist/types/config.js +147 -0
- package/dist/types/constants.js +27 -0
- package/dist/types/entities.js +3 -0
- package/dist/types/operations.js +3 -0
- package/dist/types/props.js +3 -0
- package/dist/types/views.js +5 -0
- package/dist/types.js +8 -111
- package/dist/ui/color-themes.js +33 -33
- package/dist/ui/compose/ComposeDialog.js +36 -21
- package/dist/ui/dash/PageForm.js +21 -15
- package/dist/ui/dash/PostForm.js +22 -16
- package/dist/ui/dash/collections/CollectionForm.js +152 -0
- package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
- package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
- package/dist/ui/dash/media/MediaListContent.js +166 -0
- package/dist/ui/dash/media/ViewMediaContent.js +212 -0
- package/dist/ui/dash/pages/LinkFormContent.js +130 -0
- package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
- package/dist/ui/dash/settings/AccountContent.js +209 -0
- package/dist/ui/dash/settings/AppearanceContent.js +259 -0
- package/dist/ui/dash/settings/GeneralContent.js +536 -0
- package/dist/ui/dash/settings/SettingsNav.js +41 -0
- package/dist/ui/font-themes.js +36 -0
- package/dist/ui/layouts/BaseLayout.js +24 -2
- package/dist/ui/layouts/SiteLayout.js +47 -19
- package/package.json +1 -1
- package/src/app.tsx +93 -553
- package/src/client.ts +1 -0
- package/src/i18n/locales/en.po +240 -175
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +240 -175
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +240 -175
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/lib/__tests__/config.test.ts +192 -0
- package/src/lib/__tests__/favicon.test.ts +151 -0
- package/src/lib/__tests__/image.test.ts +2 -6
- package/src/lib/__tests__/timezones.test.ts +61 -0
- package/src/lib/__tests__/view.test.ts +2 -2
- package/src/lib/avatar-upload.ts +165 -0
- package/src/lib/config.ts +47 -0
- package/src/lib/constants.ts +19 -11
- package/src/lib/favicon.ts +115 -0
- package/src/lib/image.ts +13 -21
- package/src/lib/media-helpers.ts +2 -2
- package/src/lib/navigation.ts +33 -2
- package/src/lib/render.tsx +15 -1
- package/src/lib/schemas.ts +39 -0
- package/src/lib/timezones.ts +325 -0
- package/src/lib/view.ts +1 -1
- package/src/routes/api/posts.ts +1 -1
- package/src/routes/api/upload.ts +2 -3
- package/src/routes/auth/reset.tsx +239 -0
- package/src/routes/auth/setup.tsx +189 -0
- package/src/routes/auth/signin.tsx +163 -0
- package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
- package/src/routes/dash/collections.tsx +17 -366
- package/src/routes/dash/media.tsx +12 -414
- package/src/routes/dash/pages.tsx +8 -348
- package/src/routes/dash/redirects.tsx +20 -14
- package/src/routes/dash/settings.tsx +243 -534
- package/src/routes/feed/__tests__/rss.test.ts +141 -0
- package/src/routes/feed/rss.ts +3 -1
- package/src/routes/feed/sitemap.ts +4 -2
- package/src/routes/pages/featured.tsx +7 -1
- package/src/routes/pages/home.tsx +25 -2
- package/src/routes/pages/latest.tsx +59 -0
- package/src/services/post.ts +34 -66
- package/src/styles/components.css +0 -65
- package/src/styles/tokens.css +1 -1
- package/src/styles/ui.css +24 -40
- package/src/types/bindings.ts +30 -0
- package/src/types/config.ts +183 -0
- package/src/types/constants.ts +26 -0
- package/src/types/entities.ts +109 -0
- package/src/types/operations.ts +88 -0
- package/src/types/props.ts +115 -0
- package/src/types/views.ts +172 -0
- package/src/types.ts +8 -644
- package/src/ui/__tests__/font-themes.test.ts +34 -0
- package/src/ui/color-themes.ts +34 -34
- package/src/ui/compose/ComposeDialog.tsx +40 -21
- package/src/ui/dash/PageForm.tsx +25 -19
- package/src/ui/dash/PostForm.tsx +26 -20
- package/src/ui/dash/collections/CollectionForm.tsx +153 -0
- package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
- package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
- package/src/ui/dash/media/MediaListContent.tsx +201 -0
- package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
- package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
- package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
- package/src/ui/dash/settings/AccountContent.tsx +176 -0
- package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
- package/src/ui/dash/settings/GeneralContent.tsx +533 -0
- package/src/ui/dash/settings/SettingsNav.tsx +56 -0
- package/src/ui/font-themes.ts +54 -0
- package/src/ui/layouts/BaseLayout.tsx +17 -0
- package/src/ui/layouts/SiteLayout.tsx +45 -31
|
@@ -0,0 +1,325 @@
|
|
|
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
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface TimezoneEntry {
|
|
9
|
+
/** Display value stored in settings */
|
|
10
|
+
value: string;
|
|
11
|
+
/** Human-readable label for the select UI */
|
|
12
|
+
label: string;
|
|
13
|
+
/** UTC offset string, e.g. "+08:00" */
|
|
14
|
+
offset: string;
|
|
15
|
+
/** IANA timezone names that map to this entry */
|
|
16
|
+
iana: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const TIMEZONES: TimezoneEntry[] = [
|
|
20
|
+
{
|
|
21
|
+
value: "International Date Line West",
|
|
22
|
+
label: "(UTC-12:00) International Date Line West",
|
|
23
|
+
offset: "-12:00",
|
|
24
|
+
iana: ["Etc/GMT+12"],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
value: "American Samoa",
|
|
28
|
+
label: "(UTC-11:00) American Samoa",
|
|
29
|
+
offset: "-11:00",
|
|
30
|
+
iana: ["Pacific/Pago_Pago", "Pacific/Midway", "Etc/GMT+11"],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
value: "Hawaii",
|
|
34
|
+
label: "(UTC-10:00) Hawaii",
|
|
35
|
+
offset: "-10:00",
|
|
36
|
+
iana: ["Pacific/Honolulu", "Etc/GMT+10"],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
value: "Alaska",
|
|
40
|
+
label: "(UTC-09:00) Alaska",
|
|
41
|
+
offset: "-09:00",
|
|
42
|
+
iana: ["America/Anchorage", "America/Juneau", "America/Nome"],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
value: "Pacific Time (US & Canada)",
|
|
46
|
+
label: "(UTC-08:00) Pacific Time (US & Canada)",
|
|
47
|
+
offset: "-08:00",
|
|
48
|
+
iana: ["America/Los_Angeles", "America/Vancouver", "America/Tijuana"],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
value: "Mountain Time (US & Canada)",
|
|
52
|
+
label: "(UTC-07:00) Mountain Time (US & Canada)",
|
|
53
|
+
offset: "-07:00",
|
|
54
|
+
iana: [
|
|
55
|
+
"America/Denver",
|
|
56
|
+
"America/Edmonton",
|
|
57
|
+
"America/Phoenix",
|
|
58
|
+
"America/Boise",
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
value: "Central Time (US & Canada)",
|
|
63
|
+
label: "(UTC-06:00) Central Time (US & Canada)",
|
|
64
|
+
offset: "-06:00",
|
|
65
|
+
iana: [
|
|
66
|
+
"America/Chicago",
|
|
67
|
+
"America/Winnipeg",
|
|
68
|
+
"America/Mexico_City",
|
|
69
|
+
"America/Guatemala",
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
value: "Eastern Time (US & Canada)",
|
|
74
|
+
label: "(UTC-05:00) Eastern Time (US & Canada)",
|
|
75
|
+
offset: "-05:00",
|
|
76
|
+
iana: [
|
|
77
|
+
"America/New_York",
|
|
78
|
+
"America/Toronto",
|
|
79
|
+
"America/Detroit",
|
|
80
|
+
"America/Indiana/Indianapolis",
|
|
81
|
+
"America/Bogota",
|
|
82
|
+
"America/Lima",
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
value: "Atlantic Time (Canada)",
|
|
87
|
+
label: "(UTC-04:00) Atlantic Time (Canada)",
|
|
88
|
+
offset: "-04:00",
|
|
89
|
+
iana: [
|
|
90
|
+
"America/Halifax",
|
|
91
|
+
"America/Caracas",
|
|
92
|
+
"America/Santiago",
|
|
93
|
+
"America/La_Paz",
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
value: "Newfoundland",
|
|
98
|
+
label: "(UTC-03:30) Newfoundland",
|
|
99
|
+
offset: "-03:30",
|
|
100
|
+
iana: ["America/St_Johns"],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
value: "Buenos Aires",
|
|
104
|
+
label: "(UTC-03:00) Buenos Aires",
|
|
105
|
+
offset: "-03:00",
|
|
106
|
+
iana: [
|
|
107
|
+
"America/Argentina/Buenos_Aires",
|
|
108
|
+
"America/Sao_Paulo",
|
|
109
|
+
"America/Montevideo",
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
value: "Mid-Atlantic",
|
|
114
|
+
label: "(UTC-02:00) Mid-Atlantic",
|
|
115
|
+
offset: "-02:00",
|
|
116
|
+
iana: ["Atlantic/South_Georgia", "Etc/GMT+2"],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
value: "Azores",
|
|
120
|
+
label: "(UTC-01:00) Azores",
|
|
121
|
+
offset: "-01:00",
|
|
122
|
+
iana: ["Atlantic/Azores", "Atlantic/Cape_Verde"],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
value: "UTC",
|
|
126
|
+
label: "(UTC+00:00) UTC",
|
|
127
|
+
offset: "+00:00",
|
|
128
|
+
iana: ["Etc/UTC", "UTC", "Etc/GMT", "Africa/Accra"],
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
value: "London",
|
|
132
|
+
label: "(UTC+00:00) London",
|
|
133
|
+
offset: "+00:00",
|
|
134
|
+
iana: ["Europe/London", "Europe/Dublin", "Europe/Lisbon"],
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
value: "Central European Time",
|
|
138
|
+
label: "(UTC+01:00) Central European Time",
|
|
139
|
+
offset: "+01:00",
|
|
140
|
+
iana: [
|
|
141
|
+
"Europe/Paris",
|
|
142
|
+
"Europe/Berlin",
|
|
143
|
+
"Europe/Amsterdam",
|
|
144
|
+
"Europe/Rome",
|
|
145
|
+
"Europe/Madrid",
|
|
146
|
+
"Europe/Brussels",
|
|
147
|
+
"Europe/Vienna",
|
|
148
|
+
"Europe/Warsaw",
|
|
149
|
+
"Europe/Prague",
|
|
150
|
+
"Europe/Stockholm",
|
|
151
|
+
"Europe/Oslo",
|
|
152
|
+
"Europe/Copenhagen",
|
|
153
|
+
"Europe/Zurich",
|
|
154
|
+
"Africa/Lagos",
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
value: "Eastern European Time",
|
|
159
|
+
label: "(UTC+02:00) Eastern European Time",
|
|
160
|
+
offset: "+02:00",
|
|
161
|
+
iana: [
|
|
162
|
+
"Europe/Helsinki",
|
|
163
|
+
"Europe/Athens",
|
|
164
|
+
"Europe/Bucharest",
|
|
165
|
+
"Europe/Istanbul",
|
|
166
|
+
"Africa/Cairo",
|
|
167
|
+
"Africa/Johannesburg",
|
|
168
|
+
"Asia/Jerusalem",
|
|
169
|
+
"Asia/Beirut",
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
value: "Moscow",
|
|
174
|
+
label: "(UTC+03:00) Moscow",
|
|
175
|
+
offset: "+03:00",
|
|
176
|
+
iana: [
|
|
177
|
+
"Europe/Moscow",
|
|
178
|
+
"Europe/Minsk",
|
|
179
|
+
"Asia/Baghdad",
|
|
180
|
+
"Asia/Riyadh",
|
|
181
|
+
"Africa/Nairobi",
|
|
182
|
+
"Asia/Kuwait",
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
value: "Tehran",
|
|
187
|
+
label: "(UTC+03:30) Tehran",
|
|
188
|
+
offset: "+03:30",
|
|
189
|
+
iana: ["Asia/Tehran"],
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
value: "Dubai",
|
|
193
|
+
label: "(UTC+04:00) Dubai",
|
|
194
|
+
offset: "+04:00",
|
|
195
|
+
iana: ["Asia/Dubai", "Asia/Muscat", "Asia/Baku", "Asia/Tbilisi"],
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
value: "Kabul",
|
|
199
|
+
label: "(UTC+04:30) Kabul",
|
|
200
|
+
offset: "+04:30",
|
|
201
|
+
iana: ["Asia/Kabul"],
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
value: "Karachi",
|
|
205
|
+
label: "(UTC+05:00) Karachi",
|
|
206
|
+
offset: "+05:00",
|
|
207
|
+
iana: ["Asia/Karachi", "Asia/Tashkent", "Asia/Yekaterinburg"],
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
value: "Mumbai",
|
|
211
|
+
label: "(UTC+05:30) Mumbai",
|
|
212
|
+
offset: "+05:30",
|
|
213
|
+
iana: ["Asia/Kolkata", "Asia/Calcutta", "Asia/Colombo"],
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
value: "Kathmandu",
|
|
217
|
+
label: "(UTC+05:45) Kathmandu",
|
|
218
|
+
offset: "+05:45",
|
|
219
|
+
iana: ["Asia/Kathmandu"],
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
value: "Dhaka",
|
|
223
|
+
label: "(UTC+06:00) Dhaka",
|
|
224
|
+
offset: "+06:00",
|
|
225
|
+
iana: ["Asia/Dhaka", "Asia/Almaty", "Asia/Omsk"],
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
value: "Yangon",
|
|
229
|
+
label: "(UTC+06:30) Yangon",
|
|
230
|
+
offset: "+06:30",
|
|
231
|
+
iana: ["Asia/Yangon", "Asia/Rangoon"],
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
value: "Bangkok",
|
|
235
|
+
label: "(UTC+07:00) Bangkok",
|
|
236
|
+
offset: "+07:00",
|
|
237
|
+
iana: [
|
|
238
|
+
"Asia/Bangkok",
|
|
239
|
+
"Asia/Jakarta",
|
|
240
|
+
"Asia/Ho_Chi_Minh",
|
|
241
|
+
"Asia/Krasnoyarsk",
|
|
242
|
+
],
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
value: "Beijing",
|
|
246
|
+
label: "(UTC+08:00) Beijing",
|
|
247
|
+
offset: "+08:00",
|
|
248
|
+
iana: [
|
|
249
|
+
"Asia/Shanghai",
|
|
250
|
+
"Asia/Hong_Kong",
|
|
251
|
+
"Asia/Taipei",
|
|
252
|
+
"Asia/Singapore",
|
|
253
|
+
"Asia/Kuala_Lumpur",
|
|
254
|
+
"Asia/Makassar",
|
|
255
|
+
"Asia/Irkutsk",
|
|
256
|
+
"Australia/Perth",
|
|
257
|
+
],
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
value: "Tokyo",
|
|
261
|
+
label: "(UTC+09:00) Tokyo",
|
|
262
|
+
offset: "+09:00",
|
|
263
|
+
iana: ["Asia/Tokyo", "Asia/Seoul", "Asia/Yakutsk"],
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
value: "Adelaide",
|
|
267
|
+
label: "(UTC+09:30) Adelaide",
|
|
268
|
+
offset: "+09:30",
|
|
269
|
+
iana: ["Australia/Adelaide", "Australia/Darwin"],
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
value: "Sydney",
|
|
273
|
+
label: "(UTC+10:00) Sydney",
|
|
274
|
+
offset: "+10:00",
|
|
275
|
+
iana: [
|
|
276
|
+
"Australia/Sydney",
|
|
277
|
+
"Australia/Melbourne",
|
|
278
|
+
"Australia/Brisbane",
|
|
279
|
+
"Australia/Hobart",
|
|
280
|
+
"Pacific/Guam",
|
|
281
|
+
"Asia/Vladivostok",
|
|
282
|
+
],
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
value: "Noumea",
|
|
286
|
+
label: "(UTC+11:00) Noumea",
|
|
287
|
+
offset: "+11:00",
|
|
288
|
+
iana: ["Pacific/Noumea", "Asia/Magadan", "Pacific/Guadalcanal"],
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
value: "Auckland",
|
|
292
|
+
label: "(UTC+12:00) Auckland",
|
|
293
|
+
offset: "+12:00",
|
|
294
|
+
iana: ["Pacific/Auckland", "Pacific/Fiji", "Asia/Kamchatka"],
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
value: "Nuku'alofa",
|
|
298
|
+
label: "(UTC+13:00) Nuku'alofa",
|
|
299
|
+
offset: "+13:00",
|
|
300
|
+
iana: ["Pacific/Tongatapu", "Pacific/Apia"],
|
|
301
|
+
},
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Maps an IANA timezone name (e.g. "Asia/Shanghai") to our timezone list value
|
|
306
|
+
* (e.g. "Beijing"). Returns "UTC" if no match found.
|
|
307
|
+
*
|
|
308
|
+
* @param iana - IANA timezone identifier from Intl.DateTimeFormat
|
|
309
|
+
* @returns The matching timezone value from TIMEZONES
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* ```ts
|
|
313
|
+
* mapIanaToTimezone("Asia/Shanghai"); // "Beijing"
|
|
314
|
+
* mapIanaToTimezone("America/New_York"); // "Eastern Time (US & Canada)"
|
|
315
|
+
* mapIanaToTimezone("Unknown/Zone"); // "UTC"
|
|
316
|
+
* ```
|
|
317
|
+
*/
|
|
318
|
+
export function mapIanaToTimezone(iana: string): string {
|
|
319
|
+
for (const tz of TIMEZONES) {
|
|
320
|
+
if (tz.iana.includes(iana)) {
|
|
321
|
+
return tz.value;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return "UTC";
|
|
325
|
+
}
|
package/src/lib/view.ts
CHANGED
|
@@ -77,7 +77,7 @@ export function toMediaView(media: Media, ctx: MediaContext): MediaView {
|
|
|
77
77
|
ctx.r2PublicUrl,
|
|
78
78
|
ctx.s3PublicUrl,
|
|
79
79
|
);
|
|
80
|
-
const url = getMediaUrl(media.
|
|
80
|
+
const url = getMediaUrl(media.storageKey, publicUrl);
|
|
81
81
|
const thumbnailUrl = getImageUrl(url, ctx.imageTransformUrl, {
|
|
82
82
|
width: 400,
|
|
83
83
|
quality: 80,
|
package/src/routes/api/posts.ts
CHANGED
|
@@ -36,7 +36,7 @@ function toMediaAttachment(
|
|
|
36
36
|
r2PublicUrl,
|
|
37
37
|
s3PublicUrl,
|
|
38
38
|
);
|
|
39
|
-
const url = getMediaUrl(m.
|
|
39
|
+
const url = getMediaUrl(m.storageKey, publicUrl);
|
|
40
40
|
const previewUrl = getImageUrl(url, imageTransformUrl, {
|
|
41
41
|
width: 400,
|
|
42
42
|
quality: 80,
|
package/src/routes/api/upload.ts
CHANGED
|
@@ -40,7 +40,7 @@ function renderMediaCard(
|
|
|
40
40
|
publicUrl?: string,
|
|
41
41
|
imageTransformUrl?: string,
|
|
42
42
|
): string {
|
|
43
|
-
const fullUrl = getMediaUrl(media.
|
|
43
|
+
const fullUrl = getMediaUrl(media.storageKey, publicUrl);
|
|
44
44
|
const thumbnailUrl = getImageUrl(fullUrl, imageTransformUrl, {
|
|
45
45
|
width: 300,
|
|
46
46
|
quality: 80,
|
|
@@ -229,7 +229,7 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
229
229
|
c.env.R2_PUBLIC_URL,
|
|
230
230
|
c.env.S3_PUBLIC_URL,
|
|
231
231
|
);
|
|
232
|
-
const publicUrl = getMediaUrl(
|
|
232
|
+
const publicUrl = getMediaUrl(storageKey, mediaPublicUrl);
|
|
233
233
|
return c.json({
|
|
234
234
|
id: media.id,
|
|
235
235
|
filename: media.filename,
|
|
@@ -263,7 +263,6 @@ uploadApiRoutes.get("/", async (c) => {
|
|
|
263
263
|
id: m.id,
|
|
264
264
|
filename: m.filename,
|
|
265
265
|
url: getMediaUrl(
|
|
266
|
-
m.id,
|
|
267
266
|
m.storageKey,
|
|
268
267
|
getPublicUrlForProvider(m.provider, r2PublicUrl, s3PublicUrl),
|
|
269
268
|
),
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password Reset Routes
|
|
3
|
+
*
|
|
4
|
+
* One-time token-based password reset flow.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Hono } from "hono";
|
|
8
|
+
import type { FC } from "hono/jsx";
|
|
9
|
+
import { useLingui } from "@lingui/react/macro";
|
|
10
|
+
import { hashPassword } from "better-auth/crypto";
|
|
11
|
+
import type { Bindings } from "../../types.js";
|
|
12
|
+
import type { AppVariables } from "../../app.js";
|
|
13
|
+
import { BaseLayout } from "../../ui/layouts/BaseLayout.js";
|
|
14
|
+
import { dsRedirect, dsToast } from "../../lib/sse.js";
|
|
15
|
+
import { SETTINGS_KEYS } from "../../lib/constants.js";
|
|
16
|
+
import { ResetPasswordSchema } from "../../lib/schemas.js";
|
|
17
|
+
|
|
18
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
19
|
+
|
|
20
|
+
const ResetContent: FC<{ token: string }> = ({ token }) => {
|
|
21
|
+
const { t } = useLingui();
|
|
22
|
+
const signals = JSON.stringify({
|
|
23
|
+
password: "",
|
|
24
|
+
confirmPassword: "",
|
|
25
|
+
token,
|
|
26
|
+
}).replace(/</g, "\\u003c");
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div class="min-h-screen flex items-center justify-center">
|
|
30
|
+
<div class="card max-w-md w-full">
|
|
31
|
+
<header>
|
|
32
|
+
<h2>
|
|
33
|
+
{t({
|
|
34
|
+
message: "Reset Password",
|
|
35
|
+
comment: "@context: Password reset page heading",
|
|
36
|
+
})}
|
|
37
|
+
</h2>
|
|
38
|
+
<p>
|
|
39
|
+
{t({
|
|
40
|
+
message: "Enter your new password.",
|
|
41
|
+
comment: "@context: Password reset page description",
|
|
42
|
+
})}
|
|
43
|
+
</p>
|
|
44
|
+
</header>
|
|
45
|
+
<section>
|
|
46
|
+
<form
|
|
47
|
+
data-signals={signals}
|
|
48
|
+
data-on:submit__prevent="@post('/reset')"
|
|
49
|
+
data-indicator="_loading"
|
|
50
|
+
class="flex flex-col gap-4"
|
|
51
|
+
>
|
|
52
|
+
<div class="field">
|
|
53
|
+
<label class="label">
|
|
54
|
+
{t({
|
|
55
|
+
message: "New Password",
|
|
56
|
+
comment: "@context: Password reset form field",
|
|
57
|
+
})}
|
|
58
|
+
</label>
|
|
59
|
+
<input
|
|
60
|
+
type="password"
|
|
61
|
+
data-bind="password"
|
|
62
|
+
class="input"
|
|
63
|
+
required
|
|
64
|
+
minLength={8}
|
|
65
|
+
autocomplete="new-password"
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="field">
|
|
69
|
+
<label class="label">
|
|
70
|
+
{t({
|
|
71
|
+
message: "Confirm Password",
|
|
72
|
+
comment: "@context: Password reset form field",
|
|
73
|
+
})}
|
|
74
|
+
</label>
|
|
75
|
+
<input
|
|
76
|
+
type="password"
|
|
77
|
+
data-bind="confirmPassword"
|
|
78
|
+
class="input"
|
|
79
|
+
required
|
|
80
|
+
minLength={8}
|
|
81
|
+
autocomplete="new-password"
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
<button type="submit" class="btn" data-attr:disabled="$_loading">
|
|
85
|
+
<svg
|
|
86
|
+
data-show="$_loading"
|
|
87
|
+
style="display:none"
|
|
88
|
+
class="animate-spin size-4"
|
|
89
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
90
|
+
viewBox="0 0 24 24"
|
|
91
|
+
fill="none"
|
|
92
|
+
stroke="currentColor"
|
|
93
|
+
stroke-width="2"
|
|
94
|
+
stroke-linecap="round"
|
|
95
|
+
stroke-linejoin="round"
|
|
96
|
+
role="status"
|
|
97
|
+
>
|
|
98
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
99
|
+
</svg>
|
|
100
|
+
{t({
|
|
101
|
+
message: "Reset Password",
|
|
102
|
+
comment: "@context: Password reset form submit button",
|
|
103
|
+
})}
|
|
104
|
+
</button>
|
|
105
|
+
</form>
|
|
106
|
+
</section>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const ResetErrorContent: FC = () => {
|
|
113
|
+
const { t } = useLingui();
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div class="min-h-screen flex items-center justify-center">
|
|
117
|
+
<div class="card max-w-md w-full">
|
|
118
|
+
<header>
|
|
119
|
+
<h2>
|
|
120
|
+
{t({
|
|
121
|
+
message: "Invalid or Expired Link",
|
|
122
|
+
comment: "@context: Password reset error heading",
|
|
123
|
+
})}
|
|
124
|
+
</h2>
|
|
125
|
+
</header>
|
|
126
|
+
<section>
|
|
127
|
+
<p class="text-muted-foreground">
|
|
128
|
+
{t({
|
|
129
|
+
message:
|
|
130
|
+
"This password reset link is invalid or has expired. Please generate a new one.",
|
|
131
|
+
comment: "@context: Password reset error description",
|
|
132
|
+
})}
|
|
133
|
+
</p>
|
|
134
|
+
</section>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Validate a password reset token against the stored value.
|
|
142
|
+
* Returns true if the token is valid and not expired.
|
|
143
|
+
*/
|
|
144
|
+
async function validateResetToken(
|
|
145
|
+
settings: { get(key: string): Promise<string | null> },
|
|
146
|
+
token: string,
|
|
147
|
+
): Promise<boolean> {
|
|
148
|
+
const stored = await settings.get(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
|
|
149
|
+
if (!stored) return false;
|
|
150
|
+
|
|
151
|
+
const separatorIndex = stored.lastIndexOf(":");
|
|
152
|
+
const storedToken = stored.substring(0, separatorIndex);
|
|
153
|
+
const expiry = parseInt(stored.substring(separatorIndex + 1), 10);
|
|
154
|
+
const now = Math.floor(Date.now() / 1000);
|
|
155
|
+
|
|
156
|
+
return token === storedToken && now <= expiry;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const resetRoutes = new Hono<Env>();
|
|
160
|
+
|
|
161
|
+
resetRoutes.get("/reset", async (c) => {
|
|
162
|
+
const token = c.req.query("token");
|
|
163
|
+
if (!token) {
|
|
164
|
+
return c.html(
|
|
165
|
+
<BaseLayout title="Reset Password - Jant" c={c}>
|
|
166
|
+
<ResetErrorContent />
|
|
167
|
+
</BaseLayout>,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const isValid = await validateResetToken(c.var.services.settings, token);
|
|
172
|
+
if (!isValid) {
|
|
173
|
+
return c.html(
|
|
174
|
+
<BaseLayout title="Reset Password - Jant" c={c}>
|
|
175
|
+
<ResetErrorContent />
|
|
176
|
+
</BaseLayout>,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return c.html(
|
|
181
|
+
<BaseLayout title="Reset Password - Jant" c={c}>
|
|
182
|
+
<ResetContent token={token} />
|
|
183
|
+
</BaseLayout>,
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
resetRoutes.post("/reset", async (c) => {
|
|
188
|
+
const body = await c.req.json();
|
|
189
|
+
const parsed = ResetPasswordSchema.safeParse(body);
|
|
190
|
+
|
|
191
|
+
if (!parsed.success) {
|
|
192
|
+
const msg = parsed.error.errors[0]?.message ?? "Invalid input";
|
|
193
|
+
return dsToast(msg, "error");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const { password, token } = parsed.data;
|
|
197
|
+
|
|
198
|
+
// Validate token
|
|
199
|
+
const isValid = await validateResetToken(c.var.services.settings, token);
|
|
200
|
+
if (!isValid) {
|
|
201
|
+
return dsToast("Invalid or expired reset link.", "error");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const hashedPassword = await hashPassword(password);
|
|
206
|
+
const db = c.env.DB.withSession() as unknown as D1Database;
|
|
207
|
+
|
|
208
|
+
// Get admin user
|
|
209
|
+
const userResult = await db
|
|
210
|
+
.prepare("SELECT id FROM user LIMIT 1")
|
|
211
|
+
.first<{ id: string }>();
|
|
212
|
+
if (!userResult) {
|
|
213
|
+
return dsToast("No user account found.", "error");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Update password
|
|
217
|
+
await db
|
|
218
|
+
.prepare(
|
|
219
|
+
"UPDATE account SET password = ? WHERE user_id = ? AND provider_id = 'credential'",
|
|
220
|
+
)
|
|
221
|
+
.bind(hashedPassword, userResult.id)
|
|
222
|
+
.run();
|
|
223
|
+
|
|
224
|
+
// Delete all sessions
|
|
225
|
+
await db
|
|
226
|
+
.prepare("DELETE FROM session WHERE user_id = ?")
|
|
227
|
+
.bind(userResult.id)
|
|
228
|
+
.run();
|
|
229
|
+
|
|
230
|
+
// Delete the reset token
|
|
231
|
+
await c.var.services.settings.remove(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
|
|
232
|
+
|
|
233
|
+
return dsRedirect("/signin?reset");
|
|
234
|
+
} catch (err) {
|
|
235
|
+
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
236
|
+
console.error("Password reset error:", err);
|
|
237
|
+
return dsToast("Failed to reset password.", "error");
|
|
238
|
+
}
|
|
239
|
+
});
|