@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.
Files changed (133) hide show
  1. package/dist/app.js +70 -563
  2. package/dist/auth.js +3 -0
  3. package/dist/client.js +1 -0
  4. package/dist/i18n/locales/en.js +1 -1
  5. package/dist/i18n/locales/zh-Hans.js +1 -1
  6. package/dist/i18n/locales/zh-Hant.js +1 -1
  7. package/dist/lib/avatar-upload.js +134 -0
  8. package/dist/lib/config.js +39 -0
  9. package/dist/lib/constants.js +10 -10
  10. package/dist/lib/favicon.js +102 -0
  11. package/dist/lib/image.js +13 -17
  12. package/dist/lib/media-helpers.js +2 -2
  13. package/dist/lib/navigation.js +23 -3
  14. package/dist/lib/render.js +10 -1
  15. package/dist/lib/schemas.js +31 -0
  16. package/dist/lib/timezones.js +388 -0
  17. package/dist/lib/view.js +1 -1
  18. package/dist/routes/api/posts.js +1 -1
  19. package/dist/routes/api/upload.js +3 -3
  20. package/dist/routes/auth/reset.js +221 -0
  21. package/dist/routes/auth/setup.js +194 -0
  22. package/dist/routes/auth/signin.js +176 -0
  23. package/dist/routes/dash/collections.js +23 -415
  24. package/dist/routes/dash/media.js +12 -392
  25. package/dist/routes/dash/pages.js +7 -330
  26. package/dist/routes/dash/redirects.js +18 -12
  27. package/dist/routes/dash/settings.js +198 -577
  28. package/dist/routes/feed/rss.js +2 -1
  29. package/dist/routes/feed/sitemap.js +4 -2
  30. package/dist/routes/pages/featured.js +5 -1
  31. package/dist/routes/pages/home.js +26 -1
  32. package/dist/routes/pages/latest.js +45 -0
  33. package/dist/services/post.js +30 -50
  34. package/dist/types/bindings.js +3 -0
  35. package/dist/types/config.js +147 -0
  36. package/dist/types/constants.js +27 -0
  37. package/dist/types/entities.js +3 -0
  38. package/dist/types/operations.js +3 -0
  39. package/dist/types/props.js +3 -0
  40. package/dist/types/views.js +5 -0
  41. package/dist/types.js +8 -111
  42. package/dist/ui/color-themes.js +33 -33
  43. package/dist/ui/compose/ComposeDialog.js +36 -21
  44. package/dist/ui/dash/PageForm.js +21 -15
  45. package/dist/ui/dash/PostForm.js +22 -16
  46. package/dist/ui/dash/collections/CollectionForm.js +152 -0
  47. package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
  48. package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
  49. package/dist/ui/dash/media/MediaListContent.js +166 -0
  50. package/dist/ui/dash/media/ViewMediaContent.js +212 -0
  51. package/dist/ui/dash/pages/LinkFormContent.js +130 -0
  52. package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
  53. package/dist/ui/dash/settings/AccountContent.js +209 -0
  54. package/dist/ui/dash/settings/AppearanceContent.js +259 -0
  55. package/dist/ui/dash/settings/GeneralContent.js +536 -0
  56. package/dist/ui/dash/settings/SettingsNav.js +41 -0
  57. package/dist/ui/font-themes.js +36 -0
  58. package/dist/ui/layouts/BaseLayout.js +24 -2
  59. package/dist/ui/layouts/SiteLayout.js +47 -19
  60. package/package.json +1 -1
  61. package/src/app.tsx +95 -553
  62. package/src/auth.ts +4 -1
  63. package/src/client.ts +1 -0
  64. package/src/i18n/locales/en.po +240 -175
  65. package/src/i18n/locales/en.ts +1 -1
  66. package/src/i18n/locales/zh-Hans.po +240 -175
  67. package/src/i18n/locales/zh-Hans.ts +1 -1
  68. package/src/i18n/locales/zh-Hant.po +240 -175
  69. package/src/i18n/locales/zh-Hant.ts +1 -1
  70. package/src/lib/__tests__/config.test.ts +192 -0
  71. package/src/lib/__tests__/favicon.test.ts +151 -0
  72. package/src/lib/__tests__/image.test.ts +2 -6
  73. package/src/lib/__tests__/timezones.test.ts +61 -0
  74. package/src/lib/__tests__/view.test.ts +2 -2
  75. package/src/lib/avatar-upload.ts +165 -0
  76. package/src/lib/config.ts +47 -0
  77. package/src/lib/constants.ts +19 -11
  78. package/src/lib/favicon.ts +115 -0
  79. package/src/lib/image.ts +13 -21
  80. package/src/lib/media-helpers.ts +2 -2
  81. package/src/lib/navigation.ts +33 -2
  82. package/src/lib/render.tsx +15 -1
  83. package/src/lib/schemas.ts +39 -0
  84. package/src/lib/timezones.ts +325 -0
  85. package/src/lib/view.ts +1 -1
  86. package/src/routes/api/posts.ts +1 -1
  87. package/src/routes/api/upload.ts +2 -3
  88. package/src/routes/auth/reset.tsx +239 -0
  89. package/src/routes/auth/setup.tsx +189 -0
  90. package/src/routes/auth/signin.tsx +163 -0
  91. package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
  92. package/src/routes/dash/collections.tsx +17 -366
  93. package/src/routes/dash/media.tsx +12 -414
  94. package/src/routes/dash/pages.tsx +8 -348
  95. package/src/routes/dash/redirects.tsx +20 -14
  96. package/src/routes/dash/settings.tsx +243 -534
  97. package/src/routes/feed/__tests__/rss.test.ts +141 -0
  98. package/src/routes/feed/rss.ts +3 -1
  99. package/src/routes/feed/sitemap.ts +4 -2
  100. package/src/routes/pages/featured.tsx +7 -1
  101. package/src/routes/pages/home.tsx +25 -2
  102. package/src/routes/pages/latest.tsx +59 -0
  103. package/src/services/post.ts +34 -66
  104. package/src/styles/components.css +0 -65
  105. package/src/styles/tokens.css +1 -1
  106. package/src/styles/ui.css +24 -40
  107. package/src/types/bindings.ts +30 -0
  108. package/src/types/config.ts +183 -0
  109. package/src/types/constants.ts +26 -0
  110. package/src/types/entities.ts +109 -0
  111. package/src/types/operations.ts +88 -0
  112. package/src/types/props.ts +115 -0
  113. package/src/types/views.ts +172 -0
  114. package/src/types.ts +8 -644
  115. package/src/ui/__tests__/font-themes.test.ts +34 -0
  116. package/src/ui/color-themes.ts +34 -34
  117. package/src/ui/compose/ComposeDialog.tsx +40 -21
  118. package/src/ui/dash/PageForm.tsx +25 -19
  119. package/src/ui/dash/PostForm.tsx +26 -20
  120. package/src/ui/dash/collections/CollectionForm.tsx +153 -0
  121. package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
  122. package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
  123. package/src/ui/dash/media/MediaListContent.tsx +201 -0
  124. package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
  125. package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
  126. package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
  127. package/src/ui/dash/settings/AccountContent.tsx +176 -0
  128. package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
  129. package/src/ui/dash/settings/GeneralContent.tsx +533 -0
  130. package/src/ui/dash/settings/SettingsNav.tsx +56 -0
  131. package/src/ui/font-themes.ts +54 -0
  132. package/src/ui/layouts/BaseLayout.tsx +17 -0
  133. 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
- import { hashPassword } from "better-auth/crypto";
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(SETTINGS_KEYS.CUSTOM_CSS)
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
- const themeStyle = buildThemeStyle(activeTheme, resolvedConfig.cssVariables);
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
- // Setup page component
170
- const SetupContent = ()=>{
171
- const { i18n: $__i18n, _: $__ } = $_useLingui();
172
- return /*#__PURE__*/ _jsx("div", {
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 (UUIDv7-based URLs with extension)
721
- app.get("/media/:idWithExt", async (c)=>{
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
- // Extract ID from "uuid.ext" format
727
- const idWithExt = c.req.param("idWithExt");
728
- const mediaId = idWithExt.replace(/\.[^.]+$/, "");
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 || media.mimeType);
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);