@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.
@@ -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 }) => loadAccountData(locals, fetch);
5
+ export const load: PageServerLoad = async ({ locals, fetch }) => {
6
+ await loadProtectedLayout({ locals, fetch });
7
+ return loadAccountData(locals, fetch);
8
+ };
@@ -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, "/admin");
11
+ if (user.twoFactorEnabled) redirect(302, "/account");
12
12
  return { user: locals.user };
13
13
  };
@@ -51,7 +51,7 @@
51
51
  error = i18n.t.auth.twoFactorInvalidCode;
52
52
  return;
53
53
  }
54
- await goto("/admin", { invalidateAll: true });
54
+ await goto("/account", { invalidateAll: true });
55
55
  }
56
56
 
57
57
  function downloadCodes(): void {
@@ -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("/admin/account")}`);
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(/\/admin\/account/);
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, "/admin/account");
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 expect(page).toHaveURL(/\/admin\/account/);
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, "/admin");
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 them into the app (no longer bounced to /setup-2fa).
95
- await expect(page).toHaveURL(/\/admin/);
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, "/admin/account");
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 expect(page).toHaveURL(/\/admin\/account/);
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",
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": {