@jant/core 0.2.17 → 0.2.18
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.d.ts +1 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +319 -115
- package/dist/i18n/context.d.ts +2 -2
- package/dist/i18n/context.js +1 -1
- package/dist/i18n/i18n.d.ts +1 -1
- package/dist/i18n/i18n.js +1 -1
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/locales/en.d.ts.map +1 -1
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.d.ts.map +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.d.ts.map +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/lib/config.d.ts +44 -10
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +69 -44
- package/dist/lib/constants.d.ts +2 -1
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +5 -2
- package/dist/lib/sse.d.ts +15 -0
- package/dist/lib/sse.d.ts.map +1 -1
- package/dist/lib/sse.js +13 -0
- package/dist/lib/theme.d.ts +44 -0
- package/dist/lib/theme.d.ts.map +1 -0
- package/dist/lib/theme.js +65 -0
- package/dist/routes/dash/appearance.d.ts +13 -0
- package/dist/routes/dash/appearance.d.ts.map +1 -0
- package/dist/routes/dash/appearance.js +164 -0
- package/dist/routes/dash/settings.d.ts.map +1 -1
- package/dist/routes/dash/settings.js +38 -37
- package/dist/services/settings.d.ts +1 -0
- package/dist/services/settings.d.ts.map +1 -1
- package/dist/services/settings.js +3 -0
- package/dist/theme/color-themes.d.ts +30 -0
- package/dist/theme/color-themes.d.ts.map +1 -0
- package/dist/theme/color-themes.js +268 -0
- package/dist/theme/layouts/BaseLayout.d.ts +5 -0
- package/dist/theme/layouts/BaseLayout.d.ts.map +1 -1
- package/dist/theme/layouts/BaseLayout.js +70 -3
- package/dist/theme/layouts/DashLayout.d.ts +2 -0
- package/dist/theme/layouts/DashLayout.d.ts.map +1 -1
- package/dist/theme/layouts/DashLayout.js +10 -1
- package/dist/theme/layouts/index.d.ts +1 -1
- package/dist/theme/layouts/index.d.ts.map +1 -1
- package/dist/types.d.ts +53 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +52 -0
- package/package.json +1 -1
- package/src/app.tsx +272 -55
- package/src/db/migrations/{0000_solid_moon_knight.sql → 0000_square_wallflower.sql} +3 -3
- package/src/db/migrations/meta/0000_snapshot.json +9 -9
- package/src/db/migrations/meta/_journal.json +2 -30
- package/src/i18n/context.tsx +2 -2
- package/src/i18n/i18n.ts +1 -1
- package/src/i18n/index.ts +1 -1
- package/src/i18n/locales/en.po +328 -252
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +315 -278
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +315 -278
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/lib/config.ts +73 -47
- package/src/lib/constants.ts +3 -0
- package/src/lib/sse.ts +38 -0
- package/src/lib/theme.ts +86 -0
- package/src/preset.css +9 -0
- package/src/routes/dash/appearance.tsx +180 -0
- package/src/routes/dash/settings.tsx +50 -52
- package/src/services/settings.ts +5 -0
- package/src/styles/components.css +93 -0
- package/src/theme/color-themes.ts +321 -0
- package/src/theme/layouts/BaseLayout.tsx +61 -1
- package/src/theme/layouts/DashLayout.tsx +13 -2
- package/src/theme/layouts/index.ts +5 -1
- package/src/types.ts +62 -1
- package/src/db/migrations/0001_add_search_fts.sql +0 -40
- package/src/db/migrations/0002_collection_path.sql +0 -2
- package/src/db/migrations/0003_collection_path_nullable.sql +0 -21
- package/src/db/migrations/0004_media_uuid.sql +0 -35
package/src/app.tsx
CHANGED
|
@@ -10,6 +10,8 @@ import { createAuth, type Auth } from "./auth.js";
|
|
|
10
10
|
import { i18nMiddleware } from "./i18n/index.js";
|
|
11
11
|
import { useLingui } from "@lingui/react/macro";
|
|
12
12
|
import type { Bindings, JantConfig } from "./types.js";
|
|
13
|
+
import { SETTINGS_KEYS } from "./lib/constants.js";
|
|
14
|
+
import { hashPassword } from "better-auth/crypto";
|
|
13
15
|
|
|
14
16
|
// Routes - Pages
|
|
15
17
|
import { homeRoutes } from "./routes/pages/home.js";
|
|
@@ -27,6 +29,7 @@ import { mediaRoutes as dashMediaRoutes } from "./routes/dash/media.js";
|
|
|
27
29
|
import { settingsRoutes as dashSettingsRoutes } from "./routes/dash/settings.js";
|
|
28
30
|
import { redirectsRoutes as dashRedirectsRoutes } from "./routes/dash/redirects.js";
|
|
29
31
|
import { collectionsRoutes as dashCollectionsRoutes } from "./routes/dash/collections.js";
|
|
32
|
+
import { appearanceRoutes as dashAppearanceRoutes } from "./routes/dash/appearance.js";
|
|
30
33
|
|
|
31
34
|
// Routes - API
|
|
32
35
|
import { postsApiRoutes } from "./routes/api/posts.js";
|
|
@@ -43,12 +46,14 @@ import { requireAuth } from "./middleware/auth.js";
|
|
|
43
46
|
// Layouts for auth pages
|
|
44
47
|
import { BaseLayout } from "./theme/layouts/index.js";
|
|
45
48
|
import { sse } from "./lib/sse.js";
|
|
49
|
+
import { getAvailableThemes, buildThemeStyle } from "./lib/theme.js";
|
|
46
50
|
|
|
47
51
|
// Extend Hono's context variables
|
|
48
52
|
export interface AppVariables {
|
|
49
53
|
services: Services;
|
|
50
54
|
auth: Auth;
|
|
51
55
|
config: JantConfig;
|
|
56
|
+
themeStyle: string;
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
export type App = Hono<{ Bindings: Bindings; Variables: AppVariables }>;
|
|
@@ -100,6 +105,18 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
100
105
|
await next();
|
|
101
106
|
});
|
|
102
107
|
|
|
108
|
+
// Theme middleware - resolve active color theme and build CSS
|
|
109
|
+
app.use("*", async (c, next) => {
|
|
110
|
+
const themeId = await c.var.services.settings.get(SETTINGS_KEYS.THEME);
|
|
111
|
+
const themes = getAvailableThemes(config);
|
|
112
|
+
const activeTheme = themeId
|
|
113
|
+
? themes.find((t) => t.id === themeId)
|
|
114
|
+
: undefined;
|
|
115
|
+
const themeStyle = buildThemeStyle(activeTheme, config.theme?.cssVariables);
|
|
116
|
+
c.set("themeStyle", themeStyle);
|
|
117
|
+
await next();
|
|
118
|
+
});
|
|
119
|
+
|
|
103
120
|
// i18n middleware
|
|
104
121
|
app.use("*", i18nMiddleware());
|
|
105
122
|
|
|
@@ -165,36 +182,17 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
165
182
|
</h2>
|
|
166
183
|
<p>
|
|
167
184
|
{t({
|
|
168
|
-
message: "
|
|
185
|
+
message: "Create your admin account.",
|
|
169
186
|
comment: "@context: Setup page description",
|
|
170
187
|
})}
|
|
171
188
|
</p>
|
|
172
189
|
</header>
|
|
173
190
|
<section>
|
|
174
|
-
<div id="setup-message"></div>
|
|
175
191
|
<form
|
|
176
|
-
data-signals="{
|
|
192
|
+
data-signals="{name: '', email: '', password: ''}"
|
|
177
193
|
data-on:submit__prevent="@post('/setup')"
|
|
178
194
|
class="flex flex-col gap-4"
|
|
179
195
|
>
|
|
180
|
-
<div class="field">
|
|
181
|
-
<label class="label">
|
|
182
|
-
{t({
|
|
183
|
-
message: "Site Name",
|
|
184
|
-
comment: "@context: Setup form field - site name",
|
|
185
|
-
})}
|
|
186
|
-
</label>
|
|
187
|
-
<input
|
|
188
|
-
type="text"
|
|
189
|
-
data-bind="siteName"
|
|
190
|
-
class="input"
|
|
191
|
-
required
|
|
192
|
-
placeholder={t({
|
|
193
|
-
message: "My Blog",
|
|
194
|
-
comment: "@context: Setup site name placeholder",
|
|
195
|
-
})}
|
|
196
|
-
/>
|
|
197
|
-
</div>
|
|
198
196
|
<div class="field">
|
|
199
197
|
<label class="label">
|
|
200
198
|
{t({
|
|
@@ -270,34 +268,27 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
270
268
|
if (isComplete) return c.redirect("/");
|
|
271
269
|
|
|
272
270
|
const body = await c.req.json<{
|
|
273
|
-
siteName: string;
|
|
274
271
|
name: string;
|
|
275
272
|
email: string;
|
|
276
273
|
password: string;
|
|
277
274
|
}>();
|
|
278
|
-
const {
|
|
275
|
+
const { name, email, password } = body;
|
|
279
276
|
|
|
280
|
-
if (!
|
|
277
|
+
if (!name || !email || !password) {
|
|
281
278
|
return sse(c, async (stream) => {
|
|
282
|
-
await stream.
|
|
283
|
-
'<div id="setup-message"><div class="alert-destructive mb-4"><h2>All fields are required</h2></div></div>',
|
|
284
|
-
);
|
|
279
|
+
await stream.toast("All fields are required", "error");
|
|
285
280
|
});
|
|
286
281
|
}
|
|
287
282
|
|
|
288
283
|
if (password.length < 8) {
|
|
289
284
|
return sse(c, async (stream) => {
|
|
290
|
-
await stream.
|
|
291
|
-
'<div id="setup-message"><div class="alert-destructive mb-4"><h2>Password must be at least 8 characters</h2></div></div>',
|
|
292
|
-
);
|
|
285
|
+
await stream.toast("Password must be at least 8 characters", "error");
|
|
293
286
|
});
|
|
294
287
|
}
|
|
295
288
|
|
|
296
289
|
if (!c.var.auth) {
|
|
297
290
|
return sse(c, async (stream) => {
|
|
298
|
-
await stream.
|
|
299
|
-
'<div id="setup-message"><div class="alert-destructive mb-4"><h2>AUTH_SECRET not configured</h2></div></div>',
|
|
300
|
-
);
|
|
291
|
+
await stream.toast("AUTH_SECRET not configured", "error");
|
|
301
292
|
});
|
|
302
293
|
}
|
|
303
294
|
|
|
@@ -308,28 +299,20 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
308
299
|
|
|
309
300
|
if (!signUpResponse || "error" in signUpResponse) {
|
|
310
301
|
return sse(c, async (stream) => {
|
|
311
|
-
await stream.
|
|
312
|
-
'<div id="setup-message"><div class="alert-destructive mb-4"><h2>Failed to create account</h2></div></div>',
|
|
313
|
-
);
|
|
302
|
+
await stream.toast("Failed to create account", "error");
|
|
314
303
|
});
|
|
315
304
|
}
|
|
316
305
|
|
|
317
|
-
await c.var.services.settings.setMany({
|
|
318
|
-
SITE_NAME: siteName,
|
|
319
|
-
SITE_LANGUAGE: "en",
|
|
320
|
-
});
|
|
321
306
|
await c.var.services.settings.completeOnboarding();
|
|
322
307
|
|
|
323
308
|
return sse(c, async (stream) => {
|
|
324
|
-
await stream.redirect("/signin");
|
|
309
|
+
await stream.redirect("/signin?setup");
|
|
325
310
|
});
|
|
326
311
|
} catch (err) {
|
|
327
312
|
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
328
313
|
console.error("Setup error:", err);
|
|
329
314
|
return sse(c, async (stream) => {
|
|
330
|
-
await stream.
|
|
331
|
-
'<div id="setup-message"><div class="alert-destructive mb-4"><h2>Failed to create account</h2></div></div>',
|
|
332
|
-
);
|
|
315
|
+
await stream.toast("Failed to create account", "error");
|
|
333
316
|
});
|
|
334
317
|
}
|
|
335
318
|
});
|
|
@@ -357,7 +340,6 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
357
340
|
</h2>
|
|
358
341
|
</header>
|
|
359
342
|
<section>
|
|
360
|
-
<div id="signin-message"></div>
|
|
361
343
|
{demoEmail && demoPassword && (
|
|
362
344
|
<p class="text-muted-foreground text-sm mb-4">
|
|
363
345
|
{t({
|
|
@@ -410,8 +392,17 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
410
392
|
|
|
411
393
|
// Signin page
|
|
412
394
|
app.get("/signin", async (c) => {
|
|
395
|
+
const isSetup = c.req.query("setup") !== undefined;
|
|
396
|
+
const isReset = c.req.query("reset") !== undefined;
|
|
397
|
+
let toast: { message: string } | undefined;
|
|
398
|
+
if (isSetup) {
|
|
399
|
+
toast = { message: "Account created successfully. Please sign in." };
|
|
400
|
+
} else if (isReset) {
|
|
401
|
+
toast = { message: "Password reset successfully. Please sign in." };
|
|
402
|
+
}
|
|
403
|
+
|
|
413
404
|
return c.html(
|
|
414
|
-
<BaseLayout title="Sign In - Jant" c={c}>
|
|
405
|
+
<BaseLayout title="Sign In - Jant" c={c} toast={toast}>
|
|
415
406
|
<SigninContent
|
|
416
407
|
demoEmail={c.env.DEMO_EMAIL}
|
|
417
408
|
demoPassword={c.env.DEMO_PASSWORD}
|
|
@@ -423,9 +414,7 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
423
414
|
app.post("/signin", async (c) => {
|
|
424
415
|
if (!c.var.auth) {
|
|
425
416
|
return sse(c, async (stream) => {
|
|
426
|
-
await stream.
|
|
427
|
-
'<div id="signin-message"><div class="alert-destructive mb-4"><h2>Auth not configured</h2></div></div>',
|
|
428
|
-
);
|
|
417
|
+
await stream.toast("Auth not configured", "error");
|
|
429
418
|
});
|
|
430
419
|
}
|
|
431
420
|
|
|
@@ -446,9 +435,7 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
446
435
|
|
|
447
436
|
if (!response.ok) {
|
|
448
437
|
return sse(c, async (stream) => {
|
|
449
|
-
await stream.
|
|
450
|
-
'<div id="signin-message"><div class="alert-destructive mb-4"><h2>Invalid email or password</h2></div></div>',
|
|
451
|
-
);
|
|
438
|
+
await stream.toast("Invalid email or password", "error");
|
|
452
439
|
});
|
|
453
440
|
}
|
|
454
441
|
|
|
@@ -470,9 +457,7 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
470
457
|
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
471
458
|
console.error("Signin error:", err);
|
|
472
459
|
return sse(c, async (stream) => {
|
|
473
|
-
await stream.
|
|
474
|
-
'<div id="signin-message"><div class="alert-destructive mb-4"><h2>Invalid email or password</h2></div></div>',
|
|
475
|
-
);
|
|
460
|
+
await stream.toast("Invalid email or password", "error");
|
|
476
461
|
});
|
|
477
462
|
}
|
|
478
463
|
});
|
|
@@ -488,6 +473,237 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
488
473
|
return c.redirect("/");
|
|
489
474
|
});
|
|
490
475
|
|
|
476
|
+
// Password reset via one-time token
|
|
477
|
+
const ResetContent: FC<{ token: string }> = ({ token }) => {
|
|
478
|
+
const { t } = useLingui();
|
|
479
|
+
const signals = JSON.stringify({
|
|
480
|
+
password: "",
|
|
481
|
+
confirmPassword: "",
|
|
482
|
+
token,
|
|
483
|
+
}).replace(/</g, "\\u003c");
|
|
484
|
+
|
|
485
|
+
return (
|
|
486
|
+
<div class="min-h-screen flex items-center justify-center">
|
|
487
|
+
<div class="card max-w-md w-full">
|
|
488
|
+
<header>
|
|
489
|
+
<h2>
|
|
490
|
+
{t({
|
|
491
|
+
message: "Reset Password",
|
|
492
|
+
comment: "@context: Password reset page heading",
|
|
493
|
+
})}
|
|
494
|
+
</h2>
|
|
495
|
+
<p>
|
|
496
|
+
{t({
|
|
497
|
+
message: "Enter your new password.",
|
|
498
|
+
comment: "@context: Password reset page description",
|
|
499
|
+
})}
|
|
500
|
+
</p>
|
|
501
|
+
</header>
|
|
502
|
+
<section>
|
|
503
|
+
<form
|
|
504
|
+
data-signals={signals}
|
|
505
|
+
data-on:submit__prevent="@post('/reset')"
|
|
506
|
+
class="flex flex-col gap-4"
|
|
507
|
+
>
|
|
508
|
+
<div class="field">
|
|
509
|
+
<label class="label">
|
|
510
|
+
{t({
|
|
511
|
+
message: "New Password",
|
|
512
|
+
comment: "@context: Password reset form field",
|
|
513
|
+
})}
|
|
514
|
+
</label>
|
|
515
|
+
<input
|
|
516
|
+
type="password"
|
|
517
|
+
data-bind="password"
|
|
518
|
+
class="input"
|
|
519
|
+
required
|
|
520
|
+
minLength={8}
|
|
521
|
+
autocomplete="new-password"
|
|
522
|
+
/>
|
|
523
|
+
</div>
|
|
524
|
+
<div class="field">
|
|
525
|
+
<label class="label">
|
|
526
|
+
{t({
|
|
527
|
+
message: "Confirm Password",
|
|
528
|
+
comment: "@context: Password reset form field",
|
|
529
|
+
})}
|
|
530
|
+
</label>
|
|
531
|
+
<input
|
|
532
|
+
type="password"
|
|
533
|
+
data-bind="confirmPassword"
|
|
534
|
+
class="input"
|
|
535
|
+
required
|
|
536
|
+
minLength={8}
|
|
537
|
+
autocomplete="new-password"
|
|
538
|
+
/>
|
|
539
|
+
</div>
|
|
540
|
+
<button type="submit" class="btn">
|
|
541
|
+
{t({
|
|
542
|
+
message: "Reset Password",
|
|
543
|
+
comment: "@context: Password reset form submit button",
|
|
544
|
+
})}
|
|
545
|
+
</button>
|
|
546
|
+
</form>
|
|
547
|
+
</section>
|
|
548
|
+
</div>
|
|
549
|
+
</div>
|
|
550
|
+
);
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const ResetErrorContent: FC = () => {
|
|
554
|
+
const { t } = useLingui();
|
|
555
|
+
|
|
556
|
+
return (
|
|
557
|
+
<div class="min-h-screen flex items-center justify-center">
|
|
558
|
+
<div class="card max-w-md w-full">
|
|
559
|
+
<header>
|
|
560
|
+
<h2>
|
|
561
|
+
{t({
|
|
562
|
+
message: "Invalid or Expired Link",
|
|
563
|
+
comment: "@context: Password reset error heading",
|
|
564
|
+
})}
|
|
565
|
+
</h2>
|
|
566
|
+
</header>
|
|
567
|
+
<section>
|
|
568
|
+
<p class="text-muted-foreground">
|
|
569
|
+
{t({
|
|
570
|
+
message:
|
|
571
|
+
"This password reset link is invalid or has expired. Please generate a new one.",
|
|
572
|
+
comment: "@context: Password reset error description",
|
|
573
|
+
})}
|
|
574
|
+
</p>
|
|
575
|
+
</section>
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
);
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
app.get("/reset", async (c) => {
|
|
582
|
+
const token = c.req.query("token");
|
|
583
|
+
if (!token) {
|
|
584
|
+
return c.html(
|
|
585
|
+
<BaseLayout title="Reset Password - Jant" c={c}>
|
|
586
|
+
<ResetErrorContent />
|
|
587
|
+
</BaseLayout>,
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const stored = await c.var.services.settings.get(
|
|
592
|
+
SETTINGS_KEYS.PASSWORD_RESET_TOKEN,
|
|
593
|
+
);
|
|
594
|
+
if (!stored) {
|
|
595
|
+
return c.html(
|
|
596
|
+
<BaseLayout title="Reset Password - Jant" c={c}>
|
|
597
|
+
<ResetErrorContent />
|
|
598
|
+
</BaseLayout>,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const separatorIndex = stored.lastIndexOf(":");
|
|
603
|
+
const storedToken = stored.substring(0, separatorIndex);
|
|
604
|
+
const expiry = parseInt(stored.substring(separatorIndex + 1), 10);
|
|
605
|
+
const now = Math.floor(Date.now() / 1000);
|
|
606
|
+
|
|
607
|
+
if (token !== storedToken || now > expiry) {
|
|
608
|
+
return c.html(
|
|
609
|
+
<BaseLayout title="Reset Password - Jant" c={c}>
|
|
610
|
+
<ResetErrorContent />
|
|
611
|
+
</BaseLayout>,
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return c.html(
|
|
616
|
+
<BaseLayout title="Reset Password - Jant" c={c}>
|
|
617
|
+
<ResetContent token={token} />
|
|
618
|
+
</BaseLayout>,
|
|
619
|
+
);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
app.post("/reset", async (c) => {
|
|
623
|
+
const body = await c.req.json<{
|
|
624
|
+
password: string;
|
|
625
|
+
confirmPassword: string;
|
|
626
|
+
token: string;
|
|
627
|
+
}>();
|
|
628
|
+
const { password, confirmPassword, token } = body;
|
|
629
|
+
|
|
630
|
+
// Validate token
|
|
631
|
+
const stored = await c.var.services.settings.get(
|
|
632
|
+
SETTINGS_KEYS.PASSWORD_RESET_TOKEN,
|
|
633
|
+
);
|
|
634
|
+
if (!stored) {
|
|
635
|
+
return sse(c, async (stream) => {
|
|
636
|
+
await stream.toast("Invalid or expired reset link.", "error");
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const separatorIndex = stored.lastIndexOf(":");
|
|
641
|
+
const storedToken = stored.substring(0, separatorIndex);
|
|
642
|
+
const expiry = parseInt(stored.substring(separatorIndex + 1), 10);
|
|
643
|
+
const now = Math.floor(Date.now() / 1000);
|
|
644
|
+
|
|
645
|
+
if (token !== storedToken || now > expiry) {
|
|
646
|
+
return sse(c, async (stream) => {
|
|
647
|
+
await stream.toast("Invalid or expired reset link.", "error");
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Validate passwords
|
|
652
|
+
if (!password || password.length < 8) {
|
|
653
|
+
return sse(c, async (stream) => {
|
|
654
|
+
await stream.toast("Password must be at least 8 characters.", "error");
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (password !== confirmPassword) {
|
|
659
|
+
return sse(c, async (stream) => {
|
|
660
|
+
await stream.toast("Passwords do not match.", "error");
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
try {
|
|
665
|
+
const hashedPassword = await hashPassword(password);
|
|
666
|
+
const db = c.env.DB.withSession() as unknown as D1Database;
|
|
667
|
+
|
|
668
|
+
// Get admin user
|
|
669
|
+
const userResult = await db
|
|
670
|
+
.prepare("SELECT id FROM user LIMIT 1")
|
|
671
|
+
.first<{ id: string }>();
|
|
672
|
+
if (!userResult) {
|
|
673
|
+
return sse(c, async (stream) => {
|
|
674
|
+
await stream.toast("No user account found.", "error");
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Update password
|
|
679
|
+
await db
|
|
680
|
+
.prepare(
|
|
681
|
+
"UPDATE account SET password = ? WHERE user_id = ? AND provider_id = 'credential'",
|
|
682
|
+
)
|
|
683
|
+
.bind(hashedPassword, userResult.id)
|
|
684
|
+
.run();
|
|
685
|
+
|
|
686
|
+
// Delete all sessions
|
|
687
|
+
await db
|
|
688
|
+
.prepare("DELETE FROM session WHERE user_id = ?")
|
|
689
|
+
.bind(userResult.id)
|
|
690
|
+
.run();
|
|
691
|
+
|
|
692
|
+
// Delete the reset token
|
|
693
|
+
await c.var.services.settings.remove(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
|
|
694
|
+
|
|
695
|
+
return sse(c, async (stream) => {
|
|
696
|
+
await stream.redirect("/signin?reset");
|
|
697
|
+
});
|
|
698
|
+
} catch (err) {
|
|
699
|
+
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
700
|
+
console.error("Password reset error:", err);
|
|
701
|
+
return sse(c, async (stream) => {
|
|
702
|
+
await stream.toast("Failed to reset password.", "error");
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
|
|
491
707
|
// Dashboard routes (protected)
|
|
492
708
|
app.use("/dash/*", requireAuth());
|
|
493
709
|
app.route("/dash", dashIndexRoutes);
|
|
@@ -497,6 +713,7 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
497
713
|
app.route("/dash/settings", dashSettingsRoutes);
|
|
498
714
|
app.route("/dash/redirects", dashRedirectsRoutes);
|
|
499
715
|
app.route("/dash/collections", dashCollectionsRoutes);
|
|
716
|
+
app.route("/dash/appearance", dashAppearanceRoutes);
|
|
500
717
|
|
|
501
718
|
// API routes
|
|
502
719
|
app.route("/api/upload", uploadApiRoutes);
|
|
@@ -17,16 +17,16 @@ CREATE TABLE `account` (
|
|
|
17
17
|
--> statement-breakpoint
|
|
18
18
|
CREATE TABLE `collections` (
|
|
19
19
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
20
|
-
`
|
|
20
|
+
`path` text,
|
|
21
21
|
`title` text NOT NULL,
|
|
22
22
|
`description` text,
|
|
23
23
|
`created_at` integer NOT NULL,
|
|
24
24
|
`updated_at` integer NOT NULL
|
|
25
25
|
);
|
|
26
26
|
--> statement-breakpoint
|
|
27
|
-
CREATE UNIQUE INDEX `
|
|
27
|
+
CREATE UNIQUE INDEX `collections_path_unique` ON `collections` (`path`);--> statement-breakpoint
|
|
28
28
|
CREATE TABLE `media` (
|
|
29
|
-
`id`
|
|
29
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
30
30
|
`post_id` integer,
|
|
31
31
|
`filename` text NOT NULL,
|
|
32
32
|
`original_name` text NOT NULL,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "6",
|
|
3
3
|
"dialect": "sqlite",
|
|
4
|
-
"id": "
|
|
4
|
+
"id": "3d04f8ef-088d-43f4-9fe9-55ffa2d7a73f",
|
|
5
5
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
6
6
|
"tables": {
|
|
7
7
|
"account": {
|
|
@@ -125,11 +125,11 @@
|
|
|
125
125
|
"notNull": true,
|
|
126
126
|
"autoincrement": true
|
|
127
127
|
},
|
|
128
|
-
"
|
|
129
|
-
"name": "
|
|
128
|
+
"path": {
|
|
129
|
+
"name": "path",
|
|
130
130
|
"type": "text",
|
|
131
131
|
"primaryKey": false,
|
|
132
|
-
"notNull":
|
|
132
|
+
"notNull": false,
|
|
133
133
|
"autoincrement": false
|
|
134
134
|
},
|
|
135
135
|
"title": {
|
|
@@ -162,9 +162,9 @@
|
|
|
162
162
|
}
|
|
163
163
|
},
|
|
164
164
|
"indexes": {
|
|
165
|
-
"
|
|
166
|
-
"name": "
|
|
167
|
-
"columns": ["
|
|
165
|
+
"collections_path_unique": {
|
|
166
|
+
"name": "collections_path_unique",
|
|
167
|
+
"columns": ["path"],
|
|
168
168
|
"isUnique": true
|
|
169
169
|
}
|
|
170
170
|
},
|
|
@@ -178,10 +178,10 @@
|
|
|
178
178
|
"columns": {
|
|
179
179
|
"id": {
|
|
180
180
|
"name": "id",
|
|
181
|
-
"type": "
|
|
181
|
+
"type": "text",
|
|
182
182
|
"primaryKey": true,
|
|
183
183
|
"notNull": true,
|
|
184
|
-
"autoincrement":
|
|
184
|
+
"autoincrement": false
|
|
185
185
|
},
|
|
186
186
|
"post_id": {
|
|
187
187
|
"name": "post_id",
|
|
@@ -5,36 +5,8 @@
|
|
|
5
5
|
{
|
|
6
6
|
"idx": 0,
|
|
7
7
|
"version": "6",
|
|
8
|
-
"when":
|
|
9
|
-
"tag": "
|
|
10
|
-
"breakpoints": true
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
"idx": 1,
|
|
14
|
-
"version": "6",
|
|
15
|
-
"when": 1769859000000,
|
|
16
|
-
"tag": "0001_add_search_fts",
|
|
17
|
-
"breakpoints": true
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
"idx": 2,
|
|
21
|
-
"version": "6",
|
|
22
|
-
"when": 1769860000000,
|
|
23
|
-
"tag": "0002_collection_path",
|
|
24
|
-
"breakpoints": true
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
"idx": 3,
|
|
28
|
-
"version": "6",
|
|
29
|
-
"when": 1769861000000,
|
|
30
|
-
"tag": "0003_collection_path_nullable",
|
|
31
|
-
"breakpoints": true
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
"idx": 4,
|
|
35
|
-
"version": "6",
|
|
36
|
-
"when": 1770024000000,
|
|
37
|
-
"tag": "0004_media_uuid",
|
|
8
|
+
"when": 1770564499811,
|
|
9
|
+
"tag": "0000_square_wallflower",
|
|
38
10
|
"breakpoints": true
|
|
39
11
|
}
|
|
40
12
|
]
|
package/src/i18n/context.tsx
CHANGED
|
@@ -28,7 +28,7 @@ let currentI18n: I18n | null = null;
|
|
|
28
28
|
*
|
|
29
29
|
* @example
|
|
30
30
|
* ```tsx
|
|
31
|
-
* import { I18nProvider } from "
|
|
31
|
+
* import { I18nProvider } from "../i18n/index.js";
|
|
32
32
|
*
|
|
33
33
|
* return c.html(
|
|
34
34
|
* <I18nProvider c={c}>
|
|
@@ -56,7 +56,7 @@ export const I18nProvider: FC<I18nProviderProps> = ({ c, children }) => {
|
|
|
56
56
|
* @example
|
|
57
57
|
* ```tsx
|
|
58
58
|
* import { t } from "@lingui/core/macro";
|
|
59
|
-
* import { useLingui } from "
|
|
59
|
+
* import { useLingui } from "../i18n/index.js";
|
|
60
60
|
*
|
|
61
61
|
* function MyComponent() {
|
|
62
62
|
* const { t: _ } = useLingui(); // Use _ to avoid conflict with macro
|
package/src/i18n/i18n.ts
CHANGED
|
@@ -48,7 +48,7 @@ export function createI18n(locale: Locale): I18n {
|
|
|
48
48
|
*
|
|
49
49
|
* @example
|
|
50
50
|
* import { msg } from "@lingui/core/macro";
|
|
51
|
-
* import { getI18n } from "
|
|
51
|
+
* import { getI18n } from "../i18n/index.js";
|
|
52
52
|
*
|
|
53
53
|
* const i18n = getI18n(c);
|
|
54
54
|
* const title = i18n._(msg({ message: "Dashboard", comment: "@context: Page title" }));
|
package/src/i18n/index.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Usage:
|
|
11
11
|
* ```tsx
|
|
12
|
-
* import { useLingui, Trans, I18nProvider } from "
|
|
12
|
+
* import { useLingui, Trans, I18nProvider } from "../i18n/index.js";
|
|
13
13
|
*
|
|
14
14
|
* // Wrap your app in I18nProvider (automatically done by BaseLayout when c is provided)
|
|
15
15
|
* c.html(
|