@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/dist/app.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface AppVariables {
|
|
|
9
9
|
services: Services;
|
|
10
10
|
auth: Auth;
|
|
11
11
|
config: JantConfig;
|
|
12
|
+
themeStyle: string;
|
|
12
13
|
}
|
|
13
14
|
export type App = Hono<{
|
|
14
15
|
Bindings: Bindings;
|
|
@@ -20,12 +21,15 @@ export type App = Hono<{
|
|
|
20
21
|
* @param config - Optional configuration
|
|
21
22
|
* @returns Hono app instance
|
|
22
23
|
*
|
|
24
|
+
* Site settings (name, description, language) should be configured via
|
|
25
|
+
* environment variables (SITE_NAME, SITE_DESCRIPTION, SITE_LANGUAGE).
|
|
26
|
+
* They can also be set in the dashboard, which stores them in the database.
|
|
27
|
+
*
|
|
23
28
|
* @example
|
|
24
29
|
* ```typescript
|
|
25
30
|
* import { createApp } from "@jant/core";
|
|
26
31
|
*
|
|
27
32
|
* export default createApp({
|
|
28
|
-
* site: { name: "My Blog" },
|
|
29
33
|
* theme: { components: { PostCard: MyPostCard } },
|
|
30
34
|
* });
|
|
31
35
|
* ```
|
package/dist/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EAAc,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAGlD,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EAAc,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAGlD,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAwCvD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,UAAU,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,GAAG,GAAG,IAAI,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,YAAY,CAAA;CAAE,CAAC,CAAC;AAExE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,SAAS,CAAC,MAAM,GAAE,UAAe,GAAG,GAAG,CA+qBtD"}
|
package/dist/app.js
CHANGED
|
@@ -7,6 +7,8 @@ import { createServices } from "./services/index.js";
|
|
|
7
7
|
import { createAuth } from "./auth.js";
|
|
8
8
|
import { i18nMiddleware } from "./i18n/index.js";
|
|
9
9
|
import { useLingui as $_useLingui } from "@jant/core/i18n";
|
|
10
|
+
import { SETTINGS_KEYS } from "./lib/constants.js";
|
|
11
|
+
import { hashPassword } from "better-auth/crypto";
|
|
10
12
|
// Routes - Pages
|
|
11
13
|
import { homeRoutes } from "./routes/pages/home.js";
|
|
12
14
|
import { postRoutes } from "./routes/pages/post.js";
|
|
@@ -22,6 +24,7 @@ import { mediaRoutes as dashMediaRoutes } from "./routes/dash/media.js";
|
|
|
22
24
|
import { settingsRoutes as dashSettingsRoutes } from "./routes/dash/settings.js";
|
|
23
25
|
import { redirectsRoutes as dashRedirectsRoutes } from "./routes/dash/redirects.js";
|
|
24
26
|
import { collectionsRoutes as dashCollectionsRoutes } from "./routes/dash/collections.js";
|
|
27
|
+
import { appearanceRoutes as dashAppearanceRoutes } from "./routes/dash/appearance.js";
|
|
25
28
|
// Routes - API
|
|
26
29
|
import { postsApiRoutes } from "./routes/api/posts.js";
|
|
27
30
|
import { uploadApiRoutes } from "./routes/api/upload.js";
|
|
@@ -34,18 +37,22 @@ import { requireAuth } from "./middleware/auth.js";
|
|
|
34
37
|
// Layouts for auth pages
|
|
35
38
|
import { BaseLayout } from "./theme/layouts/index.js";
|
|
36
39
|
import { sse } from "./lib/sse.js";
|
|
40
|
+
import { getAvailableThemes, buildThemeStyle } from "./lib/theme.js";
|
|
37
41
|
/**
|
|
38
42
|
* Create a Jant application
|
|
39
43
|
*
|
|
40
44
|
* @param config - Optional configuration
|
|
41
45
|
* @returns Hono app instance
|
|
42
46
|
*
|
|
47
|
+
* Site settings (name, description, language) should be configured via
|
|
48
|
+
* environment variables (SITE_NAME, SITE_DESCRIPTION, SITE_LANGUAGE).
|
|
49
|
+
* They can also be set in the dashboard, which stores them in the database.
|
|
50
|
+
*
|
|
43
51
|
* @example
|
|
44
52
|
* ```typescript
|
|
45
53
|
* import { createApp } from "@jant/core";
|
|
46
54
|
*
|
|
47
55
|
* export default createApp({
|
|
48
|
-
* site: { name: "My Blog" },
|
|
49
56
|
* theme: { components: { PostCard: MyPostCard } },
|
|
50
57
|
* });
|
|
51
58
|
* ```
|
|
@@ -53,12 +60,18 @@ import { sse } from "./lib/sse.js";
|
|
|
53
60
|
const app = new Hono();
|
|
54
61
|
// Initialize services, auth, and config middleware
|
|
55
62
|
app.use("*", async (c, next)=>{
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
// Use withSession() to enable D1 Read Replication
|
|
64
|
+
// Automatically routes read queries to the nearest replica for lower latency
|
|
65
|
+
// See: https://developers.cloudflare.com/d1/best-practices/read-replication/
|
|
66
|
+
const session = c.env.DB.withSession();
|
|
67
|
+
// Note: Drizzle ORM doesn't officially support D1DatabaseSession yet (issue #2226)
|
|
68
|
+
// but it works at runtime. We use type assertion as a temporary workaround.
|
|
69
|
+
const db = createDatabase(session);
|
|
70
|
+
const services = createServices(db, session);
|
|
58
71
|
c.set("services", services);
|
|
59
72
|
c.set("config", config);
|
|
60
73
|
if (c.env.AUTH_SECRET) {
|
|
61
|
-
const auth = createAuth(
|
|
74
|
+
const auth = createAuth(session, {
|
|
62
75
|
secret: c.env.AUTH_SECRET,
|
|
63
76
|
baseURL: c.env.SITE_URL
|
|
64
77
|
});
|
|
@@ -66,6 +79,15 @@ import { sse } from "./lib/sse.js";
|
|
|
66
79
|
}
|
|
67
80
|
await next();
|
|
68
81
|
});
|
|
82
|
+
// Theme middleware - resolve active color theme and build CSS
|
|
83
|
+
app.use("*", async (c, next)=>{
|
|
84
|
+
const themeId = await c.var.services.settings.get(SETTINGS_KEYS.THEME);
|
|
85
|
+
const themes = getAvailableThemes(config);
|
|
86
|
+
const activeTheme = themeId ? themes.find((t)=>t.id === themeId) : undefined;
|
|
87
|
+
const themeStyle = buildThemeStyle(activeTheme, config.theme?.cssVariables);
|
|
88
|
+
c.set("themeStyle", themeStyle);
|
|
89
|
+
await next();
|
|
90
|
+
});
|
|
69
91
|
// i18n middleware
|
|
70
92
|
app.use("*", i18nMiddleware());
|
|
71
93
|
// Trailing slash redirect (redirect /foo/ to /foo)
|
|
@@ -125,112 +147,85 @@ import { sse } from "./lib/sse.js";
|
|
|
125
147
|
}),
|
|
126
148
|
/*#__PURE__*/ _jsx("p", {
|
|
127
149
|
children: $__i18n._({
|
|
128
|
-
id: "
|
|
129
|
-
message: "
|
|
150
|
+
id: "GX2VMa",
|
|
151
|
+
message: "Create your admin account."
|
|
130
152
|
})
|
|
131
153
|
})
|
|
132
154
|
]
|
|
133
155
|
}),
|
|
134
|
-
/*#__PURE__*/
|
|
135
|
-
children:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
"
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
class: "label",
|
|
149
|
-
children: $__i18n._({
|
|
150
|
-
id: "SJmfuf",
|
|
151
|
-
message: "Site Name"
|
|
152
|
-
})
|
|
153
|
-
}),
|
|
154
|
-
/*#__PURE__*/ _jsx("input", {
|
|
155
|
-
type: "text",
|
|
156
|
-
"data-bind": "siteName",
|
|
157
|
-
class: "input",
|
|
158
|
-
required: true,
|
|
159
|
-
placeholder: $__i18n._({
|
|
160
|
-
id: "HfyyXl",
|
|
161
|
-
message: "My Blog"
|
|
162
|
-
})
|
|
156
|
+
/*#__PURE__*/ _jsx("section", {
|
|
157
|
+
children: /*#__PURE__*/ _jsxs("form", {
|
|
158
|
+
"data-signals": "{name: '', email: '', password: ''}",
|
|
159
|
+
"data-on:submit__prevent": "@post('/setup')",
|
|
160
|
+
class: "flex flex-col gap-4",
|
|
161
|
+
children: [
|
|
162
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
163
|
+
class: "field",
|
|
164
|
+
children: [
|
|
165
|
+
/*#__PURE__*/ _jsx("label", {
|
|
166
|
+
class: "label",
|
|
167
|
+
children: $__i18n._({
|
|
168
|
+
id: "/Rj5P4",
|
|
169
|
+
message: "Your Name"
|
|
163
170
|
})
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
})
|
|
183
|
-
]
|
|
184
|
-
}),
|
|
185
|
-
/*#__PURE__*/ _jsxs("div", {
|
|
186
|
-
class: "field",
|
|
187
|
-
children: [
|
|
188
|
-
/*#__PURE__*/ _jsx("label", {
|
|
189
|
-
class: "label",
|
|
190
|
-
children: $__i18n._({
|
|
191
|
-
id: "O3oNi5",
|
|
192
|
-
message: "Email"
|
|
193
|
-
})
|
|
194
|
-
}),
|
|
195
|
-
/*#__PURE__*/ _jsx("input", {
|
|
196
|
-
type: "email",
|
|
197
|
-
"data-bind": "email",
|
|
198
|
-
class: "input",
|
|
199
|
-
required: true,
|
|
200
|
-
placeholder: "you@example.com"
|
|
171
|
+
}),
|
|
172
|
+
/*#__PURE__*/ _jsx("input", {
|
|
173
|
+
type: "text",
|
|
174
|
+
"data-bind": "name",
|
|
175
|
+
class: "input",
|
|
176
|
+
required: true,
|
|
177
|
+
placeholder: "John Doe"
|
|
178
|
+
})
|
|
179
|
+
]
|
|
180
|
+
}),
|
|
181
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
182
|
+
class: "field",
|
|
183
|
+
children: [
|
|
184
|
+
/*#__PURE__*/ _jsx("label", {
|
|
185
|
+
class: "label",
|
|
186
|
+
children: $__i18n._({
|
|
187
|
+
id: "O3oNi5",
|
|
188
|
+
message: "Email"
|
|
201
189
|
})
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
190
|
+
}),
|
|
191
|
+
/*#__PURE__*/ _jsx("input", {
|
|
192
|
+
type: "email",
|
|
193
|
+
"data-bind": "email",
|
|
194
|
+
class: "input",
|
|
195
|
+
required: true,
|
|
196
|
+
placeholder: "you@example.com"
|
|
197
|
+
})
|
|
198
|
+
]
|
|
199
|
+
}),
|
|
200
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
201
|
+
class: "field",
|
|
202
|
+
children: [
|
|
203
|
+
/*#__PURE__*/ _jsx("label", {
|
|
204
|
+
class: "label",
|
|
205
|
+
children: $__i18n._({
|
|
206
|
+
id: "8ZsakT",
|
|
207
|
+
message: "Password"
|
|
220
208
|
})
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
message: "Complete Setup"
|
|
209
|
+
}),
|
|
210
|
+
/*#__PURE__*/ _jsx("input", {
|
|
211
|
+
type: "password",
|
|
212
|
+
"data-bind": "password",
|
|
213
|
+
class: "input",
|
|
214
|
+
required: true,
|
|
215
|
+
minLength: 8
|
|
229
216
|
})
|
|
217
|
+
]
|
|
218
|
+
}),
|
|
219
|
+
/*#__PURE__*/ _jsx("button", {
|
|
220
|
+
type: "submit",
|
|
221
|
+
class: "btn",
|
|
222
|
+
children: $__i18n._({
|
|
223
|
+
id: "EGwzOK",
|
|
224
|
+
message: "Complete Setup"
|
|
230
225
|
})
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
226
|
+
})
|
|
227
|
+
]
|
|
228
|
+
})
|
|
234
229
|
})
|
|
235
230
|
]
|
|
236
231
|
})
|
|
@@ -250,20 +245,20 @@ import { sse } from "./lib/sse.js";
|
|
|
250
245
|
const isComplete = await c.var.services.settings.isOnboardingComplete();
|
|
251
246
|
if (isComplete) return c.redirect("/");
|
|
252
247
|
const body = await c.req.json();
|
|
253
|
-
const {
|
|
254
|
-
if (!
|
|
248
|
+
const { name, email, password } = body;
|
|
249
|
+
if (!name || !email || !password) {
|
|
255
250
|
return sse(c, async (stream)=>{
|
|
256
|
-
await stream.
|
|
251
|
+
await stream.toast("All fields are required", "error");
|
|
257
252
|
});
|
|
258
253
|
}
|
|
259
254
|
if (password.length < 8) {
|
|
260
255
|
return sse(c, async (stream)=>{
|
|
261
|
-
await stream.
|
|
256
|
+
await stream.toast("Password must be at least 8 characters", "error");
|
|
262
257
|
});
|
|
263
258
|
}
|
|
264
259
|
if (!c.var.auth) {
|
|
265
260
|
return sse(c, async (stream)=>{
|
|
266
|
-
await stream.
|
|
261
|
+
await stream.toast("AUTH_SECRET not configured", "error");
|
|
267
262
|
});
|
|
268
263
|
}
|
|
269
264
|
try {
|
|
@@ -276,22 +271,18 @@ import { sse } from "./lib/sse.js";
|
|
|
276
271
|
});
|
|
277
272
|
if (!signUpResponse || "error" in signUpResponse) {
|
|
278
273
|
return sse(c, async (stream)=>{
|
|
279
|
-
await stream.
|
|
274
|
+
await stream.toast("Failed to create account", "error");
|
|
280
275
|
});
|
|
281
276
|
}
|
|
282
|
-
await c.var.services.settings.setMany({
|
|
283
|
-
SITE_NAME: siteName,
|
|
284
|
-
SITE_LANGUAGE: "en"
|
|
285
|
-
});
|
|
286
277
|
await c.var.services.settings.completeOnboarding();
|
|
287
278
|
return sse(c, async (stream)=>{
|
|
288
|
-
await stream.redirect("/signin");
|
|
279
|
+
await stream.redirect("/signin?setup");
|
|
289
280
|
});
|
|
290
281
|
} catch (err) {
|
|
291
282
|
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
292
283
|
console.error("Setup error:", err);
|
|
293
284
|
return sse(c, async (stream)=>{
|
|
294
|
-
await stream.
|
|
285
|
+
await stream.toast("Failed to create account", "error");
|
|
295
286
|
});
|
|
296
287
|
}
|
|
297
288
|
});
|
|
@@ -317,9 +308,6 @@ import { sse } from "./lib/sse.js";
|
|
|
317
308
|
}),
|
|
318
309
|
/*#__PURE__*/ _jsxs("section", {
|
|
319
310
|
children: [
|
|
320
|
-
/*#__PURE__*/ _jsx("div", {
|
|
321
|
-
id: "signin-message"
|
|
322
|
-
}),
|
|
323
311
|
demoEmail && demoPassword && /*#__PURE__*/ _jsx("p", {
|
|
324
312
|
class: "text-muted-foreground text-sm mb-4",
|
|
325
313
|
children: $__i18n._({
|
|
@@ -386,9 +374,22 @@ import { sse } from "./lib/sse.js";
|
|
|
386
374
|
};
|
|
387
375
|
// Signin page
|
|
388
376
|
app.get("/signin", async (c)=>{
|
|
377
|
+
const isSetup = c.req.query("setup") !== undefined;
|
|
378
|
+
const isReset = c.req.query("reset") !== undefined;
|
|
379
|
+
let toast;
|
|
380
|
+
if (isSetup) {
|
|
381
|
+
toast = {
|
|
382
|
+
message: "Account created successfully. Please sign in."
|
|
383
|
+
};
|
|
384
|
+
} else if (isReset) {
|
|
385
|
+
toast = {
|
|
386
|
+
message: "Password reset successfully. Please sign in."
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
389
|
return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
|
|
390
390
|
title: "Sign In - Jant",
|
|
391
391
|
c: c,
|
|
392
|
+
toast: toast,
|
|
392
393
|
children: /*#__PURE__*/ _jsx(SigninContent, {
|
|
393
394
|
demoEmail: c.env.DEMO_EMAIL,
|
|
394
395
|
demoPassword: c.env.DEMO_PASSWORD
|
|
@@ -398,7 +399,7 @@ import { sse } from "./lib/sse.js";
|
|
|
398
399
|
app.post("/signin", async (c)=>{
|
|
399
400
|
if (!c.var.auth) {
|
|
400
401
|
return sse(c, async (stream)=>{
|
|
401
|
-
await stream.
|
|
402
|
+
await stream.toast("Auth not configured", "error");
|
|
402
403
|
});
|
|
403
404
|
}
|
|
404
405
|
const body = await c.req.json();
|
|
@@ -417,7 +418,7 @@ import { sse } from "./lib/sse.js";
|
|
|
417
418
|
const response = await c.var.auth.handler(signInRequest);
|
|
418
419
|
if (!response.ok) {
|
|
419
420
|
return sse(c, async (stream)=>{
|
|
420
|
-
await stream.
|
|
421
|
+
await stream.toast("Invalid email or password", "error");
|
|
421
422
|
});
|
|
422
423
|
}
|
|
423
424
|
// Forward Set-Cookie headers from auth response
|
|
@@ -435,7 +436,7 @@ import { sse } from "./lib/sse.js";
|
|
|
435
436
|
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
436
437
|
console.error("Signin error:", err);
|
|
437
438
|
return sse(c, async (stream)=>{
|
|
438
|
-
await stream.
|
|
439
|
+
await stream.toast("Invalid email or password", "error");
|
|
439
440
|
});
|
|
440
441
|
}
|
|
441
442
|
});
|
|
@@ -451,6 +452,217 @@ import { sse } from "./lib/sse.js";
|
|
|
451
452
|
}
|
|
452
453
|
return c.redirect("/");
|
|
453
454
|
});
|
|
455
|
+
// Password reset via one-time token
|
|
456
|
+
const ResetContent = ({ token })=>{
|
|
457
|
+
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
458
|
+
const signals = JSON.stringify({
|
|
459
|
+
password: "",
|
|
460
|
+
confirmPassword: "",
|
|
461
|
+
token
|
|
462
|
+
}).replace(/</g, "\\u003c");
|
|
463
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
464
|
+
class: "min-h-screen flex items-center justify-center",
|
|
465
|
+
children: /*#__PURE__*/ _jsxs("div", {
|
|
466
|
+
class: "card max-w-md w-full",
|
|
467
|
+
children: [
|
|
468
|
+
/*#__PURE__*/ _jsxs("header", {
|
|
469
|
+
children: [
|
|
470
|
+
/*#__PURE__*/ _jsx("h2", {
|
|
471
|
+
children: $__i18n._({
|
|
472
|
+
id: "KbS2K9",
|
|
473
|
+
message: "Reset Password"
|
|
474
|
+
})
|
|
475
|
+
}),
|
|
476
|
+
/*#__PURE__*/ _jsx("p", {
|
|
477
|
+
children: $__i18n._({
|
|
478
|
+
id: "hWOZIv",
|
|
479
|
+
message: "Enter your new password."
|
|
480
|
+
})
|
|
481
|
+
})
|
|
482
|
+
]
|
|
483
|
+
}),
|
|
484
|
+
/*#__PURE__*/ _jsx("section", {
|
|
485
|
+
children: /*#__PURE__*/ _jsxs("form", {
|
|
486
|
+
"data-signals": signals,
|
|
487
|
+
"data-on:submit__prevent": "@post('/reset')",
|
|
488
|
+
class: "flex flex-col gap-4",
|
|
489
|
+
children: [
|
|
490
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
491
|
+
class: "field",
|
|
492
|
+
children: [
|
|
493
|
+
/*#__PURE__*/ _jsx("label", {
|
|
494
|
+
class: "label",
|
|
495
|
+
children: $__i18n._({
|
|
496
|
+
id: "7vhWI8",
|
|
497
|
+
message: "New Password"
|
|
498
|
+
})
|
|
499
|
+
}),
|
|
500
|
+
/*#__PURE__*/ _jsx("input", {
|
|
501
|
+
type: "password",
|
|
502
|
+
"data-bind": "password",
|
|
503
|
+
class: "input",
|
|
504
|
+
required: true,
|
|
505
|
+
minLength: 8,
|
|
506
|
+
autocomplete: "new-password"
|
|
507
|
+
})
|
|
508
|
+
]
|
|
509
|
+
}),
|
|
510
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
511
|
+
class: "field",
|
|
512
|
+
children: [
|
|
513
|
+
/*#__PURE__*/ _jsx("label", {
|
|
514
|
+
class: "label",
|
|
515
|
+
children: $__i18n._({
|
|
516
|
+
id: "p2/GCq",
|
|
517
|
+
message: "Confirm Password"
|
|
518
|
+
})
|
|
519
|
+
}),
|
|
520
|
+
/*#__PURE__*/ _jsx("input", {
|
|
521
|
+
type: "password",
|
|
522
|
+
"data-bind": "confirmPassword",
|
|
523
|
+
class: "input",
|
|
524
|
+
required: true,
|
|
525
|
+
minLength: 8,
|
|
526
|
+
autocomplete: "new-password"
|
|
527
|
+
})
|
|
528
|
+
]
|
|
529
|
+
}),
|
|
530
|
+
/*#__PURE__*/ _jsx("button", {
|
|
531
|
+
type: "submit",
|
|
532
|
+
class: "btn",
|
|
533
|
+
children: $__i18n._({
|
|
534
|
+
id: "KbS2K9",
|
|
535
|
+
message: "Reset Password"
|
|
536
|
+
})
|
|
537
|
+
})
|
|
538
|
+
]
|
|
539
|
+
})
|
|
540
|
+
})
|
|
541
|
+
]
|
|
542
|
+
})
|
|
543
|
+
});
|
|
544
|
+
};
|
|
545
|
+
const ResetErrorContent = ()=>{
|
|
546
|
+
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
547
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
548
|
+
class: "min-h-screen flex items-center justify-center",
|
|
549
|
+
children: /*#__PURE__*/ _jsxs("div", {
|
|
550
|
+
class: "card max-w-md w-full",
|
|
551
|
+
children: [
|
|
552
|
+
/*#__PURE__*/ _jsx("header", {
|
|
553
|
+
children: /*#__PURE__*/ _jsx("h2", {
|
|
554
|
+
children: $__i18n._({
|
|
555
|
+
id: "7aECQB",
|
|
556
|
+
message: "Invalid or Expired Link"
|
|
557
|
+
})
|
|
558
|
+
})
|
|
559
|
+
}),
|
|
560
|
+
/*#__PURE__*/ _jsx("section", {
|
|
561
|
+
children: /*#__PURE__*/ _jsx("p", {
|
|
562
|
+
class: "text-muted-foreground",
|
|
563
|
+
children: $__i18n._({
|
|
564
|
+
id: "GbVAnd",
|
|
565
|
+
message: "This password reset link is invalid or has expired. Please generate a new one."
|
|
566
|
+
})
|
|
567
|
+
})
|
|
568
|
+
})
|
|
569
|
+
]
|
|
570
|
+
})
|
|
571
|
+
});
|
|
572
|
+
};
|
|
573
|
+
app.get("/reset", async (c)=>{
|
|
574
|
+
const token = c.req.query("token");
|
|
575
|
+
if (!token) {
|
|
576
|
+
return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
|
|
577
|
+
title: "Reset Password - Jant",
|
|
578
|
+
c: c,
|
|
579
|
+
children: /*#__PURE__*/ _jsx(ResetErrorContent, {})
|
|
580
|
+
}));
|
|
581
|
+
}
|
|
582
|
+
const stored = await c.var.services.settings.get(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
|
|
583
|
+
if (!stored) {
|
|
584
|
+
return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
|
|
585
|
+
title: "Reset Password - Jant",
|
|
586
|
+
c: c,
|
|
587
|
+
children: /*#__PURE__*/ _jsx(ResetErrorContent, {})
|
|
588
|
+
}));
|
|
589
|
+
}
|
|
590
|
+
const separatorIndex = stored.lastIndexOf(":");
|
|
591
|
+
const storedToken = stored.substring(0, separatorIndex);
|
|
592
|
+
const expiry = parseInt(stored.substring(separatorIndex + 1), 10);
|
|
593
|
+
const now = Math.floor(Date.now() / 1000);
|
|
594
|
+
if (token !== storedToken || now > expiry) {
|
|
595
|
+
return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
|
|
596
|
+
title: "Reset Password - Jant",
|
|
597
|
+
c: c,
|
|
598
|
+
children: /*#__PURE__*/ _jsx(ResetErrorContent, {})
|
|
599
|
+
}));
|
|
600
|
+
}
|
|
601
|
+
return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
|
|
602
|
+
title: "Reset Password - Jant",
|
|
603
|
+
c: c,
|
|
604
|
+
children: /*#__PURE__*/ _jsx(ResetContent, {
|
|
605
|
+
token: token
|
|
606
|
+
})
|
|
607
|
+
}));
|
|
608
|
+
});
|
|
609
|
+
app.post("/reset", async (c)=>{
|
|
610
|
+
const body = await c.req.json();
|
|
611
|
+
const { password, confirmPassword, token } = body;
|
|
612
|
+
// Validate token
|
|
613
|
+
const stored = await c.var.services.settings.get(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
|
|
614
|
+
if (!stored) {
|
|
615
|
+
return sse(c, async (stream)=>{
|
|
616
|
+
await stream.toast("Invalid or expired reset link.", "error");
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
const separatorIndex = stored.lastIndexOf(":");
|
|
620
|
+
const storedToken = stored.substring(0, separatorIndex);
|
|
621
|
+
const expiry = parseInt(stored.substring(separatorIndex + 1), 10);
|
|
622
|
+
const now = Math.floor(Date.now() / 1000);
|
|
623
|
+
if (token !== storedToken || now > expiry) {
|
|
624
|
+
return sse(c, async (stream)=>{
|
|
625
|
+
await stream.toast("Invalid or expired reset link.", "error");
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
// Validate passwords
|
|
629
|
+
if (!password || password.length < 8) {
|
|
630
|
+
return sse(c, async (stream)=>{
|
|
631
|
+
await stream.toast("Password must be at least 8 characters.", "error");
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
if (password !== confirmPassword) {
|
|
635
|
+
return sse(c, async (stream)=>{
|
|
636
|
+
await stream.toast("Passwords do not match.", "error");
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
try {
|
|
640
|
+
const hashedPassword = await hashPassword(password);
|
|
641
|
+
const db = c.env.DB.withSession();
|
|
642
|
+
// Get admin user
|
|
643
|
+
const userResult = await db.prepare("SELECT id FROM user LIMIT 1").first();
|
|
644
|
+
if (!userResult) {
|
|
645
|
+
return sse(c, async (stream)=>{
|
|
646
|
+
await stream.toast("No user account found.", "error");
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
// Update password
|
|
650
|
+
await db.prepare("UPDATE account SET password = ? WHERE user_id = ? AND provider_id = 'credential'").bind(hashedPassword, userResult.id).run();
|
|
651
|
+
// Delete all sessions
|
|
652
|
+
await db.prepare("DELETE FROM session WHERE user_id = ?").bind(userResult.id).run();
|
|
653
|
+
// Delete the reset token
|
|
654
|
+
await c.var.services.settings.remove(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
|
|
655
|
+
return sse(c, async (stream)=>{
|
|
656
|
+
await stream.redirect("/signin?reset");
|
|
657
|
+
});
|
|
658
|
+
} catch (err) {
|
|
659
|
+
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
660
|
+
console.error("Password reset error:", err);
|
|
661
|
+
return sse(c, async (stream)=>{
|
|
662
|
+
await stream.toast("Failed to reset password.", "error");
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
});
|
|
454
666
|
// Dashboard routes (protected)
|
|
455
667
|
app.use("/dash/*", requireAuth());
|
|
456
668
|
app.route("/dash", dashIndexRoutes);
|
|
@@ -460,6 +672,7 @@ import { sse } from "./lib/sse.js";
|
|
|
460
672
|
app.route("/dash/settings", dashSettingsRoutes);
|
|
461
673
|
app.route("/dash/redirects", dashRedirectsRoutes);
|
|
462
674
|
app.route("/dash/collections", dashCollectionsRoutes);
|
|
675
|
+
app.route("/dash/appearance", dashAppearanceRoutes);
|
|
463
676
|
// API routes
|
|
464
677
|
app.route("/api/upload", uploadApiRoutes);
|
|
465
678
|
app.route("/api/search", searchApiRoutes);
|
package/dist/i18n/context.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ type TranslationDescriptor = {
|
|
|
21
21
|
*
|
|
22
22
|
* @example
|
|
23
23
|
* ```tsx
|
|
24
|
-
* import { I18nProvider } from "
|
|
24
|
+
* import { I18nProvider } from "../i18n/index.js";
|
|
25
25
|
*
|
|
26
26
|
* return c.html(
|
|
27
27
|
* <I18nProvider c={c}>
|
|
@@ -41,7 +41,7 @@ export declare const I18nProvider: FC<I18nProviderProps>;
|
|
|
41
41
|
* @example
|
|
42
42
|
* ```tsx
|
|
43
43
|
* import { t } from "@lingui/core/macro";
|
|
44
|
-
* import { useLingui } from "
|
|
44
|
+
* import { useLingui } from "../i18n/index.js";
|
|
45
45
|
*
|
|
46
46
|
* function MyComponent() {
|
|
47
47
|
* const { t: _ } = useLingui(); // Use _ to avoid conflict with macro
|
package/dist/i18n/context.js
CHANGED
|
@@ -22,7 +22,7 @@ export const I18nProvider = ({ c, children })=>{
|
|
|
22
22
|
* @example
|
|
23
23
|
* ```tsx
|
|
24
24
|
* import { t } from "@lingui/core/macro";
|
|
25
|
-
* import { useLingui } from "
|
|
25
|
+
* import { useLingui } from "../i18n/index.js";
|
|
26
26
|
*
|
|
27
27
|
* function MyComponent() {
|
|
28
28
|
* const { t: _ } = useLingui(); // Use _ to avoid conflict with macro
|