@jant/core 0.2.2 → 0.2.4
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/client.d.ts +4 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +6 -2
- package/dist/lib/assets.d.ts +4 -3
- package/dist/lib/assets.d.ts.map +1 -1
- package/dist/lib/assets.js +1 -3
- package/dist/theme/layouts/BaseLayout.js +0 -5
- package/package.json +4 -5
- package/src/app.tsx +377 -0
- package/src/auth.ts +38 -0
- package/src/client.ts +7 -2
- package/src/db/index.ts +14 -0
- package/src/db/migrations/0000_solid_moon_knight.sql +118 -0
- package/src/db/migrations/0001_add_search_fts.sql +40 -0
- package/src/db/migrations/0002_collection_path.sql +2 -0
- package/src/db/migrations/0003_collection_path_nullable.sql +21 -0
- package/src/db/migrations/0004_media_uuid.sql +35 -0
- package/src/db/migrations/meta/0000_snapshot.json +784 -0
- package/src/db/migrations/meta/_journal.json +41 -0
- package/src/db/schema.ts +159 -0
- package/src/i18n/EXAMPLES.md +235 -0
- package/src/i18n/README.md +296 -0
- package/src/i18n/Trans.tsx +31 -0
- package/src/i18n/context.tsx +101 -0
- package/src/i18n/detect.ts +100 -0
- package/src/i18n/i18n.ts +62 -0
- package/src/i18n/index.ts +65 -0
- package/src/i18n/locales/en.po +875 -0
- package/src/i18n/locales/en.ts +1 -0
- package/src/i18n/locales/zh-Hans.po +875 -0
- package/src/i18n/locales/zh-Hans.ts +1 -0
- package/src/i18n/locales/zh-Hant.po +875 -0
- package/src/i18n/locales/zh-Hant.ts +1 -0
- package/src/i18n/locales.ts +14 -0
- package/src/i18n/middleware.ts +59 -0
- package/src/index.ts +42 -0
- package/src/lib/assets.ts +49 -0
- package/src/lib/constants.ts +67 -0
- package/src/lib/image.ts +107 -0
- package/src/lib/index.ts +9 -0
- package/src/lib/markdown.ts +93 -0
- package/src/lib/schemas.ts +92 -0
- package/src/lib/sqid.ts +79 -0
- package/src/lib/sse.ts +152 -0
- package/src/lib/time.ts +117 -0
- package/src/lib/url.ts +107 -0
- package/src/middleware/auth.ts +59 -0
- package/src/preset.css +2 -11
- package/src/routes/api/posts.ts +127 -0
- package/src/routes/api/search.ts +53 -0
- package/src/routes/api/upload.ts +240 -0
- package/src/routes/dash/collections.tsx +341 -0
- package/src/routes/dash/index.tsx +89 -0
- package/src/routes/dash/media.tsx +551 -0
- package/src/routes/dash/pages.tsx +245 -0
- package/src/routes/dash/posts.tsx +202 -0
- package/src/routes/dash/redirects.tsx +155 -0
- package/src/routes/dash/settings.tsx +93 -0
- package/src/routes/feed/rss.ts +119 -0
- package/src/routes/feed/sitemap.ts +75 -0
- package/src/routes/pages/archive.tsx +223 -0
- package/src/routes/pages/collection.tsx +79 -0
- package/src/routes/pages/home.tsx +93 -0
- package/src/routes/pages/page.tsx +64 -0
- package/src/routes/pages/post.tsx +81 -0
- package/src/routes/pages/search.tsx +162 -0
- package/src/services/collection.ts +180 -0
- package/src/services/index.ts +40 -0
- package/src/services/media.ts +97 -0
- package/src/services/post.ts +279 -0
- package/src/services/redirect.ts +74 -0
- package/src/services/search.ts +117 -0
- package/src/services/settings.ts +76 -0
- package/src/theme/components/ActionButtons.tsx +98 -0
- package/src/theme/components/CrudPageHeader.tsx +48 -0
- package/src/theme/components/DangerZone.tsx +77 -0
- package/src/theme/components/EmptyState.tsx +56 -0
- package/src/theme/components/ListItemRow.tsx +24 -0
- package/src/theme/components/PageForm.tsx +114 -0
- package/src/theme/components/Pagination.tsx +196 -0
- package/src/theme/components/PostForm.tsx +122 -0
- package/src/theme/components/PostList.tsx +68 -0
- package/src/theme/components/ThreadView.tsx +118 -0
- package/src/theme/components/TypeBadge.tsx +28 -0
- package/src/theme/components/VisibilityBadge.tsx +33 -0
- package/src/theme/components/index.ts +12 -0
- package/src/theme/index.ts +24 -0
- package/src/theme/layouts/BaseLayout.tsx +50 -0
- package/src/theme/layouts/DashLayout.tsx +108 -0
- package/src/theme/layouts/index.ts +2 -0
- package/src/types.ts +222 -0
- package/static/assets/datastar.min.js +0 -7
package/dist/client.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client-side JavaScript entry point
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Bundles all interactive components:
|
|
5
|
+
* - Datastar (reactivity)
|
|
6
|
+
* - BaseCoat (dialogs, dropdowns, etc.)
|
|
5
7
|
*/
|
|
8
|
+
import "@sudodevnull/datastar";
|
|
6
9
|
import "basecoat-css/all";
|
|
7
10
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,uBAAuB,CAAC;AAG/B,OAAO,kBAAkB,CAAC"}
|
package/dist/client.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client-side JavaScript entry point
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
|
|
4
|
+
* Bundles all interactive components:
|
|
5
|
+
* - Datastar (reactivity)
|
|
6
|
+
* - BaseCoat (dialogs, dropdowns, etc.)
|
|
7
|
+
*/ // Datastar for reactivity (data-* attributes)
|
|
8
|
+
import "@sudodevnull/datastar";
|
|
9
|
+
// BaseCoat interactive components
|
|
6
10
|
import "basecoat-css/all";
|
package/dist/lib/assets.d.ts
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
* Asset paths for SSR
|
|
3
3
|
*
|
|
4
4
|
* Development: Paths injected via vite.config.ts `define`
|
|
5
|
-
* Production: Paths replaced at build time with hashed filenames
|
|
5
|
+
* Production: Paths replaced at build time with hashed filenames
|
|
6
6
|
*/
|
|
7
7
|
interface Assets {
|
|
8
|
-
/** CSS path
|
|
8
|
+
/** CSS path */
|
|
9
9
|
styles: string;
|
|
10
|
+
/** Main client bundle (includes Datastar + BaseCoat) */
|
|
10
11
|
client: string;
|
|
11
|
-
|
|
12
|
+
/** Image processor script (lazy-loaded on media page) */
|
|
12
13
|
imageProcessor: string;
|
|
13
14
|
}
|
|
14
15
|
/**
|
package/dist/lib/assets.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../../src/lib/assets.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,UAAU,MAAM;IACd,
|
|
1
|
+
{"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../../src/lib/assets.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,UAAU,MAAM;IACd,eAAe;IACf,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,cAAc,EAAE,MAAM,CAAC;CACxB;AAcD;;GAEG;AACH,wBAAgB,SAAS,IAAI,MAAM,CAclC;AAGD,eAAO,MAAM,MAAM,QAAc,CAAC"}
|
package/dist/lib/assets.js
CHANGED
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
* Asset paths for SSR
|
|
3
3
|
*
|
|
4
4
|
* Development: Paths injected via vite.config.ts `define`
|
|
5
|
-
* Production: Paths replaced at build time with hashed filenames
|
|
5
|
+
* Production: Paths replaced at build time with hashed filenames
|
|
6
6
|
*/ // Production paths - replaced at build time
|
|
7
7
|
const PROD_ASSETS = {
|
|
8
8
|
styles: "__JANT_ASSET_STYLES__",
|
|
9
9
|
client: "__JANT_ASSET_CLIENT__",
|
|
10
|
-
datastar: "__JANT_ASSET_DATASTAR__",
|
|
11
10
|
imageProcessor: "__JANT_ASSET_IMAGE_PROCESSOR__"
|
|
12
11
|
};
|
|
13
12
|
/**
|
|
@@ -18,7 +17,6 @@ const PROD_ASSETS = {
|
|
|
18
17
|
return {
|
|
19
18
|
styles: __JANT_DEV_STYLES__,
|
|
20
19
|
client: __JANT_DEV_CLIENT__,
|
|
21
|
-
datastar: __JANT_DEV_DATASTAR__,
|
|
22
20
|
imageProcessor: __JANT_DEV_IMAGE_PROCESSOR__
|
|
23
21
|
};
|
|
24
22
|
}
|
|
@@ -41,11 +41,6 @@ export const BaseLayout = ({ title, description, lang = "en", c, children })=>{
|
|
|
41
41
|
type: "module",
|
|
42
42
|
src: assets.client,
|
|
43
43
|
defer: true
|
|
44
|
-
}),
|
|
45
|
-
/*#__PURE__*/ _jsx("script", {
|
|
46
|
-
type: "module",
|
|
47
|
-
src: assets.datastar,
|
|
48
|
-
defer: true
|
|
49
44
|
})
|
|
50
45
|
]
|
|
51
46
|
}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jant/core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "A modern, open-source microblogging platform built on Cloudflare Workers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,15 +24,14 @@
|
|
|
24
24
|
"dist",
|
|
25
25
|
"static",
|
|
26
26
|
"migrations",
|
|
27
|
-
"src
|
|
28
|
-
"src/styles",
|
|
29
|
-
"src/client.ts"
|
|
27
|
+
"src"
|
|
30
28
|
],
|
|
31
29
|
"publishConfig": {
|
|
32
30
|
"access": "public"
|
|
33
31
|
},
|
|
34
32
|
"dependencies": {
|
|
35
33
|
"@lingui/core": "^5.9.0",
|
|
34
|
+
"@sudodevnull/datastar": "^0.19.9",
|
|
36
35
|
"better-auth": "^1.4.18",
|
|
37
36
|
"drizzle-orm": "^0.45.1",
|
|
38
37
|
"hono": "^4.11.7",
|
|
@@ -46,7 +45,6 @@
|
|
|
46
45
|
"tailwindcss": "^4.0.0"
|
|
47
46
|
},
|
|
48
47
|
"devDependencies": {
|
|
49
|
-
"basecoat-css": "^0.3.10",
|
|
50
48
|
"@cloudflare/vite-plugin": "^1.22.1",
|
|
51
49
|
"@cloudflare/workers-types": "^4.20260131.0",
|
|
52
50
|
"@eslint/js": "^9.39.2",
|
|
@@ -60,6 +58,7 @@
|
|
|
60
58
|
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
|
61
59
|
"@typescript-eslint/parser": "^8.54.0",
|
|
62
60
|
"autoprefixer": "^10.4.24",
|
|
61
|
+
"basecoat-css": "^0.3.10",
|
|
63
62
|
"drizzle-kit": "^0.31.8",
|
|
64
63
|
"eslint": "^9.39.2",
|
|
65
64
|
"eslint-plugin-react": "^7.37.5",
|
package/src/app.tsx
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jant App Factory
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
import type { FC } from "hono/jsx";
|
|
7
|
+
import { createDatabase } from "./db/index.js";
|
|
8
|
+
import { createServices, type Services } from "./services/index.js";
|
|
9
|
+
import { createAuth, type Auth } from "./auth.js";
|
|
10
|
+
import { i18nMiddleware, useLingui } from "./i18n/index.js";
|
|
11
|
+
import type { Bindings, JantConfig } from "./types.js";
|
|
12
|
+
|
|
13
|
+
// Routes - Pages
|
|
14
|
+
import { homeRoutes } from "./routes/pages/home.js";
|
|
15
|
+
import { postRoutes } from "./routes/pages/post.js";
|
|
16
|
+
import { pageRoutes } from "./routes/pages/page.js";
|
|
17
|
+
import { collectionRoutes } from "./routes/pages/collection.js";
|
|
18
|
+
import { archiveRoutes } from "./routes/pages/archive.js";
|
|
19
|
+
import { searchRoutes } from "./routes/pages/search.js";
|
|
20
|
+
|
|
21
|
+
// Routes - Dashboard
|
|
22
|
+
import { dashIndexRoutes } from "./routes/dash/index.js";
|
|
23
|
+
import { postsRoutes as dashPostsRoutes } from "./routes/dash/posts.js";
|
|
24
|
+
import { pagesRoutes as dashPagesRoutes } from "./routes/dash/pages.js";
|
|
25
|
+
import { mediaRoutes as dashMediaRoutes } from "./routes/dash/media.js";
|
|
26
|
+
import { settingsRoutes as dashSettingsRoutes } from "./routes/dash/settings.js";
|
|
27
|
+
import { redirectsRoutes as dashRedirectsRoutes } from "./routes/dash/redirects.js";
|
|
28
|
+
import { collectionsRoutes as dashCollectionsRoutes } from "./routes/dash/collections.js";
|
|
29
|
+
|
|
30
|
+
// Routes - API
|
|
31
|
+
import { postsApiRoutes } from "./routes/api/posts.js";
|
|
32
|
+
import { uploadApiRoutes } from "./routes/api/upload.js";
|
|
33
|
+
import { searchApiRoutes } from "./routes/api/search.js";
|
|
34
|
+
|
|
35
|
+
// Routes - Feed
|
|
36
|
+
import { rssRoutes } from "./routes/feed/rss.js";
|
|
37
|
+
import { sitemapRoutes } from "./routes/feed/sitemap.js";
|
|
38
|
+
|
|
39
|
+
// Middleware
|
|
40
|
+
import { requireAuth } from "./middleware/auth.js";
|
|
41
|
+
|
|
42
|
+
// Layouts for auth pages
|
|
43
|
+
import { BaseLayout } from "./theme/layouts/index.js";
|
|
44
|
+
|
|
45
|
+
// Extend Hono's context variables
|
|
46
|
+
export interface AppVariables {
|
|
47
|
+
services: Services;
|
|
48
|
+
auth: Auth;
|
|
49
|
+
config: JantConfig;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type App = Hono<{ Bindings: Bindings; Variables: AppVariables }>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create a Jant application
|
|
56
|
+
*
|
|
57
|
+
* @param config - Optional configuration
|
|
58
|
+
* @returns Hono app instance
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* import { createApp } from "@jant/core";
|
|
63
|
+
*
|
|
64
|
+
* export default createApp({
|
|
65
|
+
* site: { name: "My Blog" },
|
|
66
|
+
* theme: { components: { PostCard: MyPostCard } },
|
|
67
|
+
* });
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export function createApp(config: JantConfig = {}): App {
|
|
71
|
+
const app = new Hono<{ Bindings: Bindings; Variables: AppVariables }>();
|
|
72
|
+
|
|
73
|
+
// Initialize services, auth, and config middleware
|
|
74
|
+
app.use("*", async (c, next) => {
|
|
75
|
+
const db = createDatabase(c.env.DB);
|
|
76
|
+
const services = createServices(db, c.env.DB);
|
|
77
|
+
c.set("services", services);
|
|
78
|
+
c.set("config", config);
|
|
79
|
+
|
|
80
|
+
if (c.env.AUTH_SECRET) {
|
|
81
|
+
const auth = createAuth(c.env.DB, {
|
|
82
|
+
secret: c.env.AUTH_SECRET,
|
|
83
|
+
baseURL: c.env.SITE_URL,
|
|
84
|
+
});
|
|
85
|
+
c.set("auth", auth);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await next();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// i18n middleware
|
|
92
|
+
app.use("*", i18nMiddleware());
|
|
93
|
+
|
|
94
|
+
// Trailing slash redirect (redirect /foo/ to /foo)
|
|
95
|
+
app.use("*", async (c, next) => {
|
|
96
|
+
const url = new URL(c.req.url);
|
|
97
|
+
if (url.pathname !== "/" && url.pathname.endsWith("/")) {
|
|
98
|
+
const newUrl = url.pathname.slice(0, -1) + url.search;
|
|
99
|
+
return c.redirect(newUrl, 301);
|
|
100
|
+
}
|
|
101
|
+
await next();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Redirect middleware
|
|
105
|
+
app.use("*", async (c, next) => {
|
|
106
|
+
const path = new URL(c.req.url).pathname;
|
|
107
|
+
// Skip redirect check for API routes and static assets
|
|
108
|
+
if (path.startsWith("/api/") || path.startsWith("/assets/")) {
|
|
109
|
+
return next();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const redirect = await c.var.services.redirects.getByPath(path);
|
|
113
|
+
if (redirect) {
|
|
114
|
+
return c.redirect(redirect.toPath, redirect.type);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await next();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Health check
|
|
121
|
+
app.get("/health", (c) =>
|
|
122
|
+
c.json({
|
|
123
|
+
status: "ok",
|
|
124
|
+
auth: c.env.AUTH_SECRET ? "configured" : "missing",
|
|
125
|
+
authSecretLength: c.env.AUTH_SECRET?.length ?? 0,
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// better-auth handler
|
|
130
|
+
app.all("/api/auth/*", async (c) => {
|
|
131
|
+
if (!c.var.auth) {
|
|
132
|
+
return c.json({ error: "Auth not configured. Set AUTH_SECRET." }, 500);
|
|
133
|
+
}
|
|
134
|
+
return c.var.auth.handler(c.req.raw);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// API Routes
|
|
138
|
+
app.route("/api/posts", postsApiRoutes);
|
|
139
|
+
|
|
140
|
+
// Setup page component
|
|
141
|
+
const SetupContent: FC<{ error?: string }> = ({ error }) => {
|
|
142
|
+
const { t } = useLingui();
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div class="min-h-screen flex items-center justify-center">
|
|
146
|
+
<div class="card max-w-md w-full">
|
|
147
|
+
<header>
|
|
148
|
+
<h2>{t({ message: "Welcome to Jant", comment: "@context: Setup page welcome heading" })}</h2>
|
|
149
|
+
<p>{t({ message: "Let's set up your site.", comment: "@context: Setup page description" })}</p>
|
|
150
|
+
</header>
|
|
151
|
+
<section>
|
|
152
|
+
{error && <p class="text-destructive text-sm mb-4">{error}</p>}
|
|
153
|
+
<form method="post" action="/setup" class="flex flex-col gap-4">
|
|
154
|
+
<div class="field">
|
|
155
|
+
<label class="label">{t({ message: "Site Name", comment: "@context: Setup form field - site name" })}</label>
|
|
156
|
+
<input type="text" name="siteName" class="input" required placeholder={t({ message: "My Blog", comment: "@context: Setup site name placeholder" })} />
|
|
157
|
+
</div>
|
|
158
|
+
<div class="field">
|
|
159
|
+
<label class="label">{t({ message: "Your Name", comment: "@context: Setup form field - user name" })}</label>
|
|
160
|
+
<input type="text" name="name" class="input" required placeholder="John Doe" />
|
|
161
|
+
</div>
|
|
162
|
+
<div class="field">
|
|
163
|
+
<label class="label">{t({ message: "Email", comment: "@context: Setup/signin form field - email" })}</label>
|
|
164
|
+
<input type="email" name="email" class="input" required placeholder="you@example.com" />
|
|
165
|
+
</div>
|
|
166
|
+
<div class="field">
|
|
167
|
+
<label class="label">{t({ message: "Password", comment: "@context: Setup/signin form field - password" })}</label>
|
|
168
|
+
<input type="password" name="password" class="input" required minLength={8} />
|
|
169
|
+
</div>
|
|
170
|
+
<button type="submit" class="btn">{t({ message: "Complete Setup", comment: "@context: Setup form submit button" })}</button>
|
|
171
|
+
</form>
|
|
172
|
+
</section>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Setup page
|
|
179
|
+
app.get("/setup", async (c) => {
|
|
180
|
+
const isComplete = await c.var.services.settings.isOnboardingComplete();
|
|
181
|
+
if (isComplete) return c.redirect("/");
|
|
182
|
+
|
|
183
|
+
const error = c.req.query("error");
|
|
184
|
+
|
|
185
|
+
return c.html(
|
|
186
|
+
<BaseLayout title="Setup - Jant" c={c}>
|
|
187
|
+
<SetupContent error={error} />
|
|
188
|
+
</BaseLayout>
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
app.post("/setup", async (c) => {
|
|
193
|
+
const isComplete = await c.var.services.settings.isOnboardingComplete();
|
|
194
|
+
if (isComplete) return c.redirect("/");
|
|
195
|
+
|
|
196
|
+
const formData = await c.req.formData();
|
|
197
|
+
const siteName = formData.get("siteName") as string;
|
|
198
|
+
const name = formData.get("name") as string;
|
|
199
|
+
const email = formData.get("email") as string;
|
|
200
|
+
const password = formData.get("password") as string;
|
|
201
|
+
|
|
202
|
+
if (!siteName || !name || !email || !password) {
|
|
203
|
+
return c.redirect("/setup?error=All fields are required");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (password.length < 8) {
|
|
207
|
+
return c.redirect("/setup?error=Password must be at least 8 characters");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!c.var.auth) {
|
|
211
|
+
return c.redirect("/setup?error=AUTH_SECRET not configured");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const signUpResponse = await c.var.auth.api.signUpEmail({
|
|
216
|
+
body: { name, email, password },
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
if (!signUpResponse || "error" in signUpResponse) {
|
|
220
|
+
return c.redirect("/setup?error=Failed to create account");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await c.var.services.settings.setMany({
|
|
224
|
+
SITE_NAME: siteName,
|
|
225
|
+
SITE_LANGUAGE: "en",
|
|
226
|
+
});
|
|
227
|
+
await c.var.services.settings.completeOnboarding();
|
|
228
|
+
|
|
229
|
+
return c.redirect("/signin");
|
|
230
|
+
} catch (err) {
|
|
231
|
+
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
232
|
+
console.error("Setup error:", err);
|
|
233
|
+
return c.redirect("/setup?error=Failed to create account");
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Signin page component
|
|
238
|
+
const SigninContent: FC<{ error?: string }> = ({ error }) => {
|
|
239
|
+
const { t } = useLingui();
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<div class="min-h-screen flex items-center justify-center">
|
|
243
|
+
<div class="card max-w-md w-full">
|
|
244
|
+
<header>
|
|
245
|
+
<h2>{t({ message: "Sign In", comment: "@context: Sign in page heading" })}</h2>
|
|
246
|
+
</header>
|
|
247
|
+
<section>
|
|
248
|
+
{error && <p class="text-destructive text-sm mb-4">{error}</p>}
|
|
249
|
+
<form method="post" action="/signin" class="flex flex-col gap-4">
|
|
250
|
+
<div class="field">
|
|
251
|
+
<label class="label">{t({ message: "Email", comment: "@context: Setup/signin form field - email" })}</label>
|
|
252
|
+
<input type="email" name="email" class="input" required />
|
|
253
|
+
</div>
|
|
254
|
+
<div class="field">
|
|
255
|
+
<label class="label">{t({ message: "Password", comment: "@context: Setup/signin form field - password" })}</label>
|
|
256
|
+
<input type="password" name="password" class="input" required />
|
|
257
|
+
</div>
|
|
258
|
+
<button type="submit" class="btn">{t({ message: "Sign In", comment: "@context: Sign in form submit button" })}</button>
|
|
259
|
+
</form>
|
|
260
|
+
</section>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Signin page
|
|
267
|
+
app.get("/signin", async (c) => {
|
|
268
|
+
const error = c.req.query("error");
|
|
269
|
+
|
|
270
|
+
return c.html(
|
|
271
|
+
<BaseLayout title="Sign In - Jant" c={c}>
|
|
272
|
+
<SigninContent error={error} />
|
|
273
|
+
</BaseLayout>
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
app.post("/signin", async (c) => {
|
|
278
|
+
if (!c.var.auth) {
|
|
279
|
+
return c.redirect("/signin?error=Auth not configured");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const formData = await c.req.formData();
|
|
283
|
+
const email = formData.get("email") as string;
|
|
284
|
+
const password = formData.get("password") as string;
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const signInRequest = new Request(`${c.env.SITE_URL}/api/auth/sign-in/email`, {
|
|
288
|
+
method: "POST",
|
|
289
|
+
headers: { "Content-Type": "application/json" },
|
|
290
|
+
body: JSON.stringify({ email, password }),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const response = await c.var.auth.handler(signInRequest);
|
|
294
|
+
|
|
295
|
+
if (!response.ok) {
|
|
296
|
+
return c.redirect("/signin?error=Invalid email or password");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const headers = new Headers(response.headers);
|
|
300
|
+
headers.set("Location", "/dash");
|
|
301
|
+
|
|
302
|
+
return new Response(null, { status: 302, headers });
|
|
303
|
+
} catch (err) {
|
|
304
|
+
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
305
|
+
console.error("Signin error:", err);
|
|
306
|
+
return c.redirect("/signin?error=Invalid email or password");
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
app.get("/signout", async (c) => {
|
|
311
|
+
if (c.var.auth) {
|
|
312
|
+
try {
|
|
313
|
+
await c.var.auth.api.signOut({ headers: c.req.raw.headers });
|
|
314
|
+
} catch {
|
|
315
|
+
// Ignore signout errors
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return c.redirect("/");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Dashboard routes (protected)
|
|
322
|
+
app.use("/dash/*", requireAuth());
|
|
323
|
+
app.route("/dash", dashIndexRoutes);
|
|
324
|
+
app.route("/dash/posts", dashPostsRoutes);
|
|
325
|
+
app.route("/dash/pages", dashPagesRoutes);
|
|
326
|
+
app.route("/dash/media", dashMediaRoutes);
|
|
327
|
+
app.route("/dash/settings", dashSettingsRoutes);
|
|
328
|
+
app.route("/dash/redirects", dashRedirectsRoutes);
|
|
329
|
+
app.route("/dash/collections", dashCollectionsRoutes);
|
|
330
|
+
|
|
331
|
+
// API routes
|
|
332
|
+
app.route("/api/upload", uploadApiRoutes);
|
|
333
|
+
app.route("/api/search", searchApiRoutes);
|
|
334
|
+
|
|
335
|
+
// Media files from R2 (UUIDv7-based URLs with extension)
|
|
336
|
+
app.get("/media/:idWithExt", async (c) => {
|
|
337
|
+
if (!c.env.R2) {
|
|
338
|
+
return c.notFound();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Extract ID from "uuid.ext" format
|
|
342
|
+
const idWithExt = c.req.param("idWithExt");
|
|
343
|
+
const mediaId = idWithExt.replace(/\.[^.]+$/, "");
|
|
344
|
+
|
|
345
|
+
const media = await c.var.services.media.getById(mediaId);
|
|
346
|
+
if (!media) {
|
|
347
|
+
return c.notFound();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const object = await c.env.R2.get(media.r2Key);
|
|
351
|
+
if (!object) {
|
|
352
|
+
return c.notFound();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const headers = new Headers();
|
|
356
|
+
headers.set("Content-Type", object.httpMetadata?.contentType || media.mimeType);
|
|
357
|
+
headers.set("Cache-Control", "public, max-age=31536000, immutable");
|
|
358
|
+
|
|
359
|
+
return new Response(object.body, { headers });
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Feed routes
|
|
363
|
+
app.route("/feed", rssRoutes);
|
|
364
|
+
app.route("/", sitemapRoutes);
|
|
365
|
+
|
|
366
|
+
// Frontend routes
|
|
367
|
+
app.route("/search", searchRoutes);
|
|
368
|
+
app.route("/archive", archiveRoutes);
|
|
369
|
+
app.route("/c", collectionRoutes);
|
|
370
|
+
app.route("/p", postRoutes);
|
|
371
|
+
app.route("/", homeRoutes);
|
|
372
|
+
|
|
373
|
+
// Custom page catch-all (must be last)
|
|
374
|
+
app.route("/", pageRoutes);
|
|
375
|
+
|
|
376
|
+
return app;
|
|
377
|
+
}
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication with better-auth
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { betterAuth } from "better-auth";
|
|
6
|
+
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
7
|
+
import { drizzle } from "drizzle-orm/d1";
|
|
8
|
+
import * as schema from "./db/schema.js";
|
|
9
|
+
|
|
10
|
+
export function createAuth(d1: D1Database, options: { secret: string; baseURL: string }) {
|
|
11
|
+
const db = drizzle(d1, { schema });
|
|
12
|
+
|
|
13
|
+
return betterAuth({
|
|
14
|
+
database: drizzleAdapter(db, {
|
|
15
|
+
provider: "sqlite",
|
|
16
|
+
schema: {
|
|
17
|
+
user: schema.user,
|
|
18
|
+
session: schema.session,
|
|
19
|
+
account: schema.account,
|
|
20
|
+
verification: schema.verification,
|
|
21
|
+
},
|
|
22
|
+
}),
|
|
23
|
+
secret: options.secret,
|
|
24
|
+
baseURL: options.baseURL,
|
|
25
|
+
emailAndPassword: {
|
|
26
|
+
enabled: true,
|
|
27
|
+
autoSignIn: true,
|
|
28
|
+
},
|
|
29
|
+
session: {
|
|
30
|
+
cookieCache: {
|
|
31
|
+
enabled: true,
|
|
32
|
+
maxAge: 60 * 5, // 5 minutes
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type Auth = ReturnType<typeof createAuth>;
|
package/src/client.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client-side JavaScript entry point
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Bundles all interactive components:
|
|
5
|
+
* - Datastar (reactivity)
|
|
6
|
+
* - BaseCoat (dialogs, dropdowns, etc.)
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
|
-
//
|
|
9
|
+
// Datastar for reactivity (data-* attributes)
|
|
10
|
+
import "@sudodevnull/datastar";
|
|
11
|
+
|
|
12
|
+
// BaseCoat interactive components
|
|
8
13
|
import "basecoat-css/all";
|
package/src/db/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { drizzle } from "drizzle-orm/d1";
|
|
6
|
+
import * as schema from "./schema.js";
|
|
7
|
+
|
|
8
|
+
export type Database = ReturnType<typeof createDatabase>;
|
|
9
|
+
|
|
10
|
+
export function createDatabase(d1: D1Database) {
|
|
11
|
+
return drizzle(d1, { schema });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { schema };
|