@jant/core 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +3 -0
- package/dist/middleware/onboarding.d.ts +26 -0
- package/dist/middleware/onboarding.d.ts.map +1 -0
- package/dist/middleware/onboarding.js +36 -0
- package/dist/routes/pages/home.js +0 -4
- package/package.json +1 -1
- package/src/app.tsx +4 -0
- package/src/middleware/__tests__/onboarding.test.ts +143 -0
- package/src/middleware/onboarding.ts +60 -0
- package/src/routes/pages/home.tsx +0 -5
package/dist/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EAAc,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAGlD,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EAAc,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAGlD,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAwCvD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,UAAU,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,GAAG,GAAG,IAAI,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,YAAY,CAAA;CAAE,CAAC,CAAC;AAExE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,SAAS,CAAC,MAAM,GAAE,UAAe,GAAG,GAAG,CA0oBtD"}
|
package/dist/app.js
CHANGED
|
@@ -33,6 +33,7 @@ import { rssRoutes } from "./routes/feed/rss.js";
|
|
|
33
33
|
import { sitemapRoutes } from "./routes/feed/sitemap.js";
|
|
34
34
|
// Middleware
|
|
35
35
|
import { requireAuth } from "./middleware/auth.js";
|
|
36
|
+
import { requireOnboarding } from "./middleware/onboarding.js";
|
|
36
37
|
// Layouts for auth pages
|
|
37
38
|
import { BaseLayout } from "./theme/layouts/index.js";
|
|
38
39
|
import { dsRedirect, dsToast } from "./lib/sse.js";
|
|
@@ -78,6 +79,8 @@ import { getAvailableThemes, buildThemeStyle } from "./lib/theme.js";
|
|
|
78
79
|
}
|
|
79
80
|
await next();
|
|
80
81
|
});
|
|
82
|
+
// Onboarding gate — redirect to /setup if not yet initialized
|
|
83
|
+
app.use("*", requireOnboarding());
|
|
81
84
|
// Theme middleware - resolve active color theme and build CSS
|
|
82
85
|
app.use("*", async (c, next)=>{
|
|
83
86
|
const themeId = await c.var.services.settings.get(SETTINGS_KEYS.THEME);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding Middleware
|
|
3
|
+
*
|
|
4
|
+
* Redirects all requests to /setup if onboarding hasn't been completed.
|
|
5
|
+
* Caches the result in memory so the DB is only queried once per isolate lifetime.
|
|
6
|
+
*/
|
|
7
|
+
import type { MiddlewareHandler } from "hono";
|
|
8
|
+
import type { Bindings } from "../types.js";
|
|
9
|
+
import type { AppVariables } from "../app.js";
|
|
10
|
+
type Env = {
|
|
11
|
+
Bindings: Bindings;
|
|
12
|
+
Variables: AppVariables;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Middleware that redirects to /setup if onboarding is not complete.
|
|
16
|
+
* Uses module-level caching: once onboarding is confirmed complete,
|
|
17
|
+
* no further DB queries are made for the lifetime of the Worker isolate.
|
|
18
|
+
*/
|
|
19
|
+
export declare function requireOnboarding(): MiddlewareHandler<Env>;
|
|
20
|
+
/**
|
|
21
|
+
* Reset the onboarding cache. Only for testing.
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export declare function resetOnboardingCache(): void;
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=onboarding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"onboarding.d.ts","sourceRoot":"","sources":["../../src/middleware/onboarding.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE9C,KAAK,GAAG,GAAG;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,YAAY,CAAA;CAAE,CAAC;AAK3D;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,iBAAiB,CAAC,GAAG,CAAC,CAmB1D;AAaD;;;GAGG;AACH,wBAAgB,oBAAoB,SAEnC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding Middleware
|
|
3
|
+
*
|
|
4
|
+
* Redirects all requests to /setup if onboarding hasn't been completed.
|
|
5
|
+
* Caches the result in memory so the DB is only queried once per isolate lifetime.
|
|
6
|
+
*/ /** In-memory cache — persists across requests within a Worker isolate */ let onboardingComplete = false;
|
|
7
|
+
/**
|
|
8
|
+
* Middleware that redirects to /setup if onboarding is not complete.
|
|
9
|
+
* Uses module-level caching: once onboarding is confirmed complete,
|
|
10
|
+
* no further DB queries are made for the lifetime of the Worker isolate.
|
|
11
|
+
*/ export function requireOnboarding() {
|
|
12
|
+
return async (c, next)=>{
|
|
13
|
+
if (onboardingComplete) {
|
|
14
|
+
return next();
|
|
15
|
+
}
|
|
16
|
+
const path = new URL(c.req.url).pathname;
|
|
17
|
+
if (shouldBypass(path)) {
|
|
18
|
+
return next();
|
|
19
|
+
}
|
|
20
|
+
const isComplete = await c.var.services.settings.isOnboardingComplete();
|
|
21
|
+
if (isComplete) {
|
|
22
|
+
onboardingComplete = true;
|
|
23
|
+
return next();
|
|
24
|
+
}
|
|
25
|
+
return c.redirect("/setup");
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function shouldBypass(path) {
|
|
29
|
+
return path === "/setup" || path === "/health" || path === "/signin" || path === "/signout" || path === "/reset" || path.startsWith("/api/auth/");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Reset the onboarding cache. Only for testing.
|
|
33
|
+
* @internal
|
|
34
|
+
*/ export function resetOnboardingCache() {
|
|
35
|
+
onboardingComplete = false;
|
|
36
|
+
}
|
|
@@ -100,10 +100,6 @@ function HomeContent({ siteName, posts }) {
|
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
102
|
homeRoutes.get("/", async (c)=>{
|
|
103
|
-
const isComplete = await c.var.services.settings.isOnboardingComplete();
|
|
104
|
-
if (!isComplete) {
|
|
105
|
-
return c.redirect("/setup");
|
|
106
|
-
}
|
|
107
103
|
const siteName = await getSiteName(c);
|
|
108
104
|
const posts = await c.var.services.posts.list({
|
|
109
105
|
visibility: [
|
package/package.json
CHANGED
package/src/app.tsx
CHANGED
|
@@ -41,6 +41,7 @@ import { sitemapRoutes } from "./routes/feed/sitemap.js";
|
|
|
41
41
|
|
|
42
42
|
// Middleware
|
|
43
43
|
import { requireAuth } from "./middleware/auth.js";
|
|
44
|
+
import { requireOnboarding } from "./middleware/onboarding.js";
|
|
44
45
|
|
|
45
46
|
// Layouts for auth pages
|
|
46
47
|
import { BaseLayout } from "./theme/layouts/index.js";
|
|
@@ -104,6 +105,9 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
104
105
|
await next();
|
|
105
106
|
});
|
|
106
107
|
|
|
108
|
+
// Onboarding gate — redirect to /setup if not yet initialized
|
|
109
|
+
app.use("*", requireOnboarding());
|
|
110
|
+
|
|
107
111
|
// Theme middleware - resolve active color theme and build CSS
|
|
108
112
|
app.use("*", async (c, next) => {
|
|
109
113
|
const themeId = await c.var.services.settings.get(SETTINGS_KEYS.THEME);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { requireOnboarding, resetOnboardingCache } from "../onboarding.js";
|
|
4
|
+
import type { Bindings } from "../../types.js";
|
|
5
|
+
import type { AppVariables } from "../../app.js";
|
|
6
|
+
|
|
7
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
8
|
+
|
|
9
|
+
function createMockServices(complete: boolean) {
|
|
10
|
+
let callCount = 0;
|
|
11
|
+
const services = {
|
|
12
|
+
settings: {
|
|
13
|
+
isOnboardingComplete: async () => {
|
|
14
|
+
callCount++;
|
|
15
|
+
return complete;
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
} as AppVariables["services"];
|
|
19
|
+
return { services, getCallCount: () => callCount };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createApp(complete: boolean) {
|
|
23
|
+
const mock = createMockServices(complete);
|
|
24
|
+
const app = new Hono<Env>();
|
|
25
|
+
|
|
26
|
+
app.use("*", async (c, next) => {
|
|
27
|
+
c.set("services", mock.services);
|
|
28
|
+
await next();
|
|
29
|
+
});
|
|
30
|
+
app.use("*", requireOnboarding());
|
|
31
|
+
|
|
32
|
+
// Register routes for testing
|
|
33
|
+
app.get("/", (c) => c.text("Home"));
|
|
34
|
+
app.get("/dash", (c) => c.text("Dashboard"));
|
|
35
|
+
app.get("/archive", (c) => c.text("Archive"));
|
|
36
|
+
app.get("/p/abc", (c) => c.text("Post"));
|
|
37
|
+
app.get("/setup", (c) => c.text("Setup"));
|
|
38
|
+
app.get("/health", (c) => c.text("OK"));
|
|
39
|
+
app.get("/signin", (c) => c.text("Signin"));
|
|
40
|
+
app.get("/signout", (c) => c.text("Signout"));
|
|
41
|
+
app.get("/reset", (c) => c.text("Reset"));
|
|
42
|
+
app.get("/api/auth/session", (c) => c.json({ ok: true }));
|
|
43
|
+
|
|
44
|
+
return { app, getCallCount: mock.getCallCount };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("requireOnboarding", () => {
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
resetOnboardingCache();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("redirects to /setup when onboarding not complete", async () => {
|
|
53
|
+
const { app } = createApp(false);
|
|
54
|
+
const res = await app.request("/", { redirect: "manual" });
|
|
55
|
+
expect(res.status).toBe(302);
|
|
56
|
+
expect(res.headers.get("Location")).toBe("/setup");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("redirects /dash to /setup when onboarding not complete", async () => {
|
|
60
|
+
const { app } = createApp(false);
|
|
61
|
+
const res = await app.request("/dash", { redirect: "manual" });
|
|
62
|
+
expect(res.status).toBe(302);
|
|
63
|
+
expect(res.headers.get("Location")).toBe("/setup");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("redirects /archive to /setup when onboarding not complete", async () => {
|
|
67
|
+
const { app } = createApp(false);
|
|
68
|
+
const res = await app.request("/archive", { redirect: "manual" });
|
|
69
|
+
expect(res.status).toBe(302);
|
|
70
|
+
expect(res.headers.get("Location")).toBe("/setup");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("allows through when onboarding is complete", async () => {
|
|
74
|
+
const { app } = createApp(true);
|
|
75
|
+
const res = await app.request("/");
|
|
76
|
+
expect(res.status).toBe(200);
|
|
77
|
+
expect(await res.text()).toBe("Home");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("caches result — second request skips DB query", async () => {
|
|
81
|
+
const { app, getCallCount } = createApp(true);
|
|
82
|
+
|
|
83
|
+
await app.request("/");
|
|
84
|
+
expect(getCallCount()).toBe(1);
|
|
85
|
+
|
|
86
|
+
await app.request("/dash");
|
|
87
|
+
expect(getCallCount()).toBe(1); // still 1 — cached
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("does not cache incomplete status", async () => {
|
|
91
|
+
const { app, getCallCount } = createApp(false);
|
|
92
|
+
|
|
93
|
+
await app.request("/", { redirect: "manual" });
|
|
94
|
+
expect(getCallCount()).toBe(1);
|
|
95
|
+
|
|
96
|
+
await app.request("/dash", { redirect: "manual" });
|
|
97
|
+
expect(getCallCount()).toBe(2); // queried again
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("bypass paths", () => {
|
|
101
|
+
it("allows /setup without checking DB", async () => {
|
|
102
|
+
const { app, getCallCount } = createApp(false);
|
|
103
|
+
const res = await app.request("/setup");
|
|
104
|
+
expect(res.status).toBe(200);
|
|
105
|
+
expect(getCallCount()).toBe(0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("allows /health without checking DB", async () => {
|
|
109
|
+
const { app, getCallCount } = createApp(false);
|
|
110
|
+
const res = await app.request("/health");
|
|
111
|
+
expect(res.status).toBe(200);
|
|
112
|
+
expect(getCallCount()).toBe(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("allows /signin without checking DB", async () => {
|
|
116
|
+
const { app, getCallCount } = createApp(false);
|
|
117
|
+
const res = await app.request("/signin");
|
|
118
|
+
expect(res.status).toBe(200);
|
|
119
|
+
expect(getCallCount()).toBe(0);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("allows /signout without checking DB", async () => {
|
|
123
|
+
const { app, getCallCount } = createApp(false);
|
|
124
|
+
const res = await app.request("/signout");
|
|
125
|
+
expect(res.status).toBe(200);
|
|
126
|
+
expect(getCallCount()).toBe(0);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("allows /reset without checking DB", async () => {
|
|
130
|
+
const { app, getCallCount } = createApp(false);
|
|
131
|
+
const res = await app.request("/reset");
|
|
132
|
+
expect(res.status).toBe(200);
|
|
133
|
+
expect(getCallCount()).toBe(0);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("allows /api/auth/* without checking DB", async () => {
|
|
137
|
+
const { app, getCallCount } = createApp(false);
|
|
138
|
+
const res = await app.request("/api/auth/session");
|
|
139
|
+
expect(res.status).toBe(200);
|
|
140
|
+
expect(getCallCount()).toBe(0);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding Middleware
|
|
3
|
+
*
|
|
4
|
+
* Redirects all requests to /setup if onboarding hasn't been completed.
|
|
5
|
+
* Caches the result in memory so the DB is only queried once per isolate lifetime.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { MiddlewareHandler } from "hono";
|
|
9
|
+
import type { Bindings } from "../types.js";
|
|
10
|
+
import type { AppVariables } from "../app.js";
|
|
11
|
+
|
|
12
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
13
|
+
|
|
14
|
+
/** In-memory cache — persists across requests within a Worker isolate */
|
|
15
|
+
let onboardingComplete = false;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Middleware that redirects to /setup if onboarding is not complete.
|
|
19
|
+
* Uses module-level caching: once onboarding is confirmed complete,
|
|
20
|
+
* no further DB queries are made for the lifetime of the Worker isolate.
|
|
21
|
+
*/
|
|
22
|
+
export function requireOnboarding(): MiddlewareHandler<Env> {
|
|
23
|
+
return async (c, next) => {
|
|
24
|
+
if (onboardingComplete) {
|
|
25
|
+
return next();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const path = new URL(c.req.url).pathname;
|
|
29
|
+
if (shouldBypass(path)) {
|
|
30
|
+
return next();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const isComplete = await c.var.services.settings.isOnboardingComplete();
|
|
34
|
+
if (isComplete) {
|
|
35
|
+
onboardingComplete = true;
|
|
36
|
+
return next();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return c.redirect("/setup");
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function shouldBypass(path: string): boolean {
|
|
44
|
+
return (
|
|
45
|
+
path === "/setup" ||
|
|
46
|
+
path === "/health" ||
|
|
47
|
+
path === "/signin" ||
|
|
48
|
+
path === "/signout" ||
|
|
49
|
+
path === "/reset" ||
|
|
50
|
+
path.startsWith("/api/auth/")
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Reset the onboarding cache. Only for testing.
|
|
56
|
+
* @internal
|
|
57
|
+
*/
|
|
58
|
+
export function resetOnboardingCache() {
|
|
59
|
+
onboardingComplete = false;
|
|
60
|
+
}
|
|
@@ -102,11 +102,6 @@ function HomeContent({ siteName, posts }: { siteName: string; posts: Post[] }) {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
homeRoutes.get("/", async (c) => {
|
|
105
|
-
const isComplete = await c.var.services.settings.isOnboardingComplete();
|
|
106
|
-
if (!isComplete) {
|
|
107
|
-
return c.redirect("/setup");
|
|
108
|
-
}
|
|
109
|
-
|
|
110
105
|
const siteName = await getSiteName(c);
|
|
111
106
|
|
|
112
107
|
const posts = await c.var.services.posts.list({
|