@authagonal/login 0.1.97
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/README.md +348 -0
- package/dist/App.d.ts +1 -0
- package/dist/api.d.ts +35 -0
- package/dist/branding.d.ts +22 -0
- package/dist/branding.json +8 -0
- package/dist/components/AuthLayout.d.ts +7 -0
- package/dist/components/ui/alert.d.ts +9 -0
- package/dist/components/ui/button.d.ts +11 -0
- package/dist/components/ui/card.d.ts +8 -0
- package/dist/components/ui/input.d.ts +3 -0
- package/dist/components/ui/label.d.ts +3 -0
- package/dist/components/ui/separator.d.ts +6 -0
- package/dist/favicon.svg +1 -0
- package/dist/hooks/useDarkMode.d.ts +6 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/icons.svg +24 -0
- package/dist/index.css +3 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +6332 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/main.d.ts +2 -0
- package/dist/pages/ConsentPage.d.ts +1 -0
- package/dist/pages/DevicePage.d.ts +1 -0
- package/dist/pages/ForgotPasswordPage.d.ts +1 -0
- package/dist/pages/GrantsPage.d.ts +1 -0
- package/dist/pages/LoginPage.d.ts +1 -0
- package/dist/pages/MfaChallengePage.d.ts +1 -0
- package/dist/pages/MfaSetupPage.d.ts +1 -0
- package/dist/pages/RegisterPage.d.ts +1 -0
- package/dist/pages/ResetPasswordPage.d.ts +1 -0
- package/dist/types.d.ts +91 -0
- package/index.html +13 -0
- package/package.json +65 -0
- package/public/branding.json +8 -0
- package/public/favicon.svg +1 -0
- package/public/icons.svg +24 -0
- package/src/App.tsx +32 -0
- package/src/api.ts +156 -0
- package/src/branding.ts +55 -0
- package/src/components/AuthLayout.tsx +107 -0
- package/src/components/ui/alert.tsx +31 -0
- package/src/components/ui/button.tsx +51 -0
- package/src/components/ui/card.tsx +50 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +17 -0
- package/src/components/ui/separator.tsx +16 -0
- package/src/hooks/useDarkMode.ts +39 -0
- package/src/i18n/de.json +111 -0
- package/src/i18n/en.json +136 -0
- package/src/i18n/es.json +111 -0
- package/src/i18n/fr.json +111 -0
- package/src/i18n/index.ts +39 -0
- package/src/i18n/pt.json +111 -0
- package/src/i18n/tlh.json +111 -0
- package/src/i18n/vi.json +111 -0
- package/src/i18n/zh-Hans.json +111 -0
- package/src/index.ts +44 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +19 -0
- package/src/pages/ConsentPage.tsx +144 -0
- package/src/pages/DevicePage.tsx +145 -0
- package/src/pages/ForgotPasswordPage.tsx +90 -0
- package/src/pages/GrantsPage.tsx +87 -0
- package/src/pages/LoginPage.tsx +423 -0
- package/src/pages/MfaChallengePage.tsx +246 -0
- package/src/pages/MfaSetupPage.tsx +366 -0
- package/src/pages/RegisterPage.tsx +161 -0
- package/src/pages/ResetPasswordPage.tsx +219 -0
- package/src/styles.css +33 -0
- package/src/types.ts +112 -0
- package/tsconfig.app.json +37 -0
- package/tsconfig.json +7 -0
- package/vite.config.ts +54 -0
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function ConsentPage(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function DevicePage(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function ForgotPasswordPage(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function GrantsPage(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function LoginPage(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function MfaChallengePage(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function MfaSetupPage(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function RegisterPage(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function ResetPasswordPage(): import("react/jsx-runtime").JSX.Element;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export interface LoginResponse {
|
|
2
|
+
userId: string;
|
|
3
|
+
email: string;
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ApiError {
|
|
7
|
+
error: string;
|
|
8
|
+
message?: string;
|
|
9
|
+
retryAfter?: number;
|
|
10
|
+
redirectUrl?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface SessionResponse {
|
|
13
|
+
authenticated: boolean;
|
|
14
|
+
userId: string;
|
|
15
|
+
email: string;
|
|
16
|
+
name: string;
|
|
17
|
+
}
|
|
18
|
+
export interface SsoCheckResponse {
|
|
19
|
+
ssoRequired: boolean;
|
|
20
|
+
providerType?: string;
|
|
21
|
+
connectionId?: string;
|
|
22
|
+
redirectUrl?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ExternalProvider {
|
|
25
|
+
connectionId: string;
|
|
26
|
+
name: string;
|
|
27
|
+
loginUrl: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ProvidersResponse {
|
|
30
|
+
providers: ExternalProvider[];
|
|
31
|
+
}
|
|
32
|
+
export interface PasswordPolicyRule {
|
|
33
|
+
rule: string;
|
|
34
|
+
value: number | null;
|
|
35
|
+
label: string;
|
|
36
|
+
}
|
|
37
|
+
export interface PasswordPolicyResponse {
|
|
38
|
+
rules: PasswordPolicyRule[];
|
|
39
|
+
}
|
|
40
|
+
export interface MfaLoginResponse {
|
|
41
|
+
mfaRequired?: boolean;
|
|
42
|
+
mfaSetupRequired?: boolean;
|
|
43
|
+
mfaAvailable?: boolean;
|
|
44
|
+
clientId?: string;
|
|
45
|
+
challengeId?: string;
|
|
46
|
+
setupToken?: string;
|
|
47
|
+
methods?: string[];
|
|
48
|
+
webAuthn?: PublicKeyCredentialRequestOptionsJSON;
|
|
49
|
+
userId?: string;
|
|
50
|
+
email?: string;
|
|
51
|
+
name?: string;
|
|
52
|
+
}
|
|
53
|
+
export type PublicKeyCredentialRequestOptionsJSON = any;
|
|
54
|
+
export type PublicKeyCredentialCreationOptionsJSON = any;
|
|
55
|
+
export interface MfaVerifyResponse {
|
|
56
|
+
userId: string;
|
|
57
|
+
email: string;
|
|
58
|
+
name: string;
|
|
59
|
+
}
|
|
60
|
+
export interface MfaStatusResponse {
|
|
61
|
+
enabled: boolean;
|
|
62
|
+
methods: MfaMethod[];
|
|
63
|
+
}
|
|
64
|
+
export interface MfaMethod {
|
|
65
|
+
id: string;
|
|
66
|
+
type: string;
|
|
67
|
+
name: string;
|
|
68
|
+
createdAt: string;
|
|
69
|
+
lastUsedAt: string | null;
|
|
70
|
+
isConsumed?: boolean | null;
|
|
71
|
+
}
|
|
72
|
+
export interface MfaTotpSetupResponse {
|
|
73
|
+
setupToken: string;
|
|
74
|
+
qrCodeDataUri: string;
|
|
75
|
+
manualKey: string;
|
|
76
|
+
}
|
|
77
|
+
export interface MfaRecoveryGenerateResponse {
|
|
78
|
+
codes: string[];
|
|
79
|
+
}
|
|
80
|
+
export interface MfaWebAuthnSetupResponse {
|
|
81
|
+
setupToken: string;
|
|
82
|
+
options: PublicKeyCredentialCreationOptionsJSON;
|
|
83
|
+
}
|
|
84
|
+
export interface MfaWebAuthnConfirmResponse {
|
|
85
|
+
success: boolean;
|
|
86
|
+
credentialId: string;
|
|
87
|
+
}
|
|
88
|
+
export interface RegisterResponse {
|
|
89
|
+
success: boolean;
|
|
90
|
+
userId: string;
|
|
91
|
+
}
|
package/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>Sign In — Authagonal</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@authagonal/login",
|
|
3
|
+
"version": "0.1.97",
|
|
4
|
+
"description": "Default login UI for Authagonal — runtime-configurable via branding.json",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/authagonal/authagonal.git",
|
|
10
|
+
"directory": "login-app"
|
|
11
|
+
},
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./styles.css": "./dist/index.css"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"src",
|
|
24
|
+
"public",
|
|
25
|
+
"index.html",
|
|
26
|
+
"vite.config.ts",
|
|
27
|
+
"tsconfig.json",
|
|
28
|
+
"tsconfig.app.json"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"dev": "vite",
|
|
32
|
+
"build": "vite build && tsc -b",
|
|
33
|
+
"build:spa": "vite build --config vite.spa.config.ts",
|
|
34
|
+
"lint": "eslint .",
|
|
35
|
+
"preview": "vite preview"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"class-variance-authority": "^0.7.1",
|
|
39
|
+
"clsx": "^2.1.1",
|
|
40
|
+
"i18next": "^25.1.3",
|
|
41
|
+
"i18next-browser-languagedetector": "^8.0.6",
|
|
42
|
+
"lucide-react": "^1.7.0",
|
|
43
|
+
"react": "^19.2.4",
|
|
44
|
+
"react-dom": "^19.2.4",
|
|
45
|
+
"react-i18next": "^15.5.2",
|
|
46
|
+
"react-router-dom": "^7.13.2",
|
|
47
|
+
"tailwind-merge": "^3.5.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@eslint/js": "^9.39.4",
|
|
51
|
+
"@tailwindcss/vite": "^4.2.2",
|
|
52
|
+
"@types/node": "^24.12.0",
|
|
53
|
+
"@types/react": "^19.2.14",
|
|
54
|
+
"@types/react-dom": "^19.2.3",
|
|
55
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
56
|
+
"eslint": "^9.39.4",
|
|
57
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
58
|
+
"eslint-plugin-react-refresh": "^0.5.2",
|
|
59
|
+
"globals": "^17.4.0",
|
|
60
|
+
"tailwindcss": "^4.2.2",
|
|
61
|
+
"typescript": "~5.9.3",
|
|
62
|
+
"typescript-eslint": "^8.57.0",
|
|
63
|
+
"vite": "^8.0.1"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
|
package/public/icons.svg
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
|
3
|
+
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
|
4
|
+
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
|
5
|
+
</symbol>
|
|
6
|
+
<symbol id="discord-icon" viewBox="0 0 20 19">
|
|
7
|
+
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
|
8
|
+
</symbol>
|
|
9
|
+
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
|
10
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
|
11
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
|
12
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
|
13
|
+
</symbol>
|
|
14
|
+
<symbol id="github-icon" viewBox="0 0 19 19">
|
|
15
|
+
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
|
16
|
+
</symbol>
|
|
17
|
+
<symbol id="social-icon" viewBox="0 0 20 20">
|
|
18
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
|
19
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
|
20
|
+
</symbol>
|
|
21
|
+
<symbol id="x-icon" viewBox="0 0 19 19">
|
|
22
|
+
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
|
23
|
+
</symbol>
|
|
24
|
+
</svg>
|
package/src/App.tsx
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
|
2
|
+
import AuthLayout from './components/AuthLayout';
|
|
3
|
+
import LoginPage from './pages/LoginPage';
|
|
4
|
+
import RegisterPage from './pages/RegisterPage';
|
|
5
|
+
import ForgotPasswordPage from './pages/ForgotPasswordPage';
|
|
6
|
+
import ResetPasswordPage from './pages/ResetPasswordPage';
|
|
7
|
+
import MfaChallengePage from './pages/MfaChallengePage';
|
|
8
|
+
import MfaSetupPage from './pages/MfaSetupPage';
|
|
9
|
+
import DevicePage from './pages/DevicePage';
|
|
10
|
+
import ConsentPage from './pages/ConsentPage';
|
|
11
|
+
import GrantsPage from './pages/GrantsPage';
|
|
12
|
+
|
|
13
|
+
export default function App() {
|
|
14
|
+
return (
|
|
15
|
+
<BrowserRouter>
|
|
16
|
+
<AuthLayout>
|
|
17
|
+
<Routes>
|
|
18
|
+
<Route path="/login" element={<LoginPage />} />
|
|
19
|
+
<Route path="/register" element={<RegisterPage />} />
|
|
20
|
+
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
|
|
21
|
+
<Route path="/reset-password" element={<ResetPasswordPage />} />
|
|
22
|
+
<Route path="/mfa-challenge" element={<MfaChallengePage />} />
|
|
23
|
+
<Route path="/mfa-setup" element={<MfaSetupPage />} />
|
|
24
|
+
<Route path="/device" element={<DevicePage />} />
|
|
25
|
+
<Route path="/consent" element={<ConsentPage />} />
|
|
26
|
+
<Route path="/grants" element={<GrantsPage />} />
|
|
27
|
+
<Route path="*" element={<Navigate to="/login" replace />} />
|
|
28
|
+
</Routes>
|
|
29
|
+
</AuthLayout>
|
|
30
|
+
</BrowserRouter>
|
|
31
|
+
);
|
|
32
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { ApiError, SessionResponse, SsoCheckResponse, ProvidersResponse, PasswordPolicyResponse, MfaLoginResponse, MfaVerifyResponse, MfaStatusResponse, MfaTotpSetupResponse, MfaRecoveryGenerateResponse, MfaWebAuthnSetupResponse, MfaWebAuthnConfirmResponse, RegisterResponse } from './types';
|
|
2
|
+
|
|
3
|
+
const API_URL = import.meta.env.VITE_API_URL || '';
|
|
4
|
+
|
|
5
|
+
class ApiRequestError extends Error {
|
|
6
|
+
public error: string;
|
|
7
|
+
public retryAfter?: number;
|
|
8
|
+
public redirectUrl?: string;
|
|
9
|
+
|
|
10
|
+
constructor(apiError: ApiError) {
|
|
11
|
+
super(apiError.message || apiError.error);
|
|
12
|
+
this.name = 'ApiRequestError';
|
|
13
|
+
this.error = apiError.error;
|
|
14
|
+
this.retryAfter = apiError.retryAfter;
|
|
15
|
+
this.redirectUrl = apiError.redirectUrl;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function api<T>(path: string, options?: RequestInit): Promise<T> {
|
|
20
|
+
const url = `${API_URL}${path}`;
|
|
21
|
+
|
|
22
|
+
const response = await fetch(url, {
|
|
23
|
+
...options,
|
|
24
|
+
credentials: 'include',
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
...options?.headers,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
let apiError: ApiError;
|
|
33
|
+
try {
|
|
34
|
+
apiError = await response.json() as ApiError;
|
|
35
|
+
} catch {
|
|
36
|
+
apiError = { error: 'unknown_error', message: `Request failed with status ${response.status}` };
|
|
37
|
+
}
|
|
38
|
+
throw new ApiRequestError(apiError);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return response.json() as Promise<T>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function setupTokenHeaders(token?: string): Record<string, string> {
|
|
45
|
+
return token ? { 'X-MFA-Setup-Token': token } : {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function login(email: string, password: string, returnUrl?: string): Promise<MfaLoginResponse> {
|
|
49
|
+
const url = returnUrl ? `/api/auth/login?returnUrl=${encodeURIComponent(returnUrl)}` : '/api/auth/login';
|
|
50
|
+
return api<MfaLoginResponse>(url, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
body: JSON.stringify({ email, password }),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function register(email: string, password: string, firstName?: string, lastName?: string): Promise<RegisterResponse> {
|
|
57
|
+
return api<RegisterResponse>('/api/auth/register', {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
body: JSON.stringify({ email, password, firstName, lastName }),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function logout(): Promise<{ success: true }> {
|
|
64
|
+
return api<{ success: true }>('/api/auth/logout', {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function forgotPassword(email: string): Promise<{ success: true }> {
|
|
70
|
+
return api<{ success: true }>('/api/auth/forgot-password', {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
body: JSON.stringify({ email }),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function resetPassword(token: string, newPassword: string): Promise<{ success: true }> {
|
|
77
|
+
return api<{ success: true }>('/api/auth/reset-password', {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
body: JSON.stringify({ token, newPassword }),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function getSession(): Promise<SessionResponse> {
|
|
84
|
+
return api<SessionResponse>('/api/auth/session');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function ssoCheck(email: string): Promise<SsoCheckResponse> {
|
|
88
|
+
return api<SsoCheckResponse>(`/api/auth/sso-check?email=${encodeURIComponent(email)}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getProviders(): Promise<ProvidersResponse> {
|
|
92
|
+
return api<ProvidersResponse>('/api/auth/providers');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function getPasswordPolicy(): Promise<PasswordPolicyResponse> {
|
|
96
|
+
return api<PasswordPolicyResponse>('/api/auth/password-policy');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function mfaVerify(challengeId: string, method: string, code?: string, assertion?: string): Promise<MfaVerifyResponse> {
|
|
100
|
+
return api<MfaVerifyResponse>('/api/auth/mfa/verify', {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
body: JSON.stringify({ challengeId, method, code, assertion }),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function mfaStatus(mfaSetupToken?: string): Promise<MfaStatusResponse> {
|
|
107
|
+
return api<MfaStatusResponse>('/api/auth/mfa/status', {
|
|
108
|
+
headers: setupTokenHeaders(mfaSetupToken),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function mfaTotpSetup(mfaSetupToken?: string): Promise<MfaTotpSetupResponse> {
|
|
113
|
+
return api<MfaTotpSetupResponse>('/api/auth/mfa/totp/setup', {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: setupTokenHeaders(mfaSetupToken),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function mfaTotpConfirm(setupToken: string, code: string, mfaSetupToken?: string): Promise<{ success: true }> {
|
|
120
|
+
return api<{ success: true }>('/api/auth/mfa/totp/confirm', {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
body: JSON.stringify({ setupToken, code }),
|
|
123
|
+
headers: setupTokenHeaders(mfaSetupToken),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function mfaRecoveryGenerate(mfaSetupToken?: string): Promise<MfaRecoveryGenerateResponse> {
|
|
128
|
+
return api<MfaRecoveryGenerateResponse>('/api/auth/mfa/recovery/generate', {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: setupTokenHeaders(mfaSetupToken),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function mfaWebAuthnSetup(mfaSetupToken?: string): Promise<MfaWebAuthnSetupResponse> {
|
|
135
|
+
return api<MfaWebAuthnSetupResponse>('/api/auth/mfa/webauthn/setup', {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: setupTokenHeaders(mfaSetupToken),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function mfaWebAuthnConfirm(setupToken: string, attestationResponse: string, mfaSetupToken?: string): Promise<MfaWebAuthnConfirmResponse> {
|
|
142
|
+
return api<MfaWebAuthnConfirmResponse>('/api/auth/mfa/webauthn/confirm', {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
body: JSON.stringify({ setupToken, attestationResponse }),
|
|
145
|
+
headers: setupTokenHeaders(mfaSetupToken),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function mfaDeleteCredential(credentialId: string, mfaSetupToken?: string): Promise<{ success: true }> {
|
|
150
|
+
return api<{ success: true }>(`/api/auth/mfa/credentials/${credentialId}`, {
|
|
151
|
+
method: 'DELETE',
|
|
152
|
+
headers: setupTokenHeaders(mfaSetupToken),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export { ApiRequestError };
|
package/src/branding.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
/** A localizable string — either a plain string or an object mapping language codes to strings. */
|
|
4
|
+
export type LocalizedString = string | Record<string, string> | null;
|
|
5
|
+
|
|
6
|
+
export interface BrandingConfig {
|
|
7
|
+
appName: string;
|
|
8
|
+
logoUrl: string | null;
|
|
9
|
+
primaryColor: string;
|
|
10
|
+
supportEmail: string | null;
|
|
11
|
+
showForgotPassword: boolean;
|
|
12
|
+
showRegistration: boolean;
|
|
13
|
+
customCssUrl: string | null;
|
|
14
|
+
welcomeTitle: LocalizedString;
|
|
15
|
+
welcomeSubtitle: LocalizedString;
|
|
16
|
+
languages: { code: string; label: string }[] | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const defaults: BrandingConfig = {
|
|
20
|
+
appName: 'Authagonal',
|
|
21
|
+
logoUrl: null,
|
|
22
|
+
primaryColor: '#2563eb',
|
|
23
|
+
supportEmail: null,
|
|
24
|
+
showForgotPassword: true,
|
|
25
|
+
showRegistration: false,
|
|
26
|
+
customCssUrl: null,
|
|
27
|
+
welcomeTitle: null,
|
|
28
|
+
welcomeSubtitle: null,
|
|
29
|
+
languages: null,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export async function loadBranding(): Promise<BrandingConfig> {
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch('/branding.json');
|
|
35
|
+
if (!response.ok) return defaults;
|
|
36
|
+
const json = await response.json();
|
|
37
|
+
return { ...defaults, ...json };
|
|
38
|
+
} catch {
|
|
39
|
+
return defaults;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const BrandingContext = createContext<BrandingConfig>(defaults);
|
|
44
|
+
|
|
45
|
+
export function useBranding(): BrandingConfig {
|
|
46
|
+
return useContext(BrandingContext);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Resolve a LocalizedString to a concrete string for the given language, or null if not set. */
|
|
50
|
+
export function resolveLocalized(value: LocalizedString, language: string): string | null {
|
|
51
|
+
if (value == null) return null;
|
|
52
|
+
if (typeof value === 'string') return value;
|
|
53
|
+
// Try exact match, then base language (e.g. "en" from "en-US"), then first available
|
|
54
|
+
return value[language] ?? value[language.split('-')[0]] ?? Object.values(value)[0] ?? null;
|
|
55
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useEffect, type ReactNode } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { useBranding } from '../branding';
|
|
4
|
+
import { useDarkMode } from '../hooks/useDarkMode';
|
|
5
|
+
import { Card } from './ui/card';
|
|
6
|
+
import { cn } from '@/lib/utils';
|
|
7
|
+
import { Sun, Moon, Monitor } from 'lucide-react';
|
|
8
|
+
|
|
9
|
+
// Ensure i18n is initialized when AuthLayout is used (including by npm consumers)
|
|
10
|
+
import '../i18n';
|
|
11
|
+
|
|
12
|
+
interface AuthLayoutProps {
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ALL_LANGUAGES: { code: string; label: string }[] = [
|
|
17
|
+
{ code: 'en', label: 'English' },
|
|
18
|
+
{ code: 'zh-Hans', label: '中文' },
|
|
19
|
+
{ code: 'de', label: 'Deutsch' },
|
|
20
|
+
{ code: 'fr', label: 'Français' },
|
|
21
|
+
{ code: 'es', label: 'Español' },
|
|
22
|
+
{ code: 'vi', label: 'Tiếng Việt' },
|
|
23
|
+
{ code: 'pt', label: 'Português' },
|
|
24
|
+
{ code: 'tlh', label: 'tlhIngan' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function ThemeToggle() {
|
|
28
|
+
const { theme, setTheme } = useDarkMode();
|
|
29
|
+
const themes = [
|
|
30
|
+
{ value: 'light' as const, icon: Sun },
|
|
31
|
+
{ value: 'system' as const, icon: Monitor },
|
|
32
|
+
{ value: 'dark' as const, icon: Moon },
|
|
33
|
+
];
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex items-center justify-center gap-0.5 mt-3" data-auth="theme-toggle">
|
|
36
|
+
{themes.map(({ value, icon: Icon }) => (
|
|
37
|
+
<button
|
|
38
|
+
key={value}
|
|
39
|
+
type="button"
|
|
40
|
+
onClick={() => setTheme(value)}
|
|
41
|
+
className={cn(
|
|
42
|
+
'p-1 rounded cursor-pointer border-none bg-transparent transition-colors',
|
|
43
|
+
theme === value
|
|
44
|
+
? 'text-gray-700 dark:text-gray-200'
|
|
45
|
+
: 'text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300'
|
|
46
|
+
)}
|
|
47
|
+
title={value}
|
|
48
|
+
aria-label={`${value} theme`}
|
|
49
|
+
>
|
|
50
|
+
<Icon className="h-3.5 w-3.5" />
|
|
51
|
+
</button>
|
|
52
|
+
))}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default function AuthLayout({ children }: AuthLayoutProps) {
|
|
58
|
+
const branding = useBranding();
|
|
59
|
+
const { i18n } = useTranslation();
|
|
60
|
+
useDarkMode();
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
document.documentElement.style.setProperty('--brand-primary', branding.primaryColor);
|
|
64
|
+
|
|
65
|
+
if (branding.customCssUrl) {
|
|
66
|
+
const link = document.createElement('link');
|
|
67
|
+
link.rel = 'stylesheet';
|
|
68
|
+
link.href = branding.customCssUrl;
|
|
69
|
+
link.id = 'branding-css';
|
|
70
|
+
document.head.appendChild(link);
|
|
71
|
+
return () => { link.remove(); };
|
|
72
|
+
}
|
|
73
|
+
}, [branding]);
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className="min-h-screen flex items-center justify-center p-4" data-auth="page" style={{ background: 'var(--auth-bg)' }}>
|
|
77
|
+
<Card style={{ background: 'var(--auth-card-bg)', borderRadius: 'var(--auth-radius, 0.5rem)', fontFamily: 'var(--auth-font, inherit)' }}>
|
|
78
|
+
<div className="text-center mb-6" data-auth="header">
|
|
79
|
+
{branding.logoUrl ? (
|
|
80
|
+
<img src={branding.logoUrl} alt={branding.appName} className="max-h-12 max-w-full object-contain mx-auto" data-auth="logo" />
|
|
81
|
+
) : (
|
|
82
|
+
<h1 className="text-2xl font-bold tracking-tight" data-auth="app-name" style={{ color: 'var(--auth-heading)' }}>{branding.appName}</h1>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
<div data-auth="content">{children}</div>
|
|
86
|
+
<div className="flex flex-wrap justify-center gap-1 mt-6 pt-4 border-t border-gray-200 dark:border-gray-800" data-auth="languages">
|
|
87
|
+
{(branding.languages ?? ALL_LANGUAGES).map((lang) => (
|
|
88
|
+
<button
|
|
89
|
+
key={lang.code}
|
|
90
|
+
type="button"
|
|
91
|
+
className={cn(
|
|
92
|
+
'bg-transparent border-none px-2 py-1 text-xs rounded cursor-pointer transition-colors',
|
|
93
|
+
i18n.language === lang.code || i18n.language?.startsWith(lang.code)
|
|
94
|
+
? 'text-primary font-semibold'
|
|
95
|
+
: 'text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800'
|
|
96
|
+
)}
|
|
97
|
+
onClick={() => i18n.changeLanguage(lang.code)}
|
|
98
|
+
>
|
|
99
|
+
{lang.label}
|
|
100
|
+
</button>
|
|
101
|
+
))}
|
|
102
|
+
</div>
|
|
103
|
+
<ThemeToggle />
|
|
104
|
+
</Card>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|