@jant/core 0.2.16 → 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 +5 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +332 -119
- 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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/config.d.ts +83 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +104 -0
- 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/collections.d.ts.map +1 -1
- package/dist/routes/dash/collections.js +5 -4
- package/dist/routes/dash/index.d.ts.map +1 -1
- package/dist/routes/dash/index.js +2 -1
- package/dist/routes/dash/media.d.ts.map +1 -1
- package/dist/routes/dash/media.js +3 -2
- package/dist/routes/dash/pages.d.ts.map +1 -1
- package/dist/routes/dash/pages.js +5 -4
- package/dist/routes/dash/posts.d.ts.map +1 -1
- package/dist/routes/dash/posts.js +5 -4
- package/dist/routes/dash/redirects.d.ts.map +1 -1
- package/dist/routes/dash/redirects.js +3 -2
- package/dist/routes/dash/settings.d.ts.map +1 -1
- package/dist/routes/dash/settings.js +39 -38
- package/dist/routes/pages/archive.d.ts.map +1 -1
- package/dist/routes/pages/archive.js +2 -1
- package/dist/routes/pages/collection.d.ts.map +1 -1
- package/dist/routes/pages/collection.js +2 -1
- package/dist/routes/pages/home.d.ts.map +1 -1
- package/dist/routes/pages/home.js +2 -1
- package/dist/routes/pages/page.d.ts.map +1 -1
- package/dist/routes/pages/page.js +2 -1
- package/dist/routes/pages/post.d.ts.map +1 -1
- package/dist/routes/pages/post.js +2 -1
- package/dist/routes/pages/search.d.ts.map +1 -1
- package/dist/routes/pages/search.js +2 -1
- 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 +64 -32
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +52 -0
- package/package.json +1 -1
- package/src/app.tsx +286 -59
- 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/index.ts +0 -2
- package/src/lib/config.ts +120 -0
- 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/collections.tsx +5 -4
- package/src/routes/dash/index.tsx +2 -1
- package/src/routes/dash/media.tsx +3 -2
- package/src/routes/dash/pages.tsx +5 -4
- package/src/routes/dash/posts.tsx +5 -4
- package/src/routes/dash/redirects.tsx +3 -2
- package/src/routes/dash/settings.tsx +51 -49
- package/src/routes/pages/archive.tsx +2 -1
- package/src/routes/pages/collection.tsx +2 -1
- package/src/routes/pages/home.tsx +2 -1
- package/src/routes/pages/page.tsx +2 -1
- package/src/routes/pages/post.tsx +2 -1
- package/src/routes/pages/search.tsx +2 -1
- 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 +74 -34
- 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 }>;
|
|
@@ -59,12 +64,15 @@ export type App = Hono<{ Bindings: Bindings; Variables: AppVariables }>;
|
|
|
59
64
|
* @param config - Optional configuration
|
|
60
65
|
* @returns Hono app instance
|
|
61
66
|
*
|
|
67
|
+
* Site settings (name, description, language) should be configured via
|
|
68
|
+
* environment variables (SITE_NAME, SITE_DESCRIPTION, SITE_LANGUAGE).
|
|
69
|
+
* They can also be set in the dashboard, which stores them in the database.
|
|
70
|
+
*
|
|
62
71
|
* @example
|
|
63
72
|
* ```typescript
|
|
64
73
|
* import { createApp } from "@jant/core";
|
|
65
74
|
*
|
|
66
75
|
* export default createApp({
|
|
67
|
-
* site: { name: "My Blog" },
|
|
68
76
|
* theme: { components: { PostCard: MyPostCard } },
|
|
69
77
|
* });
|
|
70
78
|
* ```
|
|
@@ -74,13 +82,20 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
74
82
|
|
|
75
83
|
// Initialize services, auth, and config middleware
|
|
76
84
|
app.use("*", async (c, next) => {
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
// Use withSession() to enable D1 Read Replication
|
|
86
|
+
// Automatically routes read queries to the nearest replica for lower latency
|
|
87
|
+
// See: https://developers.cloudflare.com/d1/best-practices/read-replication/
|
|
88
|
+
const session = c.env.DB.withSession();
|
|
89
|
+
|
|
90
|
+
// Note: Drizzle ORM doesn't officially support D1DatabaseSession yet (issue #2226)
|
|
91
|
+
// but it works at runtime. We use type assertion as a temporary workaround.
|
|
92
|
+
const db = createDatabase(session as unknown as D1Database);
|
|
93
|
+
const services = createServices(db, session as unknown as D1Database);
|
|
79
94
|
c.set("services", services);
|
|
80
95
|
c.set("config", config);
|
|
81
96
|
|
|
82
97
|
if (c.env.AUTH_SECRET) {
|
|
83
|
-
const auth = createAuth(
|
|
98
|
+
const auth = createAuth(session as unknown as D1Database, {
|
|
84
99
|
secret: c.env.AUTH_SECRET,
|
|
85
100
|
baseURL: c.env.SITE_URL,
|
|
86
101
|
});
|
|
@@ -90,6 +105,18 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
90
105
|
await next();
|
|
91
106
|
});
|
|
92
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
|
+
|
|
93
120
|
// i18n middleware
|
|
94
121
|
app.use("*", i18nMiddleware());
|
|
95
122
|
|
|
@@ -155,36 +182,17 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
155
182
|
</h2>
|
|
156
183
|
<p>
|
|
157
184
|
{t({
|
|
158
|
-
message: "
|
|
185
|
+
message: "Create your admin account.",
|
|
159
186
|
comment: "@context: Setup page description",
|
|
160
187
|
})}
|
|
161
188
|
</p>
|
|
162
189
|
</header>
|
|
163
190
|
<section>
|
|
164
|
-
<div id="setup-message"></div>
|
|
165
191
|
<form
|
|
166
|
-
data-signals="{
|
|
192
|
+
data-signals="{name: '', email: '', password: ''}"
|
|
167
193
|
data-on:submit__prevent="@post('/setup')"
|
|
168
194
|
class="flex flex-col gap-4"
|
|
169
195
|
>
|
|
170
|
-
<div class="field">
|
|
171
|
-
<label class="label">
|
|
172
|
-
{t({
|
|
173
|
-
message: "Site Name",
|
|
174
|
-
comment: "@context: Setup form field - site name",
|
|
175
|
-
})}
|
|
176
|
-
</label>
|
|
177
|
-
<input
|
|
178
|
-
type="text"
|
|
179
|
-
data-bind="siteName"
|
|
180
|
-
class="input"
|
|
181
|
-
required
|
|
182
|
-
placeholder={t({
|
|
183
|
-
message: "My Blog",
|
|
184
|
-
comment: "@context: Setup site name placeholder",
|
|
185
|
-
})}
|
|
186
|
-
/>
|
|
187
|
-
</div>
|
|
188
196
|
<div class="field">
|
|
189
197
|
<label class="label">
|
|
190
198
|
{t({
|
|
@@ -260,34 +268,27 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
260
268
|
if (isComplete) return c.redirect("/");
|
|
261
269
|
|
|
262
270
|
const body = await c.req.json<{
|
|
263
|
-
siteName: string;
|
|
264
271
|
name: string;
|
|
265
272
|
email: string;
|
|
266
273
|
password: string;
|
|
267
274
|
}>();
|
|
268
|
-
const {
|
|
275
|
+
const { name, email, password } = body;
|
|
269
276
|
|
|
270
|
-
if (!
|
|
277
|
+
if (!name || !email || !password) {
|
|
271
278
|
return sse(c, async (stream) => {
|
|
272
|
-
await stream.
|
|
273
|
-
'<div id="setup-message"><div class="alert-destructive mb-4"><h2>All fields are required</h2></div></div>',
|
|
274
|
-
);
|
|
279
|
+
await stream.toast("All fields are required", "error");
|
|
275
280
|
});
|
|
276
281
|
}
|
|
277
282
|
|
|
278
283
|
if (password.length < 8) {
|
|
279
284
|
return sse(c, async (stream) => {
|
|
280
|
-
await stream.
|
|
281
|
-
'<div id="setup-message"><div class="alert-destructive mb-4"><h2>Password must be at least 8 characters</h2></div></div>',
|
|
282
|
-
);
|
|
285
|
+
await stream.toast("Password must be at least 8 characters", "error");
|
|
283
286
|
});
|
|
284
287
|
}
|
|
285
288
|
|
|
286
289
|
if (!c.var.auth) {
|
|
287
290
|
return sse(c, async (stream) => {
|
|
288
|
-
await stream.
|
|
289
|
-
'<div id="setup-message"><div class="alert-destructive mb-4"><h2>AUTH_SECRET not configured</h2></div></div>',
|
|
290
|
-
);
|
|
291
|
+
await stream.toast("AUTH_SECRET not configured", "error");
|
|
291
292
|
});
|
|
292
293
|
}
|
|
293
294
|
|
|
@@ -298,28 +299,20 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
298
299
|
|
|
299
300
|
if (!signUpResponse || "error" in signUpResponse) {
|
|
300
301
|
return sse(c, async (stream) => {
|
|
301
|
-
await stream.
|
|
302
|
-
'<div id="setup-message"><div class="alert-destructive mb-4"><h2>Failed to create account</h2></div></div>',
|
|
303
|
-
);
|
|
302
|
+
await stream.toast("Failed to create account", "error");
|
|
304
303
|
});
|
|
305
304
|
}
|
|
306
305
|
|
|
307
|
-
await c.var.services.settings.setMany({
|
|
308
|
-
SITE_NAME: siteName,
|
|
309
|
-
SITE_LANGUAGE: "en",
|
|
310
|
-
});
|
|
311
306
|
await c.var.services.settings.completeOnboarding();
|
|
312
307
|
|
|
313
308
|
return sse(c, async (stream) => {
|
|
314
|
-
await stream.redirect("/signin");
|
|
309
|
+
await stream.redirect("/signin?setup");
|
|
315
310
|
});
|
|
316
311
|
} catch (err) {
|
|
317
312
|
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
318
313
|
console.error("Setup error:", err);
|
|
319
314
|
return sse(c, async (stream) => {
|
|
320
|
-
await stream.
|
|
321
|
-
'<div id="setup-message"><div class="alert-destructive mb-4"><h2>Failed to create account</h2></div></div>',
|
|
322
|
-
);
|
|
315
|
+
await stream.toast("Failed to create account", "error");
|
|
323
316
|
});
|
|
324
317
|
}
|
|
325
318
|
});
|
|
@@ -347,7 +340,6 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
347
340
|
</h2>
|
|
348
341
|
</header>
|
|
349
342
|
<section>
|
|
350
|
-
<div id="signin-message"></div>
|
|
351
343
|
{demoEmail && demoPassword && (
|
|
352
344
|
<p class="text-muted-foreground text-sm mb-4">
|
|
353
345
|
{t({
|
|
@@ -400,8 +392,17 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
400
392
|
|
|
401
393
|
// Signin page
|
|
402
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
|
+
|
|
403
404
|
return c.html(
|
|
404
|
-
<BaseLayout title="Sign In - Jant" c={c}>
|
|
405
|
+
<BaseLayout title="Sign In - Jant" c={c} toast={toast}>
|
|
405
406
|
<SigninContent
|
|
406
407
|
demoEmail={c.env.DEMO_EMAIL}
|
|
407
408
|
demoPassword={c.env.DEMO_PASSWORD}
|
|
@@ -413,9 +414,7 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
413
414
|
app.post("/signin", async (c) => {
|
|
414
415
|
if (!c.var.auth) {
|
|
415
416
|
return sse(c, async (stream) => {
|
|
416
|
-
await stream.
|
|
417
|
-
'<div id="signin-message"><div class="alert-destructive mb-4"><h2>Auth not configured</h2></div></div>',
|
|
418
|
-
);
|
|
417
|
+
await stream.toast("Auth not configured", "error");
|
|
419
418
|
});
|
|
420
419
|
}
|
|
421
420
|
|
|
@@ -436,9 +435,7 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
436
435
|
|
|
437
436
|
if (!response.ok) {
|
|
438
437
|
return sse(c, async (stream) => {
|
|
439
|
-
await stream.
|
|
440
|
-
'<div id="signin-message"><div class="alert-destructive mb-4"><h2>Invalid email or password</h2></div></div>',
|
|
441
|
-
);
|
|
438
|
+
await stream.toast("Invalid email or password", "error");
|
|
442
439
|
});
|
|
443
440
|
}
|
|
444
441
|
|
|
@@ -460,9 +457,7 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
460
457
|
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
461
458
|
console.error("Signin error:", err);
|
|
462
459
|
return sse(c, async (stream) => {
|
|
463
|
-
await stream.
|
|
464
|
-
'<div id="signin-message"><div class="alert-destructive mb-4"><h2>Invalid email or password</h2></div></div>',
|
|
465
|
-
);
|
|
460
|
+
await stream.toast("Invalid email or password", "error");
|
|
466
461
|
});
|
|
467
462
|
}
|
|
468
463
|
});
|
|
@@ -478,6 +473,237 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
478
473
|
return c.redirect("/");
|
|
479
474
|
});
|
|
480
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
|
+
|
|
481
707
|
// Dashboard routes (protected)
|
|
482
708
|
app.use("/dash/*", requireAuth());
|
|
483
709
|
app.route("/dash", dashIndexRoutes);
|
|
@@ -487,6 +713,7 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
487
713
|
app.route("/dash/settings", dashSettingsRoutes);
|
|
488
714
|
app.route("/dash/redirects", dashRedirectsRoutes);
|
|
489
715
|
app.route("/dash/collections", dashCollectionsRoutes);
|
|
716
|
+
app.route("/dash/appearance", dashAppearanceRoutes);
|
|
490
717
|
|
|
491
718
|
// API routes
|
|
492
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(
|