@jant/core 0.3.25 → 0.3.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +70 -563
- package/dist/auth.js +3 -0
- package/dist/client.js +1 -0
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/lib/avatar-upload.js +134 -0
- package/dist/lib/config.js +39 -0
- package/dist/lib/constants.js +10 -10
- package/dist/lib/favicon.js +102 -0
- package/dist/lib/image.js +13 -17
- package/dist/lib/media-helpers.js +2 -2
- package/dist/lib/navigation.js +23 -3
- package/dist/lib/render.js +10 -1
- package/dist/lib/schemas.js +31 -0
- package/dist/lib/timezones.js +388 -0
- package/dist/lib/view.js +1 -1
- package/dist/routes/api/posts.js +1 -1
- package/dist/routes/api/upload.js +3 -3
- package/dist/routes/auth/reset.js +221 -0
- package/dist/routes/auth/setup.js +194 -0
- package/dist/routes/auth/signin.js +176 -0
- package/dist/routes/dash/collections.js +23 -415
- package/dist/routes/dash/media.js +12 -392
- package/dist/routes/dash/pages.js +7 -330
- package/dist/routes/dash/redirects.js +18 -12
- package/dist/routes/dash/settings.js +198 -577
- package/dist/routes/feed/rss.js +2 -1
- package/dist/routes/feed/sitemap.js +4 -2
- package/dist/routes/pages/featured.js +5 -1
- package/dist/routes/pages/home.js +26 -1
- package/dist/routes/pages/latest.js +45 -0
- package/dist/services/post.js +30 -50
- package/dist/types/bindings.js +3 -0
- package/dist/types/config.js +147 -0
- package/dist/types/constants.js +27 -0
- package/dist/types/entities.js +3 -0
- package/dist/types/operations.js +3 -0
- package/dist/types/props.js +3 -0
- package/dist/types/views.js +5 -0
- package/dist/types.js +8 -111
- package/dist/ui/color-themes.js +33 -33
- package/dist/ui/compose/ComposeDialog.js +36 -21
- package/dist/ui/dash/PageForm.js +21 -15
- package/dist/ui/dash/PostForm.js +22 -16
- package/dist/ui/dash/collections/CollectionForm.js +152 -0
- package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
- package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
- package/dist/ui/dash/media/MediaListContent.js +166 -0
- package/dist/ui/dash/media/ViewMediaContent.js +212 -0
- package/dist/ui/dash/pages/LinkFormContent.js +130 -0
- package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
- package/dist/ui/dash/settings/AccountContent.js +209 -0
- package/dist/ui/dash/settings/AppearanceContent.js +259 -0
- package/dist/ui/dash/settings/GeneralContent.js +536 -0
- package/dist/ui/dash/settings/SettingsNav.js +41 -0
- package/dist/ui/font-themes.js +36 -0
- package/dist/ui/layouts/BaseLayout.js +24 -2
- package/dist/ui/layouts/SiteLayout.js +47 -19
- package/package.json +1 -1
- package/src/app.tsx +95 -553
- package/src/auth.ts +4 -1
- package/src/client.ts +1 -0
- package/src/i18n/locales/en.po +240 -175
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +240 -175
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +240 -175
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/lib/__tests__/config.test.ts +192 -0
- package/src/lib/__tests__/favicon.test.ts +151 -0
- package/src/lib/__tests__/image.test.ts +2 -6
- package/src/lib/__tests__/timezones.test.ts +61 -0
- package/src/lib/__tests__/view.test.ts +2 -2
- package/src/lib/avatar-upload.ts +165 -0
- package/src/lib/config.ts +47 -0
- package/src/lib/constants.ts +19 -11
- package/src/lib/favicon.ts +115 -0
- package/src/lib/image.ts +13 -21
- package/src/lib/media-helpers.ts +2 -2
- package/src/lib/navigation.ts +33 -2
- package/src/lib/render.tsx +15 -1
- package/src/lib/schemas.ts +39 -0
- package/src/lib/timezones.ts +325 -0
- package/src/lib/view.ts +1 -1
- package/src/routes/api/posts.ts +1 -1
- package/src/routes/api/upload.ts +2 -3
- package/src/routes/auth/reset.tsx +239 -0
- package/src/routes/auth/setup.tsx +189 -0
- package/src/routes/auth/signin.tsx +163 -0
- package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
- package/src/routes/dash/collections.tsx +17 -366
- package/src/routes/dash/media.tsx +12 -414
- package/src/routes/dash/pages.tsx +8 -348
- package/src/routes/dash/redirects.tsx +20 -14
- package/src/routes/dash/settings.tsx +243 -534
- package/src/routes/feed/__tests__/rss.test.ts +141 -0
- package/src/routes/feed/rss.ts +3 -1
- package/src/routes/feed/sitemap.ts +4 -2
- package/src/routes/pages/featured.tsx +7 -1
- package/src/routes/pages/home.tsx +25 -2
- package/src/routes/pages/latest.tsx +59 -0
- package/src/services/post.ts +34 -66
- package/src/styles/components.css +0 -65
- package/src/styles/tokens.css +1 -1
- package/src/styles/ui.css +24 -40
- package/src/types/bindings.ts +30 -0
- package/src/types/config.ts +183 -0
- package/src/types/constants.ts +26 -0
- package/src/types/entities.ts +109 -0
- package/src/types/operations.ts +88 -0
- package/src/types/props.ts +115 -0
- package/src/types/views.ts +172 -0
- package/src/types.ts +8 -644
- package/src/ui/__tests__/font-themes.test.ts +34 -0
- package/src/ui/color-themes.ts +34 -34
- package/src/ui/compose/ComposeDialog.tsx +40 -21
- package/src/ui/dash/PageForm.tsx +25 -19
- package/src/ui/dash/PostForm.tsx +26 -20
- package/src/ui/dash/collections/CollectionForm.tsx +153 -0
- package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
- package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
- package/src/ui/dash/media/MediaListContent.tsx +201 -0
- package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
- package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
- package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
- package/src/ui/dash/settings/AccountContent.tsx +176 -0
- package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
- package/src/ui/dash/settings/GeneralContent.tsx +533 -0
- package/src/ui/dash/settings/SettingsNav.tsx +56 -0
- package/src/ui/font-themes.ts +54 -0
- package/src/ui/layouts/BaseLayout.tsx +17 -0
- package/src/ui/layouts/SiteLayout.tsx +45 -31
package/dist/app.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
2
1
|
/**
|
|
3
2
|
* Jant App Factory
|
|
4
3
|
*/ import { Hono } from "hono";
|
|
@@ -6,9 +5,11 @@ import { createDatabase } from "./db/index.js";
|
|
|
6
5
|
import { createServices } from "./services/index.js";
|
|
7
6
|
import { createAuth } from "./auth.js";
|
|
8
7
|
import { i18nMiddleware } from "./i18n/index.js";
|
|
9
|
-
import { useLingui as $_useLingui } from "@jant/core/i18n";
|
|
10
8
|
import { SETTINGS_KEYS } from "./lib/constants.js";
|
|
11
|
-
|
|
9
|
+
// Routes - Auth
|
|
10
|
+
import { setupRoutes } from "./routes/auth/setup.js";
|
|
11
|
+
import { signinRoutes } from "./routes/auth/signin.js";
|
|
12
|
+
import { resetRoutes } from "./routes/auth/reset.js";
|
|
12
13
|
// Routes - Pages
|
|
13
14
|
import { homeRoutes } from "./routes/pages/home.js";
|
|
14
15
|
import { postRoutes } from "./routes/pages/post.js";
|
|
@@ -17,6 +18,7 @@ import { collectionRoutes } from "./routes/pages/collection.js";
|
|
|
17
18
|
import { archiveRoutes } from "./routes/pages/archive.js";
|
|
18
19
|
import { searchRoutes } from "./routes/pages/search.js";
|
|
19
20
|
import { featuredRoutes } from "./routes/pages/featured.js";
|
|
21
|
+
import { latestRoutes } from "./routes/pages/latest.js";
|
|
20
22
|
import { collectionsPageRoutes } from "./routes/pages/collections.js";
|
|
21
23
|
// Routes - Dashboard
|
|
22
24
|
import { dashIndexRoutes } from "./routes/dash/index.js";
|
|
@@ -42,11 +44,11 @@ import { sitemapRoutes } from "./routes/feed/sitemap.js";
|
|
|
42
44
|
// Middleware
|
|
43
45
|
import { requireAuth } from "./middleware/auth.js";
|
|
44
46
|
import { requireOnboarding } from "./middleware/onboarding.js";
|
|
45
|
-
// Layouts for auth pages
|
|
46
|
-
import { BaseLayout } from "./ui/layouts/BaseLayout.js";
|
|
47
|
-
import { dsRedirect, dsToast } from "./lib/sse.js";
|
|
48
47
|
import { getAvailableThemes, buildThemeStyle } from "./lib/theme.js";
|
|
49
48
|
import { createStorageDriver } from "./lib/storage.js";
|
|
49
|
+
import { BUILTIN_FONT_THEMES } from "./ui/font-themes.js";
|
|
50
|
+
import { getMediaUrl, getPublicUrlForProvider } from "./lib/image.js";
|
|
51
|
+
import { base64ToUint8Array } from "./lib/favicon.js";
|
|
50
52
|
/**
|
|
51
53
|
* Create a Jant application
|
|
52
54
|
*
|
|
@@ -73,8 +75,6 @@ import { createStorageDriver } from "./lib/storage.js";
|
|
|
73
75
|
// Initialize services, auth, and config middleware
|
|
74
76
|
app.use("*", async (c, next)=>{
|
|
75
77
|
// Use withSession() to enable D1 Read Replication
|
|
76
|
-
// Automatically routes read queries to the nearest replica for lower latency
|
|
77
|
-
// See: https://developers.cloudflare.com/d1/best-practices/read-replication/
|
|
78
78
|
const session = c.env.DB.withSession();
|
|
79
79
|
// Note: Drizzle ORM doesn't officially support D1DatabaseSession yet (issue #2226)
|
|
80
80
|
// but it works at runtime. We use type assertion as a temporary workaround.
|
|
@@ -83,11 +83,17 @@ import { createStorageDriver } from "./lib/storage.js";
|
|
|
83
83
|
c.set("services", services);
|
|
84
84
|
c.set("config", resolvedConfig);
|
|
85
85
|
c.set("storage", createStorageDriver(c.env));
|
|
86
|
+
if (!c.env.AUTH_SECRET) {
|
|
87
|
+
// eslint-disable-next-line no-console -- Startup warning is intentional
|
|
88
|
+
console.warn("[Jant] AUTH_SECRET is not set. Authentication is disabled. Set AUTH_SECRET in .dev.vars or wrangler.toml to enable auth.");
|
|
89
|
+
}
|
|
86
90
|
if (c.env.AUTH_SECRET) {
|
|
87
91
|
const baseURL = c.env.SITE_URL || new URL(c.req.url).origin;
|
|
92
|
+
const requestUrl = new URL(c.req.url);
|
|
88
93
|
const auth = createAuth(session, {
|
|
89
94
|
secret: c.env.AUTH_SECRET,
|
|
90
|
-
baseURL
|
|
95
|
+
baseURL,
|
|
96
|
+
useSecureCookies: requestUrl.protocol === "https:"
|
|
91
97
|
});
|
|
92
98
|
c.set("auth", auth);
|
|
93
99
|
}
|
|
@@ -95,17 +101,36 @@ import { createStorageDriver } from "./lib/storage.js";
|
|
|
95
101
|
});
|
|
96
102
|
// Onboarding gate — redirect to /setup if not yet initialized
|
|
97
103
|
app.use("*", requireOnboarding());
|
|
98
|
-
// Theme middleware - resolve active color theme, custom CSS, and auth state
|
|
104
|
+
// Theme middleware - resolve active color theme, font theme, custom CSS, and auth state
|
|
99
105
|
app.use("*", async (c, next)=>{
|
|
100
|
-
const [themeId, customCSS] = await Promise.all([
|
|
106
|
+
const [themeId, fontThemeId, customCSS, noindexValue, avatarKey] = await Promise.all([
|
|
101
107
|
c.var.services.settings.get(SETTINGS_KEYS.THEME),
|
|
102
|
-
c.var.services.settings.get(
|
|
108
|
+
c.var.services.settings.get("FONT_THEME"),
|
|
109
|
+
c.var.services.settings.get(SETTINGS_KEYS.CUSTOM_CSS),
|
|
110
|
+
c.var.services.settings.get("NOINDEX"),
|
|
111
|
+
c.var.services.settings.get("SITE_AVATAR")
|
|
103
112
|
]);
|
|
104
113
|
const themes = getAvailableThemes(resolvedConfig);
|
|
105
114
|
const activeTheme = themeId ? themes.find((t)=>t.id === themeId) : undefined;
|
|
106
|
-
|
|
115
|
+
// Build font override CSS variables
|
|
116
|
+
const fontTheme = fontThemeId ? BUILTIN_FONT_THEMES.find((f)=>f.id === fontThemeId) : undefined;
|
|
117
|
+
const fontOverrides = {};
|
|
118
|
+
if (fontTheme) {
|
|
119
|
+
fontOverrides["--font-body"] = fontTheme.fontFamily;
|
|
120
|
+
}
|
|
121
|
+
const themeStyle = buildThemeStyle(activeTheme, {
|
|
122
|
+
...resolvedConfig.cssVariables,
|
|
123
|
+
...fontOverrides
|
|
124
|
+
});
|
|
107
125
|
c.set("themeStyle", themeStyle);
|
|
108
126
|
c.set("customCSS", customCSS ?? "");
|
|
127
|
+
// Noindex
|
|
128
|
+
c.set("noindex", noindexValue === "true");
|
|
129
|
+
// Resolve favicon from avatar storage key
|
|
130
|
+
if (avatarKey) {
|
|
131
|
+
const publicUrl = getPublicUrlForProvider(c.env.STORAGE_DRIVER || "r2", c.env.R2_PUBLIC_URL, c.env.S3_PUBLIC_URL);
|
|
132
|
+
c.set("faviconUrl", getMediaUrl(avatarKey, publicUrl));
|
|
133
|
+
}
|
|
109
134
|
// Check auth state for data-authenticated attribute on <body>
|
|
110
135
|
let isAuthenticated = false;
|
|
111
136
|
if (c.var.auth) {
|
|
@@ -151,6 +176,27 @@ import { createStorageDriver } from "./lib/storage.js";
|
|
|
151
176
|
auth: c.env.AUTH_SECRET ? "configured" : "missing",
|
|
152
177
|
authSecretLength: c.env.AUTH_SECRET?.length ?? 0
|
|
153
178
|
}));
|
|
179
|
+
// Favicon routes - serve from DB settings (small files, avoids R2 round-trip)
|
|
180
|
+
app.get("/favicon.ico", async (c)=>{
|
|
181
|
+
const data = await c.var.services.settings.get("SITE_FAVICON_ICO");
|
|
182
|
+
if (!data) return c.notFound();
|
|
183
|
+
return new Response(base64ToUint8Array(data), {
|
|
184
|
+
headers: {
|
|
185
|
+
"Content-Type": "image/x-icon",
|
|
186
|
+
"Cache-Control": "public, max-age=86400"
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
app.get("/apple-touch-icon.png", async (c)=>{
|
|
191
|
+
const data = await c.var.services.settings.get("SITE_FAVICON_APPLE_TOUCH");
|
|
192
|
+
if (!data) return c.notFound();
|
|
193
|
+
return new Response(base64ToUint8Array(data), {
|
|
194
|
+
headers: {
|
|
195
|
+
"Content-Type": "image/png",
|
|
196
|
+
"Cache-Control": "public, max-age=86400"
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
});
|
|
154
200
|
// better-auth handler
|
|
155
201
|
app.all("/api/auth/*", async (c)=>{
|
|
156
202
|
if (!c.var.auth) {
|
|
@@ -166,545 +212,10 @@ import { createStorageDriver } from "./lib/storage.js";
|
|
|
166
212
|
app.route("/api/nav-items", navItemsApiRoutes);
|
|
167
213
|
app.route("/api/collections", collectionsApiRoutes);
|
|
168
214
|
app.route("/api/settings", settingsApiRoutes);
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
class: "min-h-screen flex items-center justify-center",
|
|
174
|
-
children: /*#__PURE__*/ _jsxs("div", {
|
|
175
|
-
class: "card max-w-md w-full",
|
|
176
|
-
children: [
|
|
177
|
-
/*#__PURE__*/ _jsxs("header", {
|
|
178
|
-
children: [
|
|
179
|
-
/*#__PURE__*/ _jsx("h2", {
|
|
180
|
-
children: $__i18n._({
|
|
181
|
-
id: "GorKul",
|
|
182
|
-
message: "Welcome to Jant"
|
|
183
|
-
})
|
|
184
|
-
}),
|
|
185
|
-
/*#__PURE__*/ _jsx("p", {
|
|
186
|
-
children: $__i18n._({
|
|
187
|
-
id: "GX2VMa",
|
|
188
|
-
message: "Create your admin account."
|
|
189
|
-
})
|
|
190
|
-
})
|
|
191
|
-
]
|
|
192
|
-
}),
|
|
193
|
-
/*#__PURE__*/ _jsx("section", {
|
|
194
|
-
children: /*#__PURE__*/ _jsxs("form", {
|
|
195
|
-
"data-signals": "{name: '', email: '', password: ''}",
|
|
196
|
-
"data-on:submit__prevent": "@post('/setup')",
|
|
197
|
-
"data-indicator": "_loading",
|
|
198
|
-
class: "flex flex-col gap-4",
|
|
199
|
-
children: [
|
|
200
|
-
/*#__PURE__*/ _jsxs("div", {
|
|
201
|
-
class: "field",
|
|
202
|
-
children: [
|
|
203
|
-
/*#__PURE__*/ _jsx("label", {
|
|
204
|
-
class: "label",
|
|
205
|
-
children: $__i18n._({
|
|
206
|
-
id: "/Rj5P4",
|
|
207
|
-
message: "Your Name"
|
|
208
|
-
})
|
|
209
|
-
}),
|
|
210
|
-
/*#__PURE__*/ _jsx("input", {
|
|
211
|
-
type: "text",
|
|
212
|
-
"data-bind": "name",
|
|
213
|
-
class: "input",
|
|
214
|
-
required: true,
|
|
215
|
-
placeholder: "John Doe"
|
|
216
|
-
})
|
|
217
|
-
]
|
|
218
|
-
}),
|
|
219
|
-
/*#__PURE__*/ _jsxs("div", {
|
|
220
|
-
class: "field",
|
|
221
|
-
children: [
|
|
222
|
-
/*#__PURE__*/ _jsx("label", {
|
|
223
|
-
class: "label",
|
|
224
|
-
children: $__i18n._({
|
|
225
|
-
id: "O3oNi5",
|
|
226
|
-
message: "Email"
|
|
227
|
-
})
|
|
228
|
-
}),
|
|
229
|
-
/*#__PURE__*/ _jsx("input", {
|
|
230
|
-
type: "email",
|
|
231
|
-
"data-bind": "email",
|
|
232
|
-
class: "input",
|
|
233
|
-
required: true,
|
|
234
|
-
placeholder: "you@example.com"
|
|
235
|
-
})
|
|
236
|
-
]
|
|
237
|
-
}),
|
|
238
|
-
/*#__PURE__*/ _jsxs("div", {
|
|
239
|
-
class: "field",
|
|
240
|
-
children: [
|
|
241
|
-
/*#__PURE__*/ _jsx("label", {
|
|
242
|
-
class: "label",
|
|
243
|
-
children: $__i18n._({
|
|
244
|
-
id: "8ZsakT",
|
|
245
|
-
message: "Password"
|
|
246
|
-
})
|
|
247
|
-
}),
|
|
248
|
-
/*#__PURE__*/ _jsx("input", {
|
|
249
|
-
type: "password",
|
|
250
|
-
"data-bind": "password",
|
|
251
|
-
class: "input",
|
|
252
|
-
required: true,
|
|
253
|
-
minLength: 8
|
|
254
|
-
})
|
|
255
|
-
]
|
|
256
|
-
}),
|
|
257
|
-
/*#__PURE__*/ _jsxs("button", {
|
|
258
|
-
type: "submit",
|
|
259
|
-
class: "btn",
|
|
260
|
-
"data-attr-disabled": "$_loading",
|
|
261
|
-
children: [
|
|
262
|
-
/*#__PURE__*/ _jsx("span", {
|
|
263
|
-
"data-show": "!$_loading",
|
|
264
|
-
children: $__i18n._({
|
|
265
|
-
id: "EGwzOK",
|
|
266
|
-
message: "Complete Setup"
|
|
267
|
-
})
|
|
268
|
-
}),
|
|
269
|
-
/*#__PURE__*/ _jsx("span", {
|
|
270
|
-
"data-show": "$_loading",
|
|
271
|
-
children: $__i18n._({
|
|
272
|
-
id: "k1ifdL",
|
|
273
|
-
message: "Processing..."
|
|
274
|
-
})
|
|
275
|
-
})
|
|
276
|
-
]
|
|
277
|
-
})
|
|
278
|
-
]
|
|
279
|
-
})
|
|
280
|
-
})
|
|
281
|
-
]
|
|
282
|
-
})
|
|
283
|
-
});
|
|
284
|
-
};
|
|
285
|
-
// Setup page
|
|
286
|
-
app.get("/setup", async (c)=>{
|
|
287
|
-
const isComplete = await c.var.services.settings.isOnboardingComplete();
|
|
288
|
-
if (isComplete) return c.redirect("/");
|
|
289
|
-
return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
|
|
290
|
-
title: "Setup - Jant",
|
|
291
|
-
c: c,
|
|
292
|
-
children: /*#__PURE__*/ _jsx(SetupContent, {})
|
|
293
|
-
}));
|
|
294
|
-
});
|
|
295
|
-
app.post("/setup", async (c)=>{
|
|
296
|
-
const isComplete = await c.var.services.settings.isOnboardingComplete();
|
|
297
|
-
if (isComplete) return c.redirect("/");
|
|
298
|
-
const body = await c.req.json();
|
|
299
|
-
const { name, email, password } = body;
|
|
300
|
-
if (!name || !email || !password) {
|
|
301
|
-
return dsToast("All fields are required", "error");
|
|
302
|
-
}
|
|
303
|
-
if (password.length < 8) {
|
|
304
|
-
return dsToast("Password must be at least 8 characters", "error");
|
|
305
|
-
}
|
|
306
|
-
if (!c.var.auth) {
|
|
307
|
-
return dsToast("AUTH_SECRET not configured", "error");
|
|
308
|
-
}
|
|
309
|
-
try {
|
|
310
|
-
const signUpResponse = await c.var.auth.api.signUpEmail({
|
|
311
|
-
body: {
|
|
312
|
-
name,
|
|
313
|
-
email,
|
|
314
|
-
password
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
if (!signUpResponse || "error" in signUpResponse) {
|
|
318
|
-
return dsToast("Failed to create account", "error");
|
|
319
|
-
}
|
|
320
|
-
await c.var.services.settings.completeOnboarding();
|
|
321
|
-
// Seed default navigation items
|
|
322
|
-
await c.var.services.navItems.create({
|
|
323
|
-
type: "link",
|
|
324
|
-
label: "Featured",
|
|
325
|
-
url: "/featured"
|
|
326
|
-
});
|
|
327
|
-
await c.var.services.navItems.create({
|
|
328
|
-
type: "link",
|
|
329
|
-
label: "Collections",
|
|
330
|
-
url: "/collections"
|
|
331
|
-
});
|
|
332
|
-
return dsRedirect("/signin?setup");
|
|
333
|
-
} catch (err) {
|
|
334
|
-
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
335
|
-
console.error("Setup error:", err);
|
|
336
|
-
return dsToast("Failed to create account", "error");
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
|
-
// Signin page component
|
|
340
|
-
const SigninContent = ({ demoEmail, demoPassword })=>{
|
|
341
|
-
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
342
|
-
const signals = JSON.stringify({
|
|
343
|
-
email: demoEmail || "",
|
|
344
|
-
password: demoPassword || ""
|
|
345
|
-
}).replace(/</g, "\\u003c");
|
|
346
|
-
return /*#__PURE__*/ _jsx("div", {
|
|
347
|
-
class: "min-h-screen flex items-center justify-center",
|
|
348
|
-
children: /*#__PURE__*/ _jsxs("div", {
|
|
349
|
-
class: "card max-w-md w-full",
|
|
350
|
-
children: [
|
|
351
|
-
/*#__PURE__*/ _jsx("header", {
|
|
352
|
-
children: /*#__PURE__*/ _jsx("h2", {
|
|
353
|
-
children: $__i18n._({
|
|
354
|
-
id: "n1ekoW",
|
|
355
|
-
message: "Sign In"
|
|
356
|
-
})
|
|
357
|
-
})
|
|
358
|
-
}),
|
|
359
|
-
/*#__PURE__*/ _jsxs("section", {
|
|
360
|
-
children: [
|
|
361
|
-
demoEmail && demoPassword && /*#__PURE__*/ _jsx("p", {
|
|
362
|
-
class: "text-muted-foreground text-sm mb-4",
|
|
363
|
-
children: $__i18n._({
|
|
364
|
-
id: "er8+x7",
|
|
365
|
-
message: "Demo account pre-filled. Just click Sign In."
|
|
366
|
-
})
|
|
367
|
-
}),
|
|
368
|
-
/*#__PURE__*/ _jsxs("form", {
|
|
369
|
-
"data-signals": signals,
|
|
370
|
-
"data-on:submit__prevent": "@post('/signin')",
|
|
371
|
-
"data-indicator": "_loading",
|
|
372
|
-
class: "flex flex-col gap-4",
|
|
373
|
-
children: [
|
|
374
|
-
/*#__PURE__*/ _jsxs("div", {
|
|
375
|
-
class: "field",
|
|
376
|
-
children: [
|
|
377
|
-
/*#__PURE__*/ _jsx("label", {
|
|
378
|
-
class: "label",
|
|
379
|
-
children: $__i18n._({
|
|
380
|
-
id: "O3oNi5",
|
|
381
|
-
message: "Email"
|
|
382
|
-
})
|
|
383
|
-
}),
|
|
384
|
-
/*#__PURE__*/ _jsx("input", {
|
|
385
|
-
type: "email",
|
|
386
|
-
"data-bind": "email",
|
|
387
|
-
class: "input",
|
|
388
|
-
required: true
|
|
389
|
-
})
|
|
390
|
-
]
|
|
391
|
-
}),
|
|
392
|
-
/*#__PURE__*/ _jsxs("div", {
|
|
393
|
-
class: "field",
|
|
394
|
-
children: [
|
|
395
|
-
/*#__PURE__*/ _jsx("label", {
|
|
396
|
-
class: "label",
|
|
397
|
-
children: $__i18n._({
|
|
398
|
-
id: "8ZsakT",
|
|
399
|
-
message: "Password"
|
|
400
|
-
})
|
|
401
|
-
}),
|
|
402
|
-
/*#__PURE__*/ _jsx("input", {
|
|
403
|
-
type: "password",
|
|
404
|
-
"data-bind": "password",
|
|
405
|
-
class: "input",
|
|
406
|
-
required: true
|
|
407
|
-
})
|
|
408
|
-
]
|
|
409
|
-
}),
|
|
410
|
-
/*#__PURE__*/ _jsxs("button", {
|
|
411
|
-
type: "submit",
|
|
412
|
-
class: "btn",
|
|
413
|
-
"data-attr-disabled": "$_loading",
|
|
414
|
-
children: [
|
|
415
|
-
/*#__PURE__*/ _jsx("span", {
|
|
416
|
-
"data-show": "!$_loading",
|
|
417
|
-
children: $__i18n._({
|
|
418
|
-
id: "n1ekoW",
|
|
419
|
-
message: "Sign In"
|
|
420
|
-
})
|
|
421
|
-
}),
|
|
422
|
-
/*#__PURE__*/ _jsx("span", {
|
|
423
|
-
"data-show": "$_loading",
|
|
424
|
-
children: $__i18n._({
|
|
425
|
-
id: "k1ifdL",
|
|
426
|
-
message: "Processing..."
|
|
427
|
-
})
|
|
428
|
-
})
|
|
429
|
-
]
|
|
430
|
-
})
|
|
431
|
-
]
|
|
432
|
-
})
|
|
433
|
-
]
|
|
434
|
-
})
|
|
435
|
-
]
|
|
436
|
-
})
|
|
437
|
-
});
|
|
438
|
-
};
|
|
439
|
-
// Signin page
|
|
440
|
-
app.get("/signin", async (c)=>{
|
|
441
|
-
const isSetup = c.req.query("setup") !== undefined;
|
|
442
|
-
const isReset = c.req.query("reset") !== undefined;
|
|
443
|
-
let toast;
|
|
444
|
-
if (isSetup) {
|
|
445
|
-
toast = {
|
|
446
|
-
message: "Account created successfully. Please sign in."
|
|
447
|
-
};
|
|
448
|
-
} else if (isReset) {
|
|
449
|
-
toast = {
|
|
450
|
-
message: "Password reset successfully. Please sign in."
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
|
|
454
|
-
title: "Sign In - Jant",
|
|
455
|
-
c: c,
|
|
456
|
-
toast: toast,
|
|
457
|
-
children: /*#__PURE__*/ _jsx(SigninContent, {
|
|
458
|
-
demoEmail: c.env.DEMO_EMAIL,
|
|
459
|
-
demoPassword: c.env.DEMO_PASSWORD
|
|
460
|
-
})
|
|
461
|
-
}));
|
|
462
|
-
});
|
|
463
|
-
app.post("/signin", async (c)=>{
|
|
464
|
-
if (!c.var.auth) {
|
|
465
|
-
return dsToast("Auth not configured", "error");
|
|
466
|
-
}
|
|
467
|
-
const body = await c.req.json();
|
|
468
|
-
const { email, password } = body;
|
|
469
|
-
try {
|
|
470
|
-
const { headers } = await c.var.auth.api.signInEmail({
|
|
471
|
-
returnHeaders: true,
|
|
472
|
-
body: {
|
|
473
|
-
email,
|
|
474
|
-
password
|
|
475
|
-
},
|
|
476
|
-
headers: c.req.raw.headers
|
|
477
|
-
});
|
|
478
|
-
return dsRedirect("/dash", {
|
|
479
|
-
headers
|
|
480
|
-
});
|
|
481
|
-
} catch {
|
|
482
|
-
return dsToast("Invalid email or password", "error");
|
|
483
|
-
}
|
|
484
|
-
});
|
|
485
|
-
app.get("/signout", async (c)=>{
|
|
486
|
-
if (c.var.auth) {
|
|
487
|
-
try {
|
|
488
|
-
await c.var.auth.api.signOut({
|
|
489
|
-
headers: c.req.raw.headers
|
|
490
|
-
});
|
|
491
|
-
} catch {
|
|
492
|
-
// Ignore signout errors
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
return c.redirect("/");
|
|
496
|
-
});
|
|
497
|
-
// Password reset via one-time token
|
|
498
|
-
const ResetContent = ({ token })=>{
|
|
499
|
-
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
500
|
-
const signals = JSON.stringify({
|
|
501
|
-
password: "",
|
|
502
|
-
confirmPassword: "",
|
|
503
|
-
token
|
|
504
|
-
}).replace(/</g, "\\u003c");
|
|
505
|
-
return /*#__PURE__*/ _jsx("div", {
|
|
506
|
-
class: "min-h-screen flex items-center justify-center",
|
|
507
|
-
children: /*#__PURE__*/ _jsxs("div", {
|
|
508
|
-
class: "card max-w-md w-full",
|
|
509
|
-
children: [
|
|
510
|
-
/*#__PURE__*/ _jsxs("header", {
|
|
511
|
-
children: [
|
|
512
|
-
/*#__PURE__*/ _jsx("h2", {
|
|
513
|
-
children: $__i18n._({
|
|
514
|
-
id: "KbS2K9",
|
|
515
|
-
message: "Reset Password"
|
|
516
|
-
})
|
|
517
|
-
}),
|
|
518
|
-
/*#__PURE__*/ _jsx("p", {
|
|
519
|
-
children: $__i18n._({
|
|
520
|
-
id: "hWOZIv",
|
|
521
|
-
message: "Enter your new password."
|
|
522
|
-
})
|
|
523
|
-
})
|
|
524
|
-
]
|
|
525
|
-
}),
|
|
526
|
-
/*#__PURE__*/ _jsx("section", {
|
|
527
|
-
children: /*#__PURE__*/ _jsxs("form", {
|
|
528
|
-
"data-signals": signals,
|
|
529
|
-
"data-on:submit__prevent": "@post('/reset')",
|
|
530
|
-
"data-indicator": "_loading",
|
|
531
|
-
class: "flex flex-col gap-4",
|
|
532
|
-
children: [
|
|
533
|
-
/*#__PURE__*/ _jsxs("div", {
|
|
534
|
-
class: "field",
|
|
535
|
-
children: [
|
|
536
|
-
/*#__PURE__*/ _jsx("label", {
|
|
537
|
-
class: "label",
|
|
538
|
-
children: $__i18n._({
|
|
539
|
-
id: "7vhWI8",
|
|
540
|
-
message: "New Password"
|
|
541
|
-
})
|
|
542
|
-
}),
|
|
543
|
-
/*#__PURE__*/ _jsx("input", {
|
|
544
|
-
type: "password",
|
|
545
|
-
"data-bind": "password",
|
|
546
|
-
class: "input",
|
|
547
|
-
required: true,
|
|
548
|
-
minLength: 8,
|
|
549
|
-
autocomplete: "new-password"
|
|
550
|
-
})
|
|
551
|
-
]
|
|
552
|
-
}),
|
|
553
|
-
/*#__PURE__*/ _jsxs("div", {
|
|
554
|
-
class: "field",
|
|
555
|
-
children: [
|
|
556
|
-
/*#__PURE__*/ _jsx("label", {
|
|
557
|
-
class: "label",
|
|
558
|
-
children: $__i18n._({
|
|
559
|
-
id: "p2/GCq",
|
|
560
|
-
message: "Confirm Password"
|
|
561
|
-
})
|
|
562
|
-
}),
|
|
563
|
-
/*#__PURE__*/ _jsx("input", {
|
|
564
|
-
type: "password",
|
|
565
|
-
"data-bind": "confirmPassword",
|
|
566
|
-
class: "input",
|
|
567
|
-
required: true,
|
|
568
|
-
minLength: 8,
|
|
569
|
-
autocomplete: "new-password"
|
|
570
|
-
})
|
|
571
|
-
]
|
|
572
|
-
}),
|
|
573
|
-
/*#__PURE__*/ _jsxs("button", {
|
|
574
|
-
type: "submit",
|
|
575
|
-
class: "btn",
|
|
576
|
-
"data-attr-disabled": "$_loading",
|
|
577
|
-
children: [
|
|
578
|
-
/*#__PURE__*/ _jsx("span", {
|
|
579
|
-
"data-show": "!$_loading",
|
|
580
|
-
children: $__i18n._({
|
|
581
|
-
id: "KbS2K9",
|
|
582
|
-
message: "Reset Password"
|
|
583
|
-
})
|
|
584
|
-
}),
|
|
585
|
-
/*#__PURE__*/ _jsx("span", {
|
|
586
|
-
"data-show": "$_loading",
|
|
587
|
-
children: $__i18n._({
|
|
588
|
-
id: "k1ifdL",
|
|
589
|
-
message: "Processing..."
|
|
590
|
-
})
|
|
591
|
-
})
|
|
592
|
-
]
|
|
593
|
-
})
|
|
594
|
-
]
|
|
595
|
-
})
|
|
596
|
-
})
|
|
597
|
-
]
|
|
598
|
-
})
|
|
599
|
-
});
|
|
600
|
-
};
|
|
601
|
-
const ResetErrorContent = ()=>{
|
|
602
|
-
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
603
|
-
return /*#__PURE__*/ _jsx("div", {
|
|
604
|
-
class: "min-h-screen flex items-center justify-center",
|
|
605
|
-
children: /*#__PURE__*/ _jsxs("div", {
|
|
606
|
-
class: "card max-w-md w-full",
|
|
607
|
-
children: [
|
|
608
|
-
/*#__PURE__*/ _jsx("header", {
|
|
609
|
-
children: /*#__PURE__*/ _jsx("h2", {
|
|
610
|
-
children: $__i18n._({
|
|
611
|
-
id: "7aECQB",
|
|
612
|
-
message: "Invalid or Expired Link"
|
|
613
|
-
})
|
|
614
|
-
})
|
|
615
|
-
}),
|
|
616
|
-
/*#__PURE__*/ _jsx("section", {
|
|
617
|
-
children: /*#__PURE__*/ _jsx("p", {
|
|
618
|
-
class: "text-muted-foreground",
|
|
619
|
-
children: $__i18n._({
|
|
620
|
-
id: "GbVAnd",
|
|
621
|
-
message: "This password reset link is invalid or has expired. Please generate a new one."
|
|
622
|
-
})
|
|
623
|
-
})
|
|
624
|
-
})
|
|
625
|
-
]
|
|
626
|
-
})
|
|
627
|
-
});
|
|
628
|
-
};
|
|
629
|
-
app.get("/reset", async (c)=>{
|
|
630
|
-
const token = c.req.query("token");
|
|
631
|
-
if (!token) {
|
|
632
|
-
return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
|
|
633
|
-
title: "Reset Password - Jant",
|
|
634
|
-
c: c,
|
|
635
|
-
children: /*#__PURE__*/ _jsx(ResetErrorContent, {})
|
|
636
|
-
}));
|
|
637
|
-
}
|
|
638
|
-
const stored = await c.var.services.settings.get(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
|
|
639
|
-
if (!stored) {
|
|
640
|
-
return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
|
|
641
|
-
title: "Reset Password - Jant",
|
|
642
|
-
c: c,
|
|
643
|
-
children: /*#__PURE__*/ _jsx(ResetErrorContent, {})
|
|
644
|
-
}));
|
|
645
|
-
}
|
|
646
|
-
const separatorIndex = stored.lastIndexOf(":");
|
|
647
|
-
const storedToken = stored.substring(0, separatorIndex);
|
|
648
|
-
const expiry = parseInt(stored.substring(separatorIndex + 1), 10);
|
|
649
|
-
const now = Math.floor(Date.now() / 1000);
|
|
650
|
-
if (token !== storedToken || now > expiry) {
|
|
651
|
-
return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
|
|
652
|
-
title: "Reset Password - Jant",
|
|
653
|
-
c: c,
|
|
654
|
-
children: /*#__PURE__*/ _jsx(ResetErrorContent, {})
|
|
655
|
-
}));
|
|
656
|
-
}
|
|
657
|
-
return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
|
|
658
|
-
title: "Reset Password - Jant",
|
|
659
|
-
c: c,
|
|
660
|
-
children: /*#__PURE__*/ _jsx(ResetContent, {
|
|
661
|
-
token: token
|
|
662
|
-
})
|
|
663
|
-
}));
|
|
664
|
-
});
|
|
665
|
-
app.post("/reset", async (c)=>{
|
|
666
|
-
const body = await c.req.json();
|
|
667
|
-
const { password, confirmPassword, token } = body;
|
|
668
|
-
// Validate token
|
|
669
|
-
const stored = await c.var.services.settings.get(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
|
|
670
|
-
if (!stored) {
|
|
671
|
-
return dsToast("Invalid or expired reset link.", "error");
|
|
672
|
-
}
|
|
673
|
-
const separatorIndex = stored.lastIndexOf(":");
|
|
674
|
-
const storedToken = stored.substring(0, separatorIndex);
|
|
675
|
-
const expiry = parseInt(stored.substring(separatorIndex + 1), 10);
|
|
676
|
-
const now = Math.floor(Date.now() / 1000);
|
|
677
|
-
if (token !== storedToken || now > expiry) {
|
|
678
|
-
return dsToast("Invalid or expired reset link.", "error");
|
|
679
|
-
}
|
|
680
|
-
// Validate passwords
|
|
681
|
-
if (!password || password.length < 8) {
|
|
682
|
-
return dsToast("Password must be at least 8 characters.", "error");
|
|
683
|
-
}
|
|
684
|
-
if (password !== confirmPassword) {
|
|
685
|
-
return dsToast("Passwords do not match.", "error");
|
|
686
|
-
}
|
|
687
|
-
try {
|
|
688
|
-
const hashedPassword = await hashPassword(password);
|
|
689
|
-
const db = c.env.DB.withSession();
|
|
690
|
-
// Get admin user
|
|
691
|
-
const userResult = await db.prepare("SELECT id FROM user LIMIT 1").first();
|
|
692
|
-
if (!userResult) {
|
|
693
|
-
return dsToast("No user account found.", "error");
|
|
694
|
-
}
|
|
695
|
-
// Update password
|
|
696
|
-
await db.prepare("UPDATE account SET password = ? WHERE user_id = ? AND provider_id = 'credential'").bind(hashedPassword, userResult.id).run();
|
|
697
|
-
// Delete all sessions
|
|
698
|
-
await db.prepare("DELETE FROM session WHERE user_id = ?").bind(userResult.id).run();
|
|
699
|
-
// Delete the reset token
|
|
700
|
-
await c.var.services.settings.remove(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
|
|
701
|
-
return dsRedirect("/signin?reset");
|
|
702
|
-
} catch (err) {
|
|
703
|
-
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
704
|
-
console.error("Password reset error:", err);
|
|
705
|
-
return dsToast("Failed to reset password.", "error");
|
|
706
|
-
}
|
|
707
|
-
});
|
|
215
|
+
// Auth routes
|
|
216
|
+
app.route("/", setupRoutes);
|
|
217
|
+
app.route("/", signinRoutes);
|
|
218
|
+
app.route("/", resetRoutes);
|
|
708
219
|
// Dashboard routes (protected)
|
|
709
220
|
app.use("/dash/*", requireAuth());
|
|
710
221
|
app.route("/dash", dashIndexRoutes);
|
|
@@ -717,25 +228,20 @@ import { createStorageDriver } from "./lib/storage.js";
|
|
|
717
228
|
// API routes
|
|
718
229
|
app.route("/api/upload", uploadApiRoutes);
|
|
719
230
|
app.route("/api/search", searchApiRoutes);
|
|
720
|
-
// Media files from storage (
|
|
721
|
-
app.get("/media
|
|
231
|
+
// Media files from storage (path matches storage key: media/YYYY/MM/uuid.ext)
|
|
232
|
+
app.get("/media/*", async (c)=>{
|
|
722
233
|
const storage = c.var.storage;
|
|
723
234
|
if (!storage) {
|
|
724
235
|
return c.notFound();
|
|
725
236
|
}
|
|
726
|
-
//
|
|
727
|
-
const
|
|
728
|
-
const
|
|
729
|
-
const media = await c.var.services.media.getById(mediaId);
|
|
730
|
-
if (!media) {
|
|
731
|
-
return c.notFound();
|
|
732
|
-
}
|
|
733
|
-
const object = await storage.get(media.storageKey);
|
|
237
|
+
// The storage key is the full path without the leading "/"
|
|
238
|
+
const storageKey = c.req.path.slice(1);
|
|
239
|
+
const object = await storage.get(storageKey);
|
|
734
240
|
if (!object) {
|
|
735
241
|
return c.notFound();
|
|
736
242
|
}
|
|
737
243
|
const headers = new Headers();
|
|
738
|
-
headers.set("Content-Type", object.contentType ||
|
|
244
|
+
headers.set("Content-Type", object.contentType || "application/octet-stream");
|
|
739
245
|
headers.set("Cache-Control", "public, max-age=31536000, immutable");
|
|
740
246
|
return new Response(object.body, {
|
|
741
247
|
headers
|
|
@@ -750,6 +256,7 @@ import { createStorageDriver } from "./lib/storage.js";
|
|
|
750
256
|
app.route("/search", searchRoutes);
|
|
751
257
|
app.route("/archive", archiveRoutes);
|
|
752
258
|
app.route("/featured", featuredRoutes);
|
|
259
|
+
app.route("/latest", latestRoutes);
|
|
753
260
|
app.route("/collections", collectionsPageRoutes);
|
|
754
261
|
app.route("/c", collectionRoutes);
|
|
755
262
|
app.route("/p", postRoutes);
|