@podosoft/podokit 0.17.3 → 0.17.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/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.server.ts +5 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/setup-2fa/+page.server.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/setup-2fa/+page.svelte +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account-flows.ui.spec.ts +4 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +40 -7
- package/package.json +1 -1
package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.server.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { loadAccountData } from "$lib/account-data.server";
|
|
2
|
+
import { loadProtectedLayout } from "$lib/server/protected-layout";
|
|
2
3
|
import type { PageServerLoad } from "./$types";
|
|
3
4
|
|
|
4
|
-
export const load: PageServerLoad = ({ locals, fetch }) =>
|
|
5
|
+
export const load: PageServerLoad = async ({ locals, fetch }) => {
|
|
6
|
+
await loadProtectedLayout({ locals, fetch });
|
|
7
|
+
return loadAccountData(locals, fetch);
|
|
8
|
+
};
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/setup-2fa/+page.server.ts
CHANGED
|
@@ -8,6 +8,6 @@ export const load: PageServerLoad = ({ locals }) => {
|
|
|
8
8
|
requireBackendAvailable(locals);
|
|
9
9
|
if (!locals.user) redirect(302, "/login");
|
|
10
10
|
const user = locals.user as App.Locals["user"] & { twoFactorEnabled?: boolean };
|
|
11
|
-
if (user.twoFactorEnabled) redirect(302, "/
|
|
11
|
+
if (user.twoFactorEnabled) redirect(302, "/account");
|
|
12
12
|
return { user: locals.user };
|
|
13
13
|
};
|
|
@@ -15,11 +15,13 @@ async function loginFresh(page: import("@playwright/test").Page, playwright: imp
|
|
|
15
15
|
const api = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
16
16
|
await api.post("/api/auth/sign-up/email", { data: { email, password: PW, name: "Acc" } });
|
|
17
17
|
await api.dispose();
|
|
18
|
-
await ready(page, `/login?redirect=${encodeURIComponent("/
|
|
18
|
+
await ready(page, `/login?redirect=${encodeURIComponent("/account")}`);
|
|
19
19
|
await page.locator("#email").fill(email);
|
|
20
20
|
await page.locator("#password").fill(PW);
|
|
21
21
|
await page.getByRole("button", { name: "Sign in", exact: true }).click();
|
|
22
|
-
await expect(page).toHaveURL(
|
|
22
|
+
await expect(page).toHaveURL((url) => url.pathname === "/account");
|
|
23
|
+
await expect(page.getByRole("heading", { name: "Account" })).toBeVisible();
|
|
24
|
+
await expect(page.getByLabel("Name", { exact: true })).toBeVisible();
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
test("account: register and verify a phone number", async ({ page, playwright }) => {
|
|
@@ -15,6 +15,12 @@ async function openHydratedLogin(page: Page, redirect?: string): Promise<void> {
|
|
|
15
15
|
await page.waitForLoadState("networkidle");
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
async function expectAccountPage(page: Page): Promise<void> {
|
|
19
|
+
await expect(page).toHaveURL((url) => url.pathname === "/account");
|
|
20
|
+
await expect(page.getByRole("heading", { name: "Account" })).toBeVisible();
|
|
21
|
+
await expect(page.getByLabel("Name", { exact: true })).toBeVisible();
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
// A disposable account (never the shared admin/user) so enabling 2FA here can't
|
|
19
25
|
// invalidate the seeded sessions the other auth specs rely on.
|
|
20
26
|
test("two-factor: sign in with a backup code from the login page", async ({ page, playwright }) => {
|
|
@@ -31,7 +37,7 @@ test("two-factor: sign in with a backup code from the login page", async ({ page
|
|
|
31
37
|
await api.dispose();
|
|
32
38
|
|
|
33
39
|
// Password sign-in hands off to the second-factor step (no session yet).
|
|
34
|
-
await openHydratedLogin(page, "/
|
|
40
|
+
await openHydratedLogin(page, "/account");
|
|
35
41
|
await page.getByLabel("Email").fill(email);
|
|
36
42
|
await page.getByLabel("Password").fill(pw);
|
|
37
43
|
await page.getByRole("button", { name: "Sign in", exact: true }).click();
|
|
@@ -52,7 +58,30 @@ test("two-factor: sign in with a backup code from the login page", async ({ page
|
|
|
52
58
|
// The real code completes the login.
|
|
53
59
|
await page.getByLabel("Backup code").fill(backupCode);
|
|
54
60
|
await page.getByRole("button", { name: "Verify", exact: true }).click();
|
|
55
|
-
await
|
|
61
|
+
await expectAccountPage(page);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("two-factor: sign in with an authenticator code from the login page", async ({ page, playwright }) => {
|
|
65
|
+
const api = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
66
|
+
const caps = await (await api.get("/api/account/capabilities")).json();
|
|
67
|
+
test.skip(!caps?.twoFactor, "two-factor not enabled");
|
|
68
|
+
|
|
69
|
+
const email = `tf-totp-ui-${Date.now()}@example.com`;
|
|
70
|
+
const pw = "Podokit3e-Str0ng!pw";
|
|
71
|
+
await api.post("/api/auth/sign-up/email", { data: { email, password: pw, name: "UI" } });
|
|
72
|
+
const enable = await (await api.post("/api/auth/two-factor/enable", { data: { password: pw } })).json();
|
|
73
|
+
await api.post("/api/auth/two-factor/verify-totp", { data: { code: totpCode(enable.totpURI) } });
|
|
74
|
+
await api.dispose();
|
|
75
|
+
|
|
76
|
+
await openHydratedLogin(page, "/account");
|
|
77
|
+
await page.getByLabel("Email").fill(email);
|
|
78
|
+
await page.getByLabel("Password").fill(pw);
|
|
79
|
+
await page.getByRole("button", { name: "Sign in", exact: true }).click();
|
|
80
|
+
await expect(page.getByTestId("two-factor-step")).toBeVisible();
|
|
81
|
+
await page.getByLabel("Authentication code").fill(totpCode(enable.totpURI));
|
|
82
|
+
await page.getByRole("button", { name: "Verify", exact: true }).click();
|
|
83
|
+
|
|
84
|
+
await expectAccountPage(page);
|
|
56
85
|
});
|
|
57
86
|
|
|
58
87
|
test("require-2fa: a new sign-up is forced through the enrolment page", async ({ page, playwright }) => {
|
|
@@ -74,7 +103,7 @@ test("require-2fa: a new sign-up is forced through the enrolment page", async ({
|
|
|
74
103
|
await probe.dispose();
|
|
75
104
|
|
|
76
105
|
// Signing in toward a protected route lands on the mandatory enrolment page.
|
|
77
|
-
await openHydratedLogin(page, "/
|
|
106
|
+
await openHydratedLogin(page, "/account");
|
|
78
107
|
await page.getByLabel("Email").fill(email);
|
|
79
108
|
await page.getByLabel("Password").fill(pw);
|
|
80
109
|
await page.getByRole("button", { name: "Sign in", exact: true }).click();
|
|
@@ -91,8 +120,12 @@ test("require-2fa: a new sign-up is forced through the enrolment page", async ({
|
|
|
91
120
|
await page.getByLabel("3. Enter the 6-digit code").fill(totpCode(uri));
|
|
92
121
|
await page.getByRole("button", { name: "Activate and continue" }).click();
|
|
93
122
|
|
|
94
|
-
// Enrolled → the gate lets
|
|
95
|
-
await
|
|
123
|
+
// Enrolled → the gate lets the regular user into their account page.
|
|
124
|
+
await expectAccountPage(page);
|
|
125
|
+
|
|
126
|
+
// An enrolled user who revisits setup is returned to the same non-admin page.
|
|
127
|
+
await page.goto("/setup-2fa");
|
|
128
|
+
await expectAccountPage(page);
|
|
96
129
|
} finally {
|
|
97
130
|
await admin.put("/api/account/settings", { data: { require2fa: false } });
|
|
98
131
|
await expect(async () => {
|
|
@@ -119,14 +152,14 @@ test("account: regenerate backup codes shows a fresh set", async ({ page, playwr
|
|
|
119
152
|
await api.dispose();
|
|
120
153
|
|
|
121
154
|
// Sign in (backup-code path) straight to the account page.
|
|
122
|
-
await openHydratedLogin(page, "/
|
|
155
|
+
await openHydratedLogin(page, "/account");
|
|
123
156
|
await page.getByLabel("Email").fill(email);
|
|
124
157
|
await page.getByLabel("Password").fill(pw);
|
|
125
158
|
await page.getByRole("button", { name: "Sign in", exact: true }).click();
|
|
126
159
|
await page.getByRole("button", { name: "Use a backup code instead" }).click();
|
|
127
160
|
await page.getByLabel("Backup code").fill(backupCode);
|
|
128
161
|
await page.getByRole("button", { name: "Verify", exact: true }).click();
|
|
129
|
-
await
|
|
162
|
+
await expectAccountPage(page);
|
|
130
163
|
|
|
131
164
|
// Security → regenerate backup codes → a fresh set is shown.
|
|
132
165
|
await page.getByRole("button", { name: "Security" }).click();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@podosoft/podokit",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.4",
|
|
4
4
|
"description": "An opinionated but extensible starter toolkit and CLI for full-stack TypeScript apps with NestJS, SvelteKit, TailwindCSS, shadcn-svelte, Docker, and k3s.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|