@jant/core 0.3.25 → 0.3.26
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 +67 -562
- 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 +93 -553
- 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,6 +83,10 @@ 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;
|
|
88
92
|
const auth = createAuth(session, {
|
|
@@ -95,17 +99,36 @@ import { createStorageDriver } from "./lib/storage.js";
|
|
|
95
99
|
});
|
|
96
100
|
// Onboarding gate — redirect to /setup if not yet initialized
|
|
97
101
|
app.use("*", requireOnboarding());
|
|
98
|
-
// Theme middleware - resolve active color theme, custom CSS, and auth state
|
|
102
|
+
// Theme middleware - resolve active color theme, font theme, custom CSS, and auth state
|
|
99
103
|
app.use("*", async (c, next)=>{
|
|
100
|
-
const [themeId, customCSS] = await Promise.all([
|
|
104
|
+
const [themeId, fontThemeId, customCSS, noindexValue, avatarKey] = await Promise.all([
|
|
101
105
|
c.var.services.settings.get(SETTINGS_KEYS.THEME),
|
|
102
|
-
c.var.services.settings.get(
|
|
106
|
+
c.var.services.settings.get("FONT_THEME"),
|
|
107
|
+
c.var.services.settings.get(SETTINGS_KEYS.CUSTOM_CSS),
|
|
108
|
+
c.var.services.settings.get("NOINDEX"),
|
|
109
|
+
c.var.services.settings.get("SITE_AVATAR")
|
|
103
110
|
]);
|
|
104
111
|
const themes = getAvailableThemes(resolvedConfig);
|
|
105
112
|
const activeTheme = themeId ? themes.find((t)=>t.id === themeId) : undefined;
|
|
106
|
-
|
|
113
|
+
// Build font override CSS variables
|
|
114
|
+
const fontTheme = fontThemeId ? BUILTIN_FONT_THEMES.find((f)=>f.id === fontThemeId) : undefined;
|
|
115
|
+
const fontOverrides = {};
|
|
116
|
+
if (fontTheme) {
|
|
117
|
+
fontOverrides["--font-body"] = fontTheme.fontFamily;
|
|
118
|
+
}
|
|
119
|
+
const themeStyle = buildThemeStyle(activeTheme, {
|
|
120
|
+
...resolvedConfig.cssVariables,
|
|
121
|
+
...fontOverrides
|
|
122
|
+
});
|
|
107
123
|
c.set("themeStyle", themeStyle);
|
|
108
124
|
c.set("customCSS", customCSS ?? "");
|
|
125
|
+
// Noindex
|
|
126
|
+
c.set("noindex", noindexValue === "true");
|
|
127
|
+
// Resolve favicon from avatar storage key
|
|
128
|
+
if (avatarKey) {
|
|
129
|
+
const publicUrl = getPublicUrlForProvider(c.env.STORAGE_DRIVER || "r2", c.env.R2_PUBLIC_URL, c.env.S3_PUBLIC_URL);
|
|
130
|
+
c.set("faviconUrl", getMediaUrl(avatarKey, publicUrl));
|
|
131
|
+
}
|
|
109
132
|
// Check auth state for data-authenticated attribute on <body>
|
|
110
133
|
let isAuthenticated = false;
|
|
111
134
|
if (c.var.auth) {
|
|
@@ -151,6 +174,27 @@ import { createStorageDriver } from "./lib/storage.js";
|
|
|
151
174
|
auth: c.env.AUTH_SECRET ? "configured" : "missing",
|
|
152
175
|
authSecretLength: c.env.AUTH_SECRET?.length ?? 0
|
|
153
176
|
}));
|
|
177
|
+
// Favicon routes - serve from DB settings (small files, avoids R2 round-trip)
|
|
178
|
+
app.get("/favicon.ico", async (c)=>{
|
|
179
|
+
const data = await c.var.services.settings.get("SITE_FAVICON_ICO");
|
|
180
|
+
if (!data) return c.notFound();
|
|
181
|
+
return new Response(base64ToUint8Array(data), {
|
|
182
|
+
headers: {
|
|
183
|
+
"Content-Type": "image/x-icon",
|
|
184
|
+
"Cache-Control": "public, max-age=86400"
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
app.get("/apple-touch-icon.png", async (c)=>{
|
|
189
|
+
const data = await c.var.services.settings.get("SITE_FAVICON_APPLE_TOUCH");
|
|
190
|
+
if (!data) return c.notFound();
|
|
191
|
+
return new Response(base64ToUint8Array(data), {
|
|
192
|
+
headers: {
|
|
193
|
+
"Content-Type": "image/png",
|
|
194
|
+
"Cache-Control": "public, max-age=86400"
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
});
|
|
154
198
|
// better-auth handler
|
|
155
199
|
app.all("/api/auth/*", async (c)=>{
|
|
156
200
|
if (!c.var.auth) {
|
|
@@ -166,545 +210,10 @@ import { createStorageDriver } from "./lib/storage.js";
|
|
|
166
210
|
app.route("/api/nav-items", navItemsApiRoutes);
|
|
167
211
|
app.route("/api/collections", collectionsApiRoutes);
|
|
168
212
|
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
|
-
});
|
|
213
|
+
// Auth routes
|
|
214
|
+
app.route("/", setupRoutes);
|
|
215
|
+
app.route("/", signinRoutes);
|
|
216
|
+
app.route("/", resetRoutes);
|
|
708
217
|
// Dashboard routes (protected)
|
|
709
218
|
app.use("/dash/*", requireAuth());
|
|
710
219
|
app.route("/dash", dashIndexRoutes);
|
|
@@ -717,25 +226,20 @@ import { createStorageDriver } from "./lib/storage.js";
|
|
|
717
226
|
// API routes
|
|
718
227
|
app.route("/api/upload", uploadApiRoutes);
|
|
719
228
|
app.route("/api/search", searchApiRoutes);
|
|
720
|
-
// Media files from storage (
|
|
721
|
-
app.get("/media
|
|
229
|
+
// Media files from storage (path matches storage key: media/YYYY/MM/uuid.ext)
|
|
230
|
+
app.get("/media/*", async (c)=>{
|
|
722
231
|
const storage = c.var.storage;
|
|
723
232
|
if (!storage) {
|
|
724
233
|
return c.notFound();
|
|
725
234
|
}
|
|
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);
|
|
235
|
+
// The storage key is the full path without the leading "/"
|
|
236
|
+
const storageKey = c.req.path.slice(1);
|
|
237
|
+
const object = await storage.get(storageKey);
|
|
734
238
|
if (!object) {
|
|
735
239
|
return c.notFound();
|
|
736
240
|
}
|
|
737
241
|
const headers = new Headers();
|
|
738
|
-
headers.set("Content-Type", object.contentType ||
|
|
242
|
+
headers.set("Content-Type", object.contentType || "application/octet-stream");
|
|
739
243
|
headers.set("Cache-Control", "public, max-age=31536000, immutable");
|
|
740
244
|
return new Response(object.body, {
|
|
741
245
|
headers
|
|
@@ -750,6 +254,7 @@ import { createStorageDriver } from "./lib/storage.js";
|
|
|
750
254
|
app.route("/search", searchRoutes);
|
|
751
255
|
app.route("/archive", archiveRoutes);
|
|
752
256
|
app.route("/featured", featuredRoutes);
|
|
257
|
+
app.route("/latest", latestRoutes);
|
|
753
258
|
app.route("/collections", collectionsPageRoutes);
|
|
754
259
|
app.route("/c", collectionRoutes);
|
|
755
260
|
app.route("/p", postRoutes);
|