@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
package/dist/lib/schemas.js
CHANGED
|
@@ -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.
|
|
33
|
+
const url = getMediaUrl(media.storageKey, publicUrl);
|
|
34
34
|
const thumbnailUrl = getImageUrl(url, ctx.imageTransformUrl, {
|
|
35
35
|
width: 400,
|
|
36
36
|
quality: 80,
|
package/dist/routes/api/posts.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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(
|
|
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.
|
|
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
|
+
});
|