@jant/core 0.2.17 → 0.2.19
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 +307 -137
- package/dist/client.js +1 -0
- 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/image-processor.js +0 -4
- package/dist/lib/media-upload.js +104 -0
- package/dist/lib/sse.d.ts +82 -13
- package/dist/lib/sse.d.ts.map +1 -1
- package/dist/lib/sse.js +115 -17
- 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/api/upload.js +16 -18
- 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 +160 -0
- package/dist/routes/dash/collections.js +5 -13
- package/dist/routes/dash/media.js +17 -167
- package/dist/routes/dash/pages.js +4 -10
- package/dist/routes/dash/posts.js +4 -10
- package/dist/routes/dash/redirects.js +3 -7
- package/dist/routes/dash/settings.d.ts.map +1 -1
- package/dist/routes/dash/settings.js +52 -42
- 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 +11 -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 +260 -81
- package/src/client.ts +1 -0
- 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/image-processor.ts +0 -7
- package/src/lib/media-upload.ts +148 -0
- package/src/lib/sse.ts +156 -16
- package/src/lib/theme.ts +86 -0
- package/src/preset.css +9 -0
- package/src/routes/api/upload.ts +12 -18
- package/src/routes/dash/appearance.tsx +176 -0
- package/src/routes/dash/collections.tsx +5 -13
- package/src/routes/dash/media.tsx +16 -165
- package/src/routes/dash/pages.tsx +4 -10
- package/src/routes/dash/posts.tsx +4 -10
- package/src/routes/dash/redirects.tsx +3 -7
- package/src/routes/dash/settings.tsx +71 -55
- 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 +14 -3
- 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";
|
|
@@ -42,13 +45,15 @@ import { requireAuth } from "./middleware/auth.js";
|
|
|
42
45
|
|
|
43
46
|
// Layouts for auth pages
|
|
44
47
|
import { BaseLayout } from "./theme/layouts/index.js";
|
|
45
|
-
import {
|
|
48
|
+
import { dsRedirect, dsToast } 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,35 +268,22 @@ 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 (!
|
|
281
|
-
return
|
|
282
|
-
await stream.patchElements(
|
|
283
|
-
'<div id="setup-message"><div class="alert-destructive mb-4"><h2>All fields are required</h2></div></div>',
|
|
284
|
-
);
|
|
285
|
-
});
|
|
277
|
+
if (!name || !email || !password) {
|
|
278
|
+
return dsToast("All fields are required", "error");
|
|
286
279
|
}
|
|
287
280
|
|
|
288
281
|
if (password.length < 8) {
|
|
289
|
-
return
|
|
290
|
-
await stream.patchElements(
|
|
291
|
-
'<div id="setup-message"><div class="alert-destructive mb-4"><h2>Password must be at least 8 characters</h2></div></div>',
|
|
292
|
-
);
|
|
293
|
-
});
|
|
282
|
+
return dsToast("Password must be at least 8 characters", "error");
|
|
294
283
|
}
|
|
295
284
|
|
|
296
285
|
if (!c.var.auth) {
|
|
297
|
-
return
|
|
298
|
-
await stream.patchElements(
|
|
299
|
-
'<div id="setup-message"><div class="alert-destructive mb-4"><h2>AUTH_SECRET not configured</h2></div></div>',
|
|
300
|
-
);
|
|
301
|
-
});
|
|
286
|
+
return dsToast("AUTH_SECRET not configured", "error");
|
|
302
287
|
}
|
|
303
288
|
|
|
304
289
|
try {
|
|
@@ -307,30 +292,16 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
307
292
|
});
|
|
308
293
|
|
|
309
294
|
if (!signUpResponse || "error" in signUpResponse) {
|
|
310
|
-
return
|
|
311
|
-
await stream.patchElements(
|
|
312
|
-
'<div id="setup-message"><div class="alert-destructive mb-4"><h2>Failed to create account</h2></div></div>',
|
|
313
|
-
);
|
|
314
|
-
});
|
|
295
|
+
return dsToast("Failed to create account", "error");
|
|
315
296
|
}
|
|
316
297
|
|
|
317
|
-
await c.var.services.settings.setMany({
|
|
318
|
-
SITE_NAME: siteName,
|
|
319
|
-
SITE_LANGUAGE: "en",
|
|
320
|
-
});
|
|
321
298
|
await c.var.services.settings.completeOnboarding();
|
|
322
299
|
|
|
323
|
-
return
|
|
324
|
-
await stream.redirect("/signin");
|
|
325
|
-
});
|
|
300
|
+
return dsRedirect("/signin?setup");
|
|
326
301
|
} catch (err) {
|
|
327
302
|
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
328
303
|
console.error("Setup error:", err);
|
|
329
|
-
return
|
|
330
|
-
await stream.patchElements(
|
|
331
|
-
'<div id="setup-message"><div class="alert-destructive mb-4"><h2>Failed to create account</h2></div></div>',
|
|
332
|
-
);
|
|
333
|
-
});
|
|
304
|
+
return dsToast("Failed to create account", "error");
|
|
334
305
|
}
|
|
335
306
|
});
|
|
336
307
|
|
|
@@ -357,7 +328,6 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
357
328
|
</h2>
|
|
358
329
|
</header>
|
|
359
330
|
<section>
|
|
360
|
-
<div id="signin-message"></div>
|
|
361
331
|
{demoEmail && demoPassword && (
|
|
362
332
|
<p class="text-muted-foreground text-sm mb-4">
|
|
363
333
|
{t({
|
|
@@ -410,8 +380,17 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
410
380
|
|
|
411
381
|
// Signin page
|
|
412
382
|
app.get("/signin", async (c) => {
|
|
383
|
+
const isSetup = c.req.query("setup") !== undefined;
|
|
384
|
+
const isReset = c.req.query("reset") !== undefined;
|
|
385
|
+
let toast: { message: string } | undefined;
|
|
386
|
+
if (isSetup) {
|
|
387
|
+
toast = { message: "Account created successfully. Please sign in." };
|
|
388
|
+
} else if (isReset) {
|
|
389
|
+
toast = { message: "Password reset successfully. Please sign in." };
|
|
390
|
+
}
|
|
391
|
+
|
|
413
392
|
return c.html(
|
|
414
|
-
<BaseLayout title="Sign In - Jant" c={c}>
|
|
393
|
+
<BaseLayout title="Sign In - Jant" c={c} toast={toast}>
|
|
415
394
|
<SigninContent
|
|
416
395
|
demoEmail={c.env.DEMO_EMAIL}
|
|
417
396
|
demoPassword={c.env.DEMO_PASSWORD}
|
|
@@ -422,11 +401,7 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
422
401
|
|
|
423
402
|
app.post("/signin", async (c) => {
|
|
424
403
|
if (!c.var.auth) {
|
|
425
|
-
return
|
|
426
|
-
await stream.patchElements(
|
|
427
|
-
'<div id="signin-message"><div class="alert-destructive mb-4"><h2>Auth not configured</h2></div></div>',
|
|
428
|
-
);
|
|
429
|
-
});
|
|
404
|
+
return dsToast("Auth not configured", "error");
|
|
430
405
|
}
|
|
431
406
|
|
|
432
407
|
const body = await c.req.json<{ email: string; password: string }>();
|
|
@@ -445,11 +420,7 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
445
420
|
const response = await c.var.auth.handler(signInRequest);
|
|
446
421
|
|
|
447
422
|
if (!response.ok) {
|
|
448
|
-
return
|
|
449
|
-
await stream.patchElements(
|
|
450
|
-
'<div id="signin-message"><div class="alert-destructive mb-4"><h2>Invalid email or password</h2></div></div>',
|
|
451
|
-
);
|
|
452
|
-
});
|
|
423
|
+
return dsToast("Invalid email or password", "error");
|
|
453
424
|
}
|
|
454
425
|
|
|
455
426
|
// Forward Set-Cookie headers from auth response
|
|
@@ -459,21 +430,11 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
459
430
|
cookieHeaders["Set-Cookie"] = setCookie;
|
|
460
431
|
}
|
|
461
432
|
|
|
462
|
-
return
|
|
463
|
-
c,
|
|
464
|
-
async (stream) => {
|
|
465
|
-
await stream.redirect("/dash");
|
|
466
|
-
},
|
|
467
|
-
{ headers: cookieHeaders },
|
|
468
|
-
);
|
|
433
|
+
return dsRedirect("/dash", { headers: cookieHeaders });
|
|
469
434
|
} catch (err) {
|
|
470
435
|
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
471
436
|
console.error("Signin error:", err);
|
|
472
|
-
return
|
|
473
|
-
await stream.patchElements(
|
|
474
|
-
'<div id="signin-message"><div class="alert-destructive mb-4"><h2>Invalid email or password</h2></div></div>',
|
|
475
|
-
);
|
|
476
|
-
});
|
|
437
|
+
return dsToast("Invalid email or password", "error");
|
|
477
438
|
}
|
|
478
439
|
});
|
|
479
440
|
|
|
@@ -488,6 +449,223 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
488
449
|
return c.redirect("/");
|
|
489
450
|
});
|
|
490
451
|
|
|
452
|
+
// Password reset via one-time token
|
|
453
|
+
const ResetContent: FC<{ token: string }> = ({ token }) => {
|
|
454
|
+
const { t } = useLingui();
|
|
455
|
+
const signals = JSON.stringify({
|
|
456
|
+
password: "",
|
|
457
|
+
confirmPassword: "",
|
|
458
|
+
token,
|
|
459
|
+
}).replace(/</g, "\\u003c");
|
|
460
|
+
|
|
461
|
+
return (
|
|
462
|
+
<div class="min-h-screen flex items-center justify-center">
|
|
463
|
+
<div class="card max-w-md w-full">
|
|
464
|
+
<header>
|
|
465
|
+
<h2>
|
|
466
|
+
{t({
|
|
467
|
+
message: "Reset Password",
|
|
468
|
+
comment: "@context: Password reset page heading",
|
|
469
|
+
})}
|
|
470
|
+
</h2>
|
|
471
|
+
<p>
|
|
472
|
+
{t({
|
|
473
|
+
message: "Enter your new password.",
|
|
474
|
+
comment: "@context: Password reset page description",
|
|
475
|
+
})}
|
|
476
|
+
</p>
|
|
477
|
+
</header>
|
|
478
|
+
<section>
|
|
479
|
+
<form
|
|
480
|
+
data-signals={signals}
|
|
481
|
+
data-on:submit__prevent="@post('/reset')"
|
|
482
|
+
class="flex flex-col gap-4"
|
|
483
|
+
>
|
|
484
|
+
<div class="field">
|
|
485
|
+
<label class="label">
|
|
486
|
+
{t({
|
|
487
|
+
message: "New Password",
|
|
488
|
+
comment: "@context: Password reset form field",
|
|
489
|
+
})}
|
|
490
|
+
</label>
|
|
491
|
+
<input
|
|
492
|
+
type="password"
|
|
493
|
+
data-bind="password"
|
|
494
|
+
class="input"
|
|
495
|
+
required
|
|
496
|
+
minLength={8}
|
|
497
|
+
autocomplete="new-password"
|
|
498
|
+
/>
|
|
499
|
+
</div>
|
|
500
|
+
<div class="field">
|
|
501
|
+
<label class="label">
|
|
502
|
+
{t({
|
|
503
|
+
message: "Confirm Password",
|
|
504
|
+
comment: "@context: Password reset form field",
|
|
505
|
+
})}
|
|
506
|
+
</label>
|
|
507
|
+
<input
|
|
508
|
+
type="password"
|
|
509
|
+
data-bind="confirmPassword"
|
|
510
|
+
class="input"
|
|
511
|
+
required
|
|
512
|
+
minLength={8}
|
|
513
|
+
autocomplete="new-password"
|
|
514
|
+
/>
|
|
515
|
+
</div>
|
|
516
|
+
<button type="submit" class="btn">
|
|
517
|
+
{t({
|
|
518
|
+
message: "Reset Password",
|
|
519
|
+
comment: "@context: Password reset form submit button",
|
|
520
|
+
})}
|
|
521
|
+
</button>
|
|
522
|
+
</form>
|
|
523
|
+
</section>
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
);
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const ResetErrorContent: FC = () => {
|
|
530
|
+
const { t } = useLingui();
|
|
531
|
+
|
|
532
|
+
return (
|
|
533
|
+
<div class="min-h-screen flex items-center justify-center">
|
|
534
|
+
<div class="card max-w-md w-full">
|
|
535
|
+
<header>
|
|
536
|
+
<h2>
|
|
537
|
+
{t({
|
|
538
|
+
message: "Invalid or Expired Link",
|
|
539
|
+
comment: "@context: Password reset error heading",
|
|
540
|
+
})}
|
|
541
|
+
</h2>
|
|
542
|
+
</header>
|
|
543
|
+
<section>
|
|
544
|
+
<p class="text-muted-foreground">
|
|
545
|
+
{t({
|
|
546
|
+
message:
|
|
547
|
+
"This password reset link is invalid or has expired. Please generate a new one.",
|
|
548
|
+
comment: "@context: Password reset error description",
|
|
549
|
+
})}
|
|
550
|
+
</p>
|
|
551
|
+
</section>
|
|
552
|
+
</div>
|
|
553
|
+
</div>
|
|
554
|
+
);
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
app.get("/reset", async (c) => {
|
|
558
|
+
const token = c.req.query("token");
|
|
559
|
+
if (!token) {
|
|
560
|
+
return c.html(
|
|
561
|
+
<BaseLayout title="Reset Password - Jant" c={c}>
|
|
562
|
+
<ResetErrorContent />
|
|
563
|
+
</BaseLayout>,
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const stored = await c.var.services.settings.get(
|
|
568
|
+
SETTINGS_KEYS.PASSWORD_RESET_TOKEN,
|
|
569
|
+
);
|
|
570
|
+
if (!stored) {
|
|
571
|
+
return c.html(
|
|
572
|
+
<BaseLayout title="Reset Password - Jant" c={c}>
|
|
573
|
+
<ResetErrorContent />
|
|
574
|
+
</BaseLayout>,
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const separatorIndex = stored.lastIndexOf(":");
|
|
579
|
+
const storedToken = stored.substring(0, separatorIndex);
|
|
580
|
+
const expiry = parseInt(stored.substring(separatorIndex + 1), 10);
|
|
581
|
+
const now = Math.floor(Date.now() / 1000);
|
|
582
|
+
|
|
583
|
+
if (token !== storedToken || now > expiry) {
|
|
584
|
+
return c.html(
|
|
585
|
+
<BaseLayout title="Reset Password - Jant" c={c}>
|
|
586
|
+
<ResetErrorContent />
|
|
587
|
+
</BaseLayout>,
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return c.html(
|
|
592
|
+
<BaseLayout title="Reset Password - Jant" c={c}>
|
|
593
|
+
<ResetContent token={token} />
|
|
594
|
+
</BaseLayout>,
|
|
595
|
+
);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
app.post("/reset", async (c) => {
|
|
599
|
+
const body = await c.req.json<{
|
|
600
|
+
password: string;
|
|
601
|
+
confirmPassword: string;
|
|
602
|
+
token: string;
|
|
603
|
+
}>();
|
|
604
|
+
const { password, confirmPassword, token } = body;
|
|
605
|
+
|
|
606
|
+
// Validate token
|
|
607
|
+
const stored = await c.var.services.settings.get(
|
|
608
|
+
SETTINGS_KEYS.PASSWORD_RESET_TOKEN,
|
|
609
|
+
);
|
|
610
|
+
if (!stored) {
|
|
611
|
+
return dsToast("Invalid or expired reset link.", "error");
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const separatorIndex = stored.lastIndexOf(":");
|
|
615
|
+
const storedToken = stored.substring(0, separatorIndex);
|
|
616
|
+
const expiry = parseInt(stored.substring(separatorIndex + 1), 10);
|
|
617
|
+
const now = Math.floor(Date.now() / 1000);
|
|
618
|
+
|
|
619
|
+
if (token !== storedToken || now > expiry) {
|
|
620
|
+
return dsToast("Invalid or expired reset link.", "error");
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Validate passwords
|
|
624
|
+
if (!password || password.length < 8) {
|
|
625
|
+
return dsToast("Password must be at least 8 characters.", "error");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (password !== confirmPassword) {
|
|
629
|
+
return dsToast("Passwords do not match.", "error");
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
try {
|
|
633
|
+
const hashedPassword = await hashPassword(password);
|
|
634
|
+
const db = c.env.DB.withSession() as unknown as D1Database;
|
|
635
|
+
|
|
636
|
+
// Get admin user
|
|
637
|
+
const userResult = await db
|
|
638
|
+
.prepare("SELECT id FROM user LIMIT 1")
|
|
639
|
+
.first<{ id: string }>();
|
|
640
|
+
if (!userResult) {
|
|
641
|
+
return dsToast("No user account found.", "error");
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Update password
|
|
645
|
+
await db
|
|
646
|
+
.prepare(
|
|
647
|
+
"UPDATE account SET password = ? WHERE user_id = ? AND provider_id = 'credential'",
|
|
648
|
+
)
|
|
649
|
+
.bind(hashedPassword, userResult.id)
|
|
650
|
+
.run();
|
|
651
|
+
|
|
652
|
+
// Delete all sessions
|
|
653
|
+
await db
|
|
654
|
+
.prepare("DELETE FROM session WHERE user_id = ?")
|
|
655
|
+
.bind(userResult.id)
|
|
656
|
+
.run();
|
|
657
|
+
|
|
658
|
+
// Delete the reset token
|
|
659
|
+
await c.var.services.settings.remove(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
|
|
660
|
+
|
|
661
|
+
return dsRedirect("/signin?reset");
|
|
662
|
+
} catch (err) {
|
|
663
|
+
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
664
|
+
console.error("Password reset error:", err);
|
|
665
|
+
return dsToast("Failed to reset password.", "error");
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
|
|
491
669
|
// Dashboard routes (protected)
|
|
492
670
|
app.use("/dash/*", requireAuth());
|
|
493
671
|
app.route("/dash", dashIndexRoutes);
|
|
@@ -497,6 +675,7 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
497
675
|
app.route("/dash/settings", dashSettingsRoutes);
|
|
498
676
|
app.route("/dash/redirects", dashRedirectsRoutes);
|
|
499
677
|
app.route("/dash/collections", dashCollectionsRoutes);
|
|
678
|
+
app.route("/dash/appearance", dashAppearanceRoutes);
|
|
500
679
|
|
|
501
680
|
// API routes
|
|
502
681
|
app.route("/api/upload", uploadApiRoutes);
|
package/src/client.ts
CHANGED
|
@@ -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(
|