@podosoft/podokit 0.12.4 → 0.14.0
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/add.d.ts +27 -1
- package/dist/add.js +3 -1
- package/dist/index.js +6 -3
- package/dist/lib.d.ts +1 -1
- package/dist/lockfile.d.ts +4 -1
- package/dist/lockfile.js +4 -3
- package/dist/module-migrations.d.ts +25 -0
- package/dist/module-migrations.js +233 -0
- package/dist/templates/fullstack-nest-svelte/Dockerfile.dev +3 -1
- package/dist/templates/fullstack-nest-svelte/apps/api/package.json +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/Dockerfile +3 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/package.json +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/input/input.svelte +3 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/+page.svelte +5 -0
- package/dist/templates/fullstack-nest-svelte/compose.dev.yaml +6 -0
- package/dist/templates/fullstack-nest-svelte/dot-env.example +2 -0
- package/dist/templates/fullstack-nest-svelte/infra/k3s/configmap.yaml +1 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image-upload.interceptor.ts +39 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.controller.ts +73 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.module.ts +9 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.spec.ts +43 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.ts +110 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.validation.spec.ts +79 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.validation.ts +74 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/registry.svelte.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +155 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/session-idle-timeout.svelte +109 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-runtime.svelte +5 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +21 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +21 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/protected-layout.ts +78 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/+layout.server.ts +8 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/+layout.svelte +2 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/settings/+page.server.ts +8 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/settings/+page.svelte +73 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/+layout.server.ts +2 -47
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.server.ts +3 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.svelte +5 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.server.ts +15 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+page.svelte +4 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +44 -4
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +10 -10
- package/dist/templates/modules/admin-dashboard/files/tests/api/email.api.spec.ts +3 -3
- package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +96 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +2 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account-flows.ui.spec.ts +3 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +70 -7
- package/dist/templates/modules/admin-dashboard/files/tests/ui/appearance.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/dashboard.ui.spec.ts +18 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +3 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +6 -9
- package/dist/templates/modules/admin-dashboard/files/tests/ui/impersonation.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/login-methods.ui.spec.ts +2 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/organizations.ui.spec.ts +2 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +2 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings-dialogs.ui.spec.ts +36 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +77 -7
- package/dist/templates/modules/admin-dashboard/files/tests/ui/sidebar.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +3 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +4 -4
- package/dist/templates/modules/admin-dashboard/files/tests/ui/verify-email.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/module.manifest.json +74 -11
- package/dist/templates/modules/audit-log/module.manifest.json +1 -1
- package/dist/templates/modules/auth/files/apps/api/src/account/account.controller.ts +3 -6
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-config-store.ts +6 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +12 -5
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +9 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/user-delete-handlers.spec.ts +42 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/user-delete-handlers.ts +27 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +55 -3
- package/dist/templates/modules/auth/module.manifest.json +5 -4
- package/dist/templates/modules/file-upload/files/tests/api/files.api.spec.ts +1 -1
- package/dist/templates/modules/mailer/module.manifest.json +1 -1
- package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +1 -1
- package/dist/templates/todo/Dockerfile.dev +3 -1
- package/dist/templates/todo/apps/api/package.json +1 -1
- package/dist/templates/todo/apps/web/package.json +1 -1
- package/dist/templates/todo/compose.dev.yaml +6 -0
- package/dist/update.d.ts +8 -1
- package/dist/update.js +68 -12
- package/package.json +1 -1
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/{app-sidebar.svelte → admin-sidebar.svelte} +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/+page.svelte +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/account/+page.server.ts +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/account/+page.svelte +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/audit/+page.server.ts +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/audit/+page.svelte +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/organizations/+page.server.ts +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/organizations/+page.svelte +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/sessions/+page.server.ts +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/sessions/+page.svelte +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/settings/general-settings.svelte +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/users/+page.server.ts +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/users/+page.svelte +0 -0
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"noAccount": "No account?",
|
|
76
76
|
"signUp": "Sign up",
|
|
77
77
|
"signInFailed": "Sign in failed",
|
|
78
|
+
"idleLogout": "You were signed out after a period of inactivity. Sign in to continue.",
|
|
78
79
|
"createTitle": "Create account",
|
|
79
80
|
"createDesc": "Sign up to get started.",
|
|
80
81
|
"name": "Name",
|
|
@@ -317,6 +318,12 @@
|
|
|
317
318
|
"twoFactorDesc": "TOTP second factor with backup codes.",
|
|
318
319
|
"require2fa": "Require two-factor",
|
|
319
320
|
"require2faDesc": "Every user must enrol in 2FA after signing up before using the app.",
|
|
321
|
+
"autoLogout": "Automatic logout",
|
|
322
|
+
"autoLogoutDesc": "End sessions after a period without user activity.",
|
|
323
|
+
"autoLogoutMinutes": "{minutes} min",
|
|
324
|
+
"autoLogoutDialogDesc": "Choose how long a signed-in user can remain inactive. The policy applies to every account and open browser tab.",
|
|
325
|
+
"autoLogoutField": "Idle time (minutes)",
|
|
326
|
+
"autoLogoutHint": "Enter a whole number from 5 to 10080 minutes (7 days).",
|
|
320
327
|
"socialProviders": "Social providers",
|
|
321
328
|
"socialProvidersDesc": "OAuth sign-in (Google, GitHub).",
|
|
322
329
|
"breachCheck": "Breached-password check",
|
|
@@ -330,7 +337,7 @@
|
|
|
330
337
|
"emailTitle": "Email",
|
|
331
338
|
"emailSubtitle": "Outgoing mail (SMTP) and the email-verification policy.",
|
|
332
339
|
"securityTitle": "Security & policy",
|
|
333
|
-
"securitySubtitle": "Second factor, breached-password rejection, account deletion, and audit logging.",
|
|
340
|
+
"securitySubtitle": "Second factor, automatic logout, breached-password rejection, account deletion, and audit logging.",
|
|
334
341
|
"advancedTitle": "Advanced",
|
|
335
342
|
"advancedSubtitle": "API keys, organizations, and acting as an OIDC identity provider.",
|
|
336
343
|
"socialTitle": "Social login",
|
|
@@ -495,6 +502,19 @@
|
|
|
495
502
|
"account": {
|
|
496
503
|
"title": "Account",
|
|
497
504
|
"profile": "Profile",
|
|
505
|
+
"profileImage": "Profile image",
|
|
506
|
+
"profileImageHint": "PNG, JPEG, or WebP. Up to {size} MB and {width} × {height} px.",
|
|
507
|
+
"profileImageUpload": "Upload image",
|
|
508
|
+
"profileImageReplace": "Replace image",
|
|
509
|
+
"profileImageRemove": "Remove image",
|
|
510
|
+
"profileImageUploading": "Uploading…",
|
|
511
|
+
"profileImageUpdated": "Profile image updated",
|
|
512
|
+
"profileImageRemoved": "Profile image removed",
|
|
513
|
+
"profileImageRequired": "Choose an image to upload",
|
|
514
|
+
"profileImageTypeInvalid": "Choose a valid PNG, JPEG, or WebP image",
|
|
515
|
+
"profileImageTooLarge": "The image must be 2 MB or smaller",
|
|
516
|
+
"profileImageDimensionsInvalid": "The image must not exceed 2048 × 2048 px",
|
|
517
|
+
"profileImageFailed": "Failed to update profile image",
|
|
498
518
|
"name": "Name",
|
|
499
519
|
"username": "Username",
|
|
500
520
|
"phone": "Phone number",
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"noAccount": "계정이 없으신가요?",
|
|
76
76
|
"signUp": "회원가입",
|
|
77
77
|
"signInFailed": "로그인에 실패했습니다",
|
|
78
|
+
"idleLogout": "활동이 없어 자동으로 로그아웃되었습니다. 계속하려면 다시 로그인하세요.",
|
|
78
79
|
"createTitle": "회원가입",
|
|
79
80
|
"createDesc": "가입하고 시작하세요.",
|
|
80
81
|
"name": "이름",
|
|
@@ -317,6 +318,12 @@
|
|
|
317
318
|
"twoFactorDesc": "TOTP 2차 인증 + 백업 코드.",
|
|
318
319
|
"require2fa": "2FA 필수화",
|
|
319
320
|
"require2faDesc": "모든 사용자가 가입 후 2FA를 등록해야 앱을 사용할 수 있습니다.",
|
|
321
|
+
"autoLogout": "자동 로그아웃",
|
|
322
|
+
"autoLogoutDesc": "사용자 활동이 없는 시간이 지나면 세션을 종료합니다.",
|
|
323
|
+
"autoLogoutMinutes": "{minutes}분",
|
|
324
|
+
"autoLogoutDialogDesc": "로그인한 사용자가 활동 없이 유지할 수 있는 시간을 설정합니다. 모든 계정과 열린 브라우저 탭에 적용됩니다.",
|
|
325
|
+
"autoLogoutField": "활동 없음 시간(분)",
|
|
326
|
+
"autoLogoutHint": "5분부터 10080분(7일) 사이의 정수를 입력하세요.",
|
|
320
327
|
"socialProviders": "소셜 로그인",
|
|
321
328
|
"socialProvidersDesc": "OAuth 로그인 (Google, GitHub).",
|
|
322
329
|
"breachCheck": "유출 비밀번호 검사",
|
|
@@ -330,7 +337,7 @@
|
|
|
330
337
|
"emailTitle": "이메일",
|
|
331
338
|
"emailSubtitle": "메일 발송(SMTP) 설정과 이메일 인증 정책.",
|
|
332
339
|
"securityTitle": "보안·정책",
|
|
333
|
-
"securitySubtitle": "2단계 인증, 유출 비밀번호 거부, 계정 삭제, 감사 로그.",
|
|
340
|
+
"securitySubtitle": "2단계 인증, 자동 로그아웃, 유출 비밀번호 거부, 계정 삭제, 감사 로그.",
|
|
334
341
|
"advancedTitle": "고급",
|
|
335
342
|
"advancedSubtitle": "API 키, 조직, OIDC 공급자(자체 IdP) 역할.",
|
|
336
343
|
"socialTitle": "소셜 로그인",
|
|
@@ -495,6 +502,19 @@
|
|
|
495
502
|
"account": {
|
|
496
503
|
"title": "계정",
|
|
497
504
|
"profile": "프로필",
|
|
505
|
+
"profileImage": "프로필 이미지",
|
|
506
|
+
"profileImageHint": "PNG, JPEG, WebP · 최대 {size}MB · 최대 {width} × {height}px",
|
|
507
|
+
"profileImageUpload": "이미지 업로드",
|
|
508
|
+
"profileImageReplace": "이미지 교체",
|
|
509
|
+
"profileImageRemove": "이미지 제거",
|
|
510
|
+
"profileImageUploading": "업로드 중…",
|
|
511
|
+
"profileImageUpdated": "프로필 이미지를 변경했습니다",
|
|
512
|
+
"profileImageRemoved": "프로필 이미지를 제거했습니다",
|
|
513
|
+
"profileImageRequired": "업로드할 이미지를 선택해 주세요",
|
|
514
|
+
"profileImageTypeInvalid": "올바른 PNG, JPEG 또는 WebP 이미지를 선택해 주세요",
|
|
515
|
+
"profileImageTooLarge": "이미지는 2MB 이하여야 합니다",
|
|
516
|
+
"profileImageDimensionsInvalid": "이미지는 2048 × 2048px를 초과할 수 없습니다",
|
|
517
|
+
"profileImageFailed": "프로필 이미지 변경에 실패했습니다",
|
|
498
518
|
"name": "이름",
|
|
499
519
|
"username": "사용자명",
|
|
500
520
|
"phone": "전화번호",
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/protected-layout.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { redirect } from "@sveltejs/kit";
|
|
2
|
+
import type { Capabilities } from "@podosoft/podokit-api-client";
|
|
3
|
+
import { requireBackendAvailable } from "./guards";
|
|
4
|
+
|
|
5
|
+
type ServerFetch = (
|
|
6
|
+
input: RequestInfo | URL,
|
|
7
|
+
init?: RequestInit,
|
|
8
|
+
) => Promise<Response>;
|
|
9
|
+
|
|
10
|
+
export interface ProtectedLayoutInput {
|
|
11
|
+
locals: App.Locals;
|
|
12
|
+
fetch: ServerFetch;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ProtectedLayoutData {
|
|
16
|
+
user: App.Locals["user"];
|
|
17
|
+
impersonating: boolean;
|
|
18
|
+
capabilities: Capabilities;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Load the shared signed-in route context without imposing an application shell.
|
|
23
|
+
* The `(app)` group stays available for product UI, while `(admin)` adds admin chrome.
|
|
24
|
+
*/
|
|
25
|
+
export async function loadProtectedLayout({
|
|
26
|
+
locals,
|
|
27
|
+
fetch,
|
|
28
|
+
}: ProtectedLayoutInput): Promise<ProtectedLayoutData> {
|
|
29
|
+
requireBackendAvailable(locals);
|
|
30
|
+
|
|
31
|
+
let capabilities: Capabilities = {
|
|
32
|
+
twoFactor: false,
|
|
33
|
+
providers: [],
|
|
34
|
+
deleteAccount: false,
|
|
35
|
+
auditLog: false,
|
|
36
|
+
emailVerification: false,
|
|
37
|
+
signupApprovalRequired: false,
|
|
38
|
+
passwordBreachCheck: false,
|
|
39
|
+
magicLink: false,
|
|
40
|
+
emailOtp: false,
|
|
41
|
+
username: false,
|
|
42
|
+
multiSession: false,
|
|
43
|
+
phoneNumber: false,
|
|
44
|
+
apiKey: false,
|
|
45
|
+
passkey: false,
|
|
46
|
+
organization: false,
|
|
47
|
+
oidcProvider: false,
|
|
48
|
+
roles: ["admin", "user"],
|
|
49
|
+
};
|
|
50
|
+
try {
|
|
51
|
+
const response = await fetch("/api/account/capabilities");
|
|
52
|
+
if (response.ok) capabilities = (await response.json()) as Capabilities;
|
|
53
|
+
} catch {
|
|
54
|
+
/* keep defaults */
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Require-two-factor policy: a signed-in user who has not enrolled is sent to
|
|
58
|
+
// setup. The API guard remains authoritative; this redirect provides the UX.
|
|
59
|
+
const user = locals.user as (App.Locals["user"] & { twoFactorEnabled?: boolean }) | null;
|
|
60
|
+
let mustEnrol = false;
|
|
61
|
+
if (user && !user.twoFactorEnabled) {
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetch("/api/account/require-2fa");
|
|
64
|
+
mustEnrol =
|
|
65
|
+
response.ok &&
|
|
66
|
+
((await response.json()) as { require2fa?: boolean }).require2fa === true;
|
|
67
|
+
} catch {
|
|
68
|
+
/* do not block the app if the policy check fails */
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (mustEnrol) redirect(302, "/setup-2fa");
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
user: locals.user,
|
|
75
|
+
impersonating: Boolean(locals.session?.impersonatedBy),
|
|
76
|
+
capabilities,
|
|
77
|
+
};
|
|
78
|
+
}
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/+layout.server.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LayoutServerLoad } from "./$types";
|
|
2
|
+
import { loadProtectedLayout } from "$lib/server/protected-layout";
|
|
3
|
+
import { requireAdmin } from "$lib/server/guards";
|
|
4
|
+
|
|
5
|
+
export const load: LayoutServerLoad = async ({ locals, fetch }) => {
|
|
6
|
+
requireAdmin(locals.user, locals);
|
|
7
|
+
return loadProtectedLayout({ locals, fetch });
|
|
8
|
+
};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import * as Sidebar from "$lib/components/ui/sidebar";
|
|
4
4
|
import { Separator } from "$lib/components/ui/separator";
|
|
5
5
|
import { Button } from "$lib/components/ui/button";
|
|
6
|
-
import
|
|
6
|
+
import AdminSidebar from "$lib/components/admin-sidebar.svelte";
|
|
7
7
|
import ThemeToggle from "$lib/components/theme-toggle.svelte";
|
|
8
8
|
import LanguageSwitch from "$lib/components/language-switch.svelte";
|
|
9
9
|
import { Toaster } from "$lib/components/ui/sonner";
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
</script>
|
|
28
28
|
|
|
29
29
|
<Sidebar.Provider>
|
|
30
|
-
<
|
|
30
|
+
<AdminSidebar user={data.user} capabilities={data.capabilities} />
|
|
31
31
|
<Sidebar.Inset>
|
|
32
32
|
{#if data.impersonating}
|
|
33
33
|
<div class="bg-primary text-primary-foreground flex items-center justify-between gap-2 px-4 py-2 text-sm">
|
|
@@ -10,7 +10,14 @@ export type AuthConfigView = {
|
|
|
10
10
|
social: Record<string, SocialProviderView>;
|
|
11
11
|
catalog: ReadonlyArray<{ id: string; label: string }>;
|
|
12
12
|
smtp: { enabled: boolean; host: string; port: number; secure: boolean; user: string; from: string; hasSecret: boolean };
|
|
13
|
-
server: {
|
|
13
|
+
server: {
|
|
14
|
+
requireEmailVerification: boolean;
|
|
15
|
+
requireSignupApproval: boolean;
|
|
16
|
+
allowDelete: boolean;
|
|
17
|
+
hibp: boolean;
|
|
18
|
+
auditLog: boolean;
|
|
19
|
+
sessionIdleTimeoutMinutes: number | null;
|
|
20
|
+
};
|
|
14
21
|
};
|
|
15
22
|
|
|
16
23
|
export const load: PageServerLoad = async ({ locals, fetch }) => {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { moduleSettingsSections } from "$lib/admin/registry.svelte";
|
|
15
15
|
import { toast } from "svelte-sonner";
|
|
16
16
|
import { api } from "$lib/api";
|
|
17
|
-
import { getI18n } from "$lib/i18n";
|
|
17
|
+
import { fmt, getI18n } from "$lib/i18n";
|
|
18
18
|
import type { Capabilities } from "@podosoft/podokit-api-client";
|
|
19
19
|
import type { AuthConfigView, SocialProviderView } from "./+page.server";
|
|
20
20
|
|
|
@@ -93,14 +93,21 @@
|
|
|
93
93
|
// ── Dialogs. Secret fields are write-only; leaving them blank keeps the stored
|
|
94
94
|
// secret. The Social login dialog manages every provider (list ⇄ add/edit
|
|
95
95
|
// form) in one popup. ──
|
|
96
|
-
let openDialog = $state<"social" | "smtp" | null>(null);
|
|
96
|
+
let openDialog = $state<"social" | "smtp" | "sessionIdle" | null>(null);
|
|
97
97
|
let socialView = $state<"list" | "form">("list");
|
|
98
98
|
let socialForm = $state({ id: "", clientId: "", clientSecret: "", redirectURI: "", enabled: true, adding: false });
|
|
99
99
|
let smtp = $state({ enabled: false, host: "", port: 587, secure: false, user: "", from: "", pass: "" });
|
|
100
|
+
let sessionIdleTimeoutMinutes = $state(30);
|
|
100
101
|
$effect(() => {
|
|
101
102
|
if (!ac) return;
|
|
102
103
|
smtp = { enabled: ac.smtp.enabled, host: ac.smtp.host, port: ac.smtp.port, secure: ac.smtp.secure, user: ac.smtp.user, from: ac.smtp.from, pass: "" };
|
|
104
|
+
sessionIdleTimeoutMinutes = ac.server.sessionIdleTimeoutMinutes ?? 30;
|
|
103
105
|
});
|
|
106
|
+
const sessionIdleTimeoutValid = $derived(
|
|
107
|
+
Number.isInteger(Number(sessionIdleTimeoutMinutes))
|
|
108
|
+
&& Number(sessionIdleTimeoutMinutes) >= 5
|
|
109
|
+
&& Number(sessionIdleTimeoutMinutes) <= 10_080,
|
|
110
|
+
);
|
|
104
111
|
const editingView = $derived(socialForm.adding ? undefined : ac?.social[socialForm.id]);
|
|
105
112
|
|
|
106
113
|
function socialRedirectURI(): string {
|
|
@@ -142,6 +149,22 @@
|
|
|
142
149
|
const done = await put({ smtp: { enabled: smtp.enabled, host: smtp.host, port: Number(smtp.port), secure: smtp.secure, user: smtp.user, from: smtp.from, ...(smtp.pass ? { pass: smtp.pass } : {}) } });
|
|
143
150
|
if (done) openDialog = null;
|
|
144
151
|
}
|
|
152
|
+
async function toggleSessionIdle(enabled: boolean): Promise<void> {
|
|
153
|
+
await put({
|
|
154
|
+
server: {
|
|
155
|
+
sessionIdleTimeoutMinutes: enabled
|
|
156
|
+
? (ac?.server.sessionIdleTimeoutMinutes ?? 30)
|
|
157
|
+
: null,
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
async function saveSessionIdleTimeout(): Promise<void> {
|
|
162
|
+
if (!sessionIdleTimeoutValid) return;
|
|
163
|
+
const done = await put({
|
|
164
|
+
server: { sessionIdleTimeoutMinutes: Number(sessionIdleTimeoutMinutes) },
|
|
165
|
+
});
|
|
166
|
+
if (done) openDialog = null;
|
|
167
|
+
}
|
|
145
168
|
|
|
146
169
|
// ── Card builders (uniform Item shape) ──
|
|
147
170
|
const flagCard = (flag: EditableFlag, name: string, desc: string): Item => ({
|
|
@@ -199,6 +222,28 @@
|
|
|
199
222
|
// Only meaningful when 2FA itself is on; disabled otherwise.
|
|
200
223
|
toggle: { checked: data.require2fa, disabled: saving || !caps.twoFactor, onChange: (v) => toggleFlag("require2fa", v) },
|
|
201
224
|
},
|
|
225
|
+
{
|
|
226
|
+
key: "session-idle-timeout",
|
|
227
|
+
name: i18n.t.settings.autoLogout,
|
|
228
|
+
desc: i18n.t.settings.autoLogoutDesc,
|
|
229
|
+
status: ac?.server.sessionIdleTimeoutMinutes
|
|
230
|
+
? {
|
|
231
|
+
on: true,
|
|
232
|
+
label: fmt(i18n.t.settings.autoLogoutMinutes, {
|
|
233
|
+
minutes: ac.server.sessionIdleTimeoutMinutes,
|
|
234
|
+
}),
|
|
235
|
+
}
|
|
236
|
+
: { on: false, label: i18n.t.settings.disabled },
|
|
237
|
+
toggle: {
|
|
238
|
+
checked: ac?.server.sessionIdleTimeoutMinutes !== null && ac?.server.sessionIdleTimeoutMinutes !== undefined,
|
|
239
|
+
disabled: saving || !ac,
|
|
240
|
+
onChange: toggleSessionIdle,
|
|
241
|
+
},
|
|
242
|
+
configure: {
|
|
243
|
+
label: i18n.t.settings.configure,
|
|
244
|
+
onClick: () => (openDialog = "sessionIdle"),
|
|
245
|
+
},
|
|
246
|
+
},
|
|
202
247
|
serverCard("hibp", i18n.t.settings.breachCheck, i18n.t.settings.breachCheckDesc, caps.passwordBreachCheck),
|
|
203
248
|
serverCard("allowDelete", i18n.t.settings.accountDeletion, i18n.t.settings.accountDeletionDesc, caps.deleteAccount),
|
|
204
249
|
serverCard("auditLog", i18n.t.settings.auditLog, i18n.t.settings.auditLogDesc, caps.auditLog),
|
|
@@ -352,6 +397,32 @@
|
|
|
352
397
|
</Dialog.Content>
|
|
353
398
|
</Dialog.Root>
|
|
354
399
|
|
|
400
|
+
<!-- Sliding session expiration — enforced by better-auth and mirrored by the browser timer. -->
|
|
401
|
+
<Dialog.Root open={openDialog === "sessionIdle"} onOpenChange={(v) => { if (!v) openDialog = null; }}>
|
|
402
|
+
<Dialog.Content>
|
|
403
|
+
<Dialog.Header>
|
|
404
|
+
<Dialog.Title>{i18n.t.settings.autoLogout}</Dialog.Title>
|
|
405
|
+
<Dialog.Description>{i18n.t.settings.autoLogoutDialogDesc}</Dialog.Description>
|
|
406
|
+
</Dialog.Header>
|
|
407
|
+
<div class="flex flex-col gap-2">
|
|
408
|
+
<Label for="session-idle-timeout">{i18n.t.settings.autoLogoutField}</Label>
|
|
409
|
+
<Input
|
|
410
|
+
id="session-idle-timeout"
|
|
411
|
+
type="number"
|
|
412
|
+
min="5"
|
|
413
|
+
max="10080"
|
|
414
|
+
step="1"
|
|
415
|
+
bind:value={sessionIdleTimeoutMinutes}
|
|
416
|
+
/>
|
|
417
|
+
<p class="text-muted-foreground text-xs">{i18n.t.settings.autoLogoutHint}</p>
|
|
418
|
+
</div>
|
|
419
|
+
<Dialog.Footer>
|
|
420
|
+
<Button variant="outline" onclick={() => (openDialog = null)}>{i18n.t.settings.cancel}</Button>
|
|
421
|
+
<Button disabled={saving || !sessionIdleTimeoutValid} onclick={saveSessionIdleTimeout}>{i18n.t.settings.save}</Button>
|
|
422
|
+
</Dialog.Footer>
|
|
423
|
+
</Dialog.Content>
|
|
424
|
+
</Dialog.Root>
|
|
425
|
+
|
|
355
426
|
<!-- SMTP (email delivery) dialog — reached from the email-verification card -->
|
|
356
427
|
<Dialog.Root open={openDialog === "smtp"} onOpenChange={(v) => { if (!v) openDialog = null; }}>
|
|
357
428
|
<Dialog.Content>
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/+layout.server.ts
CHANGED
|
@@ -1,51 +1,6 @@
|
|
|
1
|
-
import { redirect } from "@sveltejs/kit";
|
|
2
1
|
import type { LayoutServerLoad } from "./$types";
|
|
3
|
-
import
|
|
4
|
-
import { requireBackendAvailable } from "$lib/server/guards";
|
|
2
|
+
import { loadProtectedLayout } from "$lib/server/protected-layout";
|
|
5
3
|
|
|
6
4
|
export const load: LayoutServerLoad = async ({ locals, fetch }) => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
let capabilities: Capabilities = {
|
|
10
|
-
twoFactor: false,
|
|
11
|
-
providers: [],
|
|
12
|
-
deleteAccount: false,
|
|
13
|
-
auditLog: false,
|
|
14
|
-
emailVerification: false,
|
|
15
|
-
signupApprovalRequired: false,
|
|
16
|
-
passwordBreachCheck: false,
|
|
17
|
-
magicLink: false,
|
|
18
|
-
emailOtp: false,
|
|
19
|
-
username: false,
|
|
20
|
-
multiSession: false,
|
|
21
|
-
phoneNumber: false,
|
|
22
|
-
apiKey: false,
|
|
23
|
-
passkey: false,
|
|
24
|
-
organization: false,
|
|
25
|
-
oidcProvider: false,
|
|
26
|
-
roles: ["admin", "user"],
|
|
27
|
-
};
|
|
28
|
-
try {
|
|
29
|
-
const res = await fetch("/api/account/capabilities");
|
|
30
|
-
if (res.ok) capabilities = (await res.json()) as Capabilities;
|
|
31
|
-
} catch {
|
|
32
|
-
/* keep defaults */
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Require-two-factor policy: a signed-in user who hasn't enrolled is sent to the
|
|
36
|
-
// enrolment page until they do (the API's TwoFactorRequiredGuard is the real
|
|
37
|
-
// boundary; this is the UX). /setup-2fa lives outside this layout, so no loop.
|
|
38
|
-
const user = locals.user as (App.Locals["user"] & { twoFactorEnabled?: boolean }) | null;
|
|
39
|
-
let mustEnrol = false;
|
|
40
|
-
if (user && !user.twoFactorEnabled) {
|
|
41
|
-
try {
|
|
42
|
-
const res = await fetch("/api/account/require-2fa");
|
|
43
|
-
mustEnrol = res.ok && ((await res.json()) as { require2fa?: boolean }).require2fa === true;
|
|
44
|
-
} catch {
|
|
45
|
-
/* don't block the app if the policy check fails */
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (mustEnrol) redirect(302, "/setup-2fa");
|
|
49
|
-
|
|
50
|
-
return { user: locals.user, impersonating: !!locals.session?.impersonatedBy, capabilities };
|
|
5
|
+
return loadProtectedLayout({ locals, fetch });
|
|
51
6
|
};
|
|
@@ -6,12 +6,13 @@ import { SIGNUP_APPROVAL_REQUIRED, type Capabilities } from "@podosoft/podokit-a
|
|
|
6
6
|
// (magic link, ...) the server actually enabled.
|
|
7
7
|
export const load: PageServerLoad = async ({ fetch, url }) => {
|
|
8
8
|
const oauthError = url.searchParams.get("error");
|
|
9
|
+
const idleLogout = url.searchParams.get("reason") === "idle";
|
|
9
10
|
if (oauthError === SIGNUP_APPROVAL_REQUIRED) redirect(302, "/pending-approval");
|
|
10
11
|
try {
|
|
11
12
|
const res = await fetch("/api/account/capabilities");
|
|
12
|
-
if (res.ok) return { capabilities: (await res.json()) as Capabilities, oauthError };
|
|
13
|
+
if (res.ok) return { capabilities: (await res.json()) as Capabilities, oauthError, idleLogout };
|
|
13
14
|
} catch {
|
|
14
15
|
/* fall through to defaults */
|
|
15
16
|
}
|
|
16
|
-
return { capabilities: null, oauthError };
|
|
17
|
+
return { capabilities: null, oauthError, idleLogout };
|
|
17
18
|
};
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.svelte
CHANGED
|
@@ -226,6 +226,11 @@
|
|
|
226
226
|
<Card.Description>{i18n.t.auth.signInDesc}</Card.Description>
|
|
227
227
|
</Card.Header>
|
|
228
228
|
<Card.Content>
|
|
229
|
+
{#if data.idleLogout}
|
|
230
|
+
<Alert.Root data-testid="session-timeout-message" class="mb-4">
|
|
231
|
+
<Alert.Description>{i18n.t.auth.idleLogout}</Alert.Description>
|
|
232
|
+
</Alert.Root>
|
|
233
|
+
{/if}
|
|
229
234
|
{#if error}
|
|
230
235
|
<Alert.Root data-testid="auth-error" variant="destructive" class="mb-4">
|
|
231
236
|
<Alert.Description>
|
|
@@ -3,6 +3,7 @@ import { redirect } from "@sveltejs/kit";
|
|
|
3
3
|
import { safeAuthRedirect } from "$lib/auth-redirect";
|
|
4
4
|
import { loadMessages } from "$lib/i18n/messages";
|
|
5
5
|
import { isPublicPath, requireBackendAvailable } from "$lib/server/guards";
|
|
6
|
+
import type { Capabilities } from "@podosoft/podokit-api-client";
|
|
6
7
|
|
|
7
8
|
// "/" is a public landing page. Auth pages return signed-in users to their safe
|
|
8
9
|
// requested page, and the admin area requires a session. /maintenance is public
|
|
@@ -42,6 +43,19 @@ export const load: LayoutServerLoad = async (event) => {
|
|
|
42
43
|
if (!locals.user && !isPublicPath(pathname)) {
|
|
43
44
|
redirect(303, `/login?redirect=${encodeURIComponent(`${pathname}${url.search}`)}`);
|
|
44
45
|
}
|
|
46
|
+
let sessionIdleTimeoutMinutes: number | null = null;
|
|
47
|
+
if (locals.user) {
|
|
48
|
+
try {
|
|
49
|
+
const response = await event.fetch("/api/account/capabilities");
|
|
50
|
+
if (response.ok) {
|
|
51
|
+
const capabilities = (await response.json()) as Capabilities;
|
|
52
|
+
sessionIdleTimeoutMinutes = capabilities.sessionIdleTimeoutMinutes ?? null;
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// Keep automatic logout disabled in the browser if policy lookup fails.
|
|
56
|
+
// The server-side session expiration remains authoritative.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
45
59
|
const messages = await loadMessages(locals.locale, site?.locale);
|
|
46
|
-
return { user: locals.user, locale: locals.locale, messages, site };
|
|
60
|
+
return { user: locals.user, locale: locals.locale, messages, site, sessionIdleTimeoutMinutes };
|
|
47
61
|
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { page } from "$app/state";
|
|
3
|
+
import AccountMenu from "$lib/components/account-menu.svelte";
|
|
2
4
|
import { Button } from "$lib/components/ui/button";
|
|
3
5
|
import LanguageSwitch from "$lib/components/language-switch.svelte";
|
|
4
6
|
import ThemeToggle from "$lib/components/theme-toggle.svelte";
|
|
@@ -6,12 +8,14 @@
|
|
|
6
8
|
import { getI18n } from "$lib/i18n";
|
|
7
9
|
|
|
8
10
|
const i18n = getI18n();
|
|
11
|
+
const user = $derived(page.data.user ?? null);
|
|
9
12
|
</script>
|
|
10
13
|
|
|
11
14
|
<div class="flex min-h-svh flex-col">
|
|
12
15
|
<header class="flex items-center justify-end gap-2 p-4">
|
|
13
16
|
<LanguageSwitch />
|
|
14
17
|
<ThemeToggle />
|
|
18
|
+
<AccountMenu {user} />
|
|
15
19
|
</header>
|
|
16
20
|
<main class="flex flex-1 flex-col items-center justify-center gap-6 px-4 text-center">
|
|
17
21
|
<div class="flex flex-col gap-3">
|
|
@@ -16,7 +16,7 @@ async function adminCtx(playwright: import("@playwright/test").PlaywrightWorkerA
|
|
|
16
16
|
return ctx;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
test("admin can enable Google OAuth from the DB and it applies without a restart
|
|
19
|
+
test("admin can enable Google OAuth from the DB and it applies without a restart", async ({ playwright }) => {
|
|
20
20
|
const admin = await adminCtx(playwright);
|
|
21
21
|
await admin.put("/api/account/auth-config", { data: { social: { google: { enabled: false } } } }); // clean start
|
|
22
22
|
const anon = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
@@ -50,7 +50,7 @@ test("admin can enable Google OAuth from the DB and it applies without a restart
|
|
|
50
50
|
await admin.dispose();
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
test("admin can add and remove a social provider dynamically
|
|
53
|
+
test("admin can add and remove a social provider dynamically", async ({ playwright }) => {
|
|
54
54
|
const admin = await adminCtx(playwright);
|
|
55
55
|
// The catalog offers providers beyond the two legacy env-backed ones.
|
|
56
56
|
const catalog = (await (await admin.get("/api/account/auth-config")).json()).catalog as Array<{ id: string }>;
|
|
@@ -83,7 +83,47 @@ test("auth-config is admin-only and never leaks secrets @smoke", async ({ playwr
|
|
|
83
83
|
await user.dispose();
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
test("
|
|
86
|
+
test("automatic logout validates and applies a sliding session lifetime", async ({ playwright }) => {
|
|
87
|
+
const admin = await adminCtx(playwright);
|
|
88
|
+
const freshAdmin = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
expect(
|
|
92
|
+
(await admin.put("/api/account/auth-config", {
|
|
93
|
+
data: { server: { sessionIdleTimeoutMinutes: 4 } },
|
|
94
|
+
})).status(),
|
|
95
|
+
).toBe(400);
|
|
96
|
+
|
|
97
|
+
const enabled = await admin.put("/api/account/auth-config", {
|
|
98
|
+
data: { server: { sessionIdleTimeoutMinutes: 30 } },
|
|
99
|
+
});
|
|
100
|
+
expect(enabled.ok()).toBeTruthy();
|
|
101
|
+
expect((await enabled.json()).server.sessionIdleTimeoutMinutes).toBe(30);
|
|
102
|
+
|
|
103
|
+
const caps = await (await admin.get("/api/account/capabilities")).json();
|
|
104
|
+
expect(caps.sessionIdleTimeoutMinutes).toBe(30);
|
|
105
|
+
|
|
106
|
+
const signIn = await freshAdmin.post("/api/auth/sign-in/email", {
|
|
107
|
+
data: { email: ADMIN.email, password: ADMIN.password },
|
|
108
|
+
});
|
|
109
|
+
expect(signIn.ok()).toBeTruthy();
|
|
110
|
+
expect(signIn.headers()["set-cookie"]).toMatch(/Max-Age=1800(?:;|,)/i);
|
|
111
|
+
|
|
112
|
+
const sessions = (await (await admin.get("/api/auth/list-sessions")).json()) as Array<{
|
|
113
|
+
expiresAt: string;
|
|
114
|
+
}>;
|
|
115
|
+
const latestExpiry = Math.max(...sessions.map((session) => new Date(session.expiresAt).getTime()));
|
|
116
|
+
expect(latestExpiry).toBeLessThanOrEqual(Date.now() + 30 * 60_000 + 10_000);
|
|
117
|
+
} finally {
|
|
118
|
+
await admin.put("/api/account/auth-config", {
|
|
119
|
+
data: { server: { sessionIdleTimeoutMinutes: null } },
|
|
120
|
+
});
|
|
121
|
+
await freshAdmin.dispose();
|
|
122
|
+
await admin.dispose();
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("new registrations require approval when the policy is enabled", async ({ playwright }) => {
|
|
87
127
|
const admin = await adminCtx(playwright);
|
|
88
128
|
const anon = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
89
129
|
const password = "Podokit3e-Str0ng!pw";
|
|
@@ -162,7 +202,7 @@ test("new registrations require approval when the policy is enabled @smoke", asy
|
|
|
162
202
|
}
|
|
163
203
|
});
|
|
164
204
|
|
|
165
|
-
test("closed public sign-up blocks new OAuth users but preserves existing sign-in
|
|
205
|
+
test("closed public sign-up blocks new OAuth users but preserves existing sign-in", async ({ playwright }) => {
|
|
166
206
|
const admin = await adminCtx(playwright);
|
|
167
207
|
const anon = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
168
208
|
const password = "Podokit3e-Str0ng!pw";
|
|
@@ -12,7 +12,7 @@ test("account/me is 401 when unauthenticated @smoke", async ({ request }) => {
|
|
|
12
12
|
expect(res.status()).toBe(401);
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
test("rejects a breached password on sign-up
|
|
15
|
+
test("rejects a breached password on sign-up", async ({ playwright }) => {
|
|
16
16
|
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
17
17
|
await ctx.post("/api/auth/sign-in/email", { data: { email: ADMIN.email, password: ADMIN.password } });
|
|
18
18
|
const caps = await (await ctx.get("/api/account/capabilities")).json();
|
|
@@ -25,7 +25,7 @@ test("rejects a breached password on sign-up @smoke", async ({ playwright }) =>
|
|
|
25
25
|
await ctx.dispose();
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
test("phone number: verify with the code delivered to the SMS sink
|
|
28
|
+
test("phone number: verify with the code delivered to the SMS sink", async ({ playwright }) => {
|
|
29
29
|
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
30
30
|
const caps = await (await ctx.get("/api/account/capabilities")).json();
|
|
31
31
|
test.skip(!caps?.phoneNumber, "phone number not enabled");
|
|
@@ -74,7 +74,7 @@ test("two-factor: enable with a real TOTP code and require it at sign-in @smoke"
|
|
|
74
74
|
await ctx2.dispose();
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
-
test("two-factor: a backup code completes sign-in and is single-use
|
|
77
|
+
test("two-factor: a backup code completes sign-in and is single-use", async ({ playwright }) => {
|
|
78
78
|
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
79
79
|
const caps = await (await ctx.get("/api/account/capabilities")).json();
|
|
80
80
|
test.skip(!caps?.twoFactor, "two-factor not enabled");
|
|
@@ -110,7 +110,7 @@ test("two-factor: a backup code completes sign-in and is single-use @smoke", asy
|
|
|
110
110
|
await ctx3.dispose();
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
-
test("two-factor: regenerating backup codes invalidates the old set
|
|
113
|
+
test("two-factor: regenerating backup codes invalidates the old set", async ({ playwright }) => {
|
|
114
114
|
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
115
115
|
const caps = await (await ctx.get("/api/account/capabilities")).json();
|
|
116
116
|
test.skip(!caps?.twoFactor, "two-factor not enabled");
|
|
@@ -142,7 +142,7 @@ test("two-factor: regenerating backup codes invalidates the old set @smoke", asy
|
|
|
142
142
|
await ctxNew.dispose();
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
-
test("require-2fa: gates an un-enrolled user until they enrol
|
|
145
|
+
test("require-2fa: gates an un-enrolled user until they enrol", async ({ playwright }) => {
|
|
146
146
|
const admin = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
147
147
|
const caps = await (await admin.get("/api/account/capabilities")).json();
|
|
148
148
|
test.skip(!caps?.twoFactor, "two-factor not enabled");
|
|
@@ -180,7 +180,7 @@ test("require-2fa: gates an un-enrolled user until they enrol @smoke", async ({
|
|
|
180
180
|
}
|
|
181
181
|
});
|
|
182
182
|
|
|
183
|
-
test("multi-session holds several accounts and switches between them
|
|
183
|
+
test("multi-session holds several accounts and switches between them", async ({ playwright }) => {
|
|
184
184
|
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
185
185
|
const caps = await (await ctx.get("/api/account/capabilities")).json();
|
|
186
186
|
test.skip(!caps?.multiSession, "multi-session not enabled");
|
|
@@ -201,7 +201,7 @@ test("multi-session holds several accounts and switches between them @smoke", as
|
|
|
201
201
|
await ctx.dispose();
|
|
202
202
|
});
|
|
203
203
|
|
|
204
|
-
test("a user can sign in with a username
|
|
204
|
+
test("a user can sign in with a username", async ({ playwright }) => {
|
|
205
205
|
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
206
206
|
const caps = await (await ctx.get("/api/account/capabilities")).json();
|
|
207
207
|
test.skip(!caps?.username, "username not enabled");
|
|
@@ -219,7 +219,7 @@ test("a user can sign in with a username @smoke", async ({ playwright }) => {
|
|
|
219
219
|
await ctx2.dispose();
|
|
220
220
|
});
|
|
221
221
|
|
|
222
|
-
test("bearer token authenticates API requests without a cookie
|
|
222
|
+
test("bearer token authenticates API requests without a cookie", async ({ playwright }) => {
|
|
223
223
|
const email = `bearer-${Date.now()}@example.com`;
|
|
224
224
|
const pw = "Podokit3e-Str0ng!pw";
|
|
225
225
|
const signup = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
@@ -241,7 +241,7 @@ test("bearer token authenticates API requests without a cookie @smoke", async ({
|
|
|
241
241
|
await api.dispose();
|
|
242
242
|
});
|
|
243
243
|
|
|
244
|
-
test("a user can create, list and revoke a personal API key
|
|
244
|
+
test("a user can create, list and revoke a personal API key", async ({ playwright }) => {
|
|
245
245
|
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
246
246
|
const caps = await (await ctx.get("/api/account/capabilities")).json();
|
|
247
247
|
test.skip(!caps?.apiKey, "api keys not enabled");
|
|
@@ -310,7 +310,7 @@ test("a user can create, list and delete an organization @smoke", async ({ playw
|
|
|
310
310
|
await ctx.dispose();
|
|
311
311
|
});
|
|
312
312
|
|
|
313
|
-
test("an organization can have a parent and manager members
|
|
313
|
+
test("an organization can have a parent and manager members", async ({ playwright }) => {
|
|
314
314
|
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
315
315
|
const caps = await (await ctx.get("/api/account/capabilities")).json();
|
|
316
316
|
test.skip(!caps?.organization, "organizations not enabled");
|