@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.
Files changed (73) hide show
  1. package/README.md +348 -0
  2. package/dist/App.d.ts +1 -0
  3. package/dist/api.d.ts +35 -0
  4. package/dist/branding.d.ts +22 -0
  5. package/dist/branding.json +8 -0
  6. package/dist/components/AuthLayout.d.ts +7 -0
  7. package/dist/components/ui/alert.d.ts +9 -0
  8. package/dist/components/ui/button.d.ts +11 -0
  9. package/dist/components/ui/card.d.ts +8 -0
  10. package/dist/components/ui/input.d.ts +3 -0
  11. package/dist/components/ui/label.d.ts +3 -0
  12. package/dist/components/ui/separator.d.ts +6 -0
  13. package/dist/favicon.svg +1 -0
  14. package/dist/hooks/useDarkMode.d.ts +6 -0
  15. package/dist/i18n/index.d.ts +2 -0
  16. package/dist/icons.svg +24 -0
  17. package/dist/index.css +3 -0
  18. package/dist/index.d.ts +23 -0
  19. package/dist/index.js +6332 -0
  20. package/dist/lib/utils.d.ts +2 -0
  21. package/dist/main.d.ts +2 -0
  22. package/dist/pages/ConsentPage.d.ts +1 -0
  23. package/dist/pages/DevicePage.d.ts +1 -0
  24. package/dist/pages/ForgotPasswordPage.d.ts +1 -0
  25. package/dist/pages/GrantsPage.d.ts +1 -0
  26. package/dist/pages/LoginPage.d.ts +1 -0
  27. package/dist/pages/MfaChallengePage.d.ts +1 -0
  28. package/dist/pages/MfaSetupPage.d.ts +1 -0
  29. package/dist/pages/RegisterPage.d.ts +1 -0
  30. package/dist/pages/ResetPasswordPage.d.ts +1 -0
  31. package/dist/types.d.ts +91 -0
  32. package/index.html +13 -0
  33. package/package.json +65 -0
  34. package/public/branding.json +8 -0
  35. package/public/favicon.svg +1 -0
  36. package/public/icons.svg +24 -0
  37. package/src/App.tsx +32 -0
  38. package/src/api.ts +156 -0
  39. package/src/branding.ts +55 -0
  40. package/src/components/AuthLayout.tsx +107 -0
  41. package/src/components/ui/alert.tsx +31 -0
  42. package/src/components/ui/button.tsx +51 -0
  43. package/src/components/ui/card.tsx +50 -0
  44. package/src/components/ui/input.tsx +21 -0
  45. package/src/components/ui/label.tsx +17 -0
  46. package/src/components/ui/separator.tsx +16 -0
  47. package/src/hooks/useDarkMode.ts +39 -0
  48. package/src/i18n/de.json +111 -0
  49. package/src/i18n/en.json +136 -0
  50. package/src/i18n/es.json +111 -0
  51. package/src/i18n/fr.json +111 -0
  52. package/src/i18n/index.ts +39 -0
  53. package/src/i18n/pt.json +111 -0
  54. package/src/i18n/tlh.json +111 -0
  55. package/src/i18n/vi.json +111 -0
  56. package/src/i18n/zh-Hans.json +111 -0
  57. package/src/index.ts +44 -0
  58. package/src/lib/utils.ts +6 -0
  59. package/src/main.tsx +19 -0
  60. package/src/pages/ConsentPage.tsx +144 -0
  61. package/src/pages/DevicePage.tsx +145 -0
  62. package/src/pages/ForgotPasswordPage.tsx +90 -0
  63. package/src/pages/GrantsPage.tsx +87 -0
  64. package/src/pages/LoginPage.tsx +423 -0
  65. package/src/pages/MfaChallengePage.tsx +246 -0
  66. package/src/pages/MfaSetupPage.tsx +366 -0
  67. package/src/pages/RegisterPage.tsx +161 -0
  68. package/src/pages/ResetPasswordPage.tsx +219 -0
  69. package/src/styles.css +33 -0
  70. package/src/types.ts +112 -0
  71. package/tsconfig.app.json +37 -0
  72. package/tsconfig.json +7 -0
  73. package/vite.config.ts +54 -0
@@ -0,0 +1,219 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useSearchParams, Link } from 'react-router-dom';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { resetPassword, ApiRequestError } from '../api';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Input } from '@/components/ui/input';
7
+ import { Label } from '@/components/ui/label';
8
+ import { Alert } from '@/components/ui/alert';
9
+ import { CardTitle, CardFooter } from '@/components/ui/card';
10
+ import { Check, X } from 'lucide-react';
11
+
12
+ interface PasswordRule {
13
+ rule: string;
14
+ value: number | null;
15
+ label: string;
16
+ }
17
+
18
+ interface PasswordRequirement {
19
+ label: string;
20
+ met: boolean;
21
+ }
22
+
23
+ const API_URL = import.meta.env.VITE_API_URL || '';
24
+
25
+ const defaultRules: PasswordRule[] = [
26
+ { rule: 'minLength', value: 8, label: 'At least 8 characters' },
27
+ { rule: 'uppercase', value: null, label: 'Uppercase letter' },
28
+ { rule: 'lowercase', value: null, label: 'Lowercase letter' },
29
+ { rule: 'digit', value: null, label: 'Number' },
30
+ { rule: 'specialChar', value: null, label: 'Special character' },
31
+ ];
32
+
33
+ function evaluateRequirements(password: string, rules: PasswordRule[]): PasswordRequirement[] {
34
+ return rules.map((r) => {
35
+ let met = false;
36
+ switch (r.rule) {
37
+ case 'minLength': met = password.length >= (r.value ?? 8); break;
38
+ case 'uppercase': met = /[A-Z]/.test(password); break;
39
+ case 'lowercase': met = /[a-z]/.test(password); break;
40
+ case 'digit': met = /[0-9]/.test(password); break;
41
+ case 'specialChar': met = /[^A-Za-z0-9]/.test(password); break;
42
+ default: met = true;
43
+ }
44
+ return { label: r.label, met };
45
+ });
46
+ }
47
+
48
+ export default function ResetPasswordPage() {
49
+ const { t } = useTranslation();
50
+ const [searchParams] = useSearchParams();
51
+ const token = searchParams.get('p') || '';
52
+
53
+ const [newPassword, setNewPassword] = useState('');
54
+ const [confirmPassword, setConfirmPassword] = useState('');
55
+ const [loading, setLoading] = useState(false);
56
+ const [error, setError] = useState('');
57
+ const [success, setSuccess] = useState(false);
58
+ const [validationError, setValidationError] = useState('');
59
+ const [rules, setRules] = useState<PasswordRule[]>(defaultRules);
60
+
61
+ useEffect(() => {
62
+ fetch(`${API_URL}/api/auth/password-policy`)
63
+ .then((r) => r.ok ? r.json() : null)
64
+ .then((data) => { if (data?.rules) setRules(data.rules); })
65
+ .catch(() => { /* use defaults */ });
66
+ }, []);
67
+
68
+ function getRuleLabel(rule: PasswordRule): string {
69
+ switch (rule.rule) {
70
+ case 'minLength': return t('ruleMinLength', { count: rule.value ?? 8 });
71
+ case 'uppercase': return t('ruleUppercase');
72
+ case 'lowercase': return t('ruleLowercase');
73
+ case 'digit': return t('ruleDigit');
74
+ case 'specialChar': return t('ruleSpecialChar');
75
+ default: return rule.label;
76
+ }
77
+ }
78
+
79
+ const localizedRules: PasswordRule[] = rules.map(r => ({
80
+ ...r,
81
+ label: getRuleLabel(r),
82
+ }));
83
+
84
+ const requirements = evaluateRequirements(newPassword, localizedRules);
85
+ const allRequirementsMet = requirements.every((r) => r.met);
86
+
87
+ async function handleSubmit(e: React.FormEvent) {
88
+ e.preventDefault();
89
+ setError('');
90
+ setValidationError('');
91
+
92
+ if (!allRequirementsMet) {
93
+ setValidationError(t('passwordNotMeetRequirements'));
94
+ return;
95
+ }
96
+
97
+ if (newPassword !== confirmPassword) {
98
+ setValidationError(t('passwordsDoNotMatch'));
99
+ return;
100
+ }
101
+
102
+ setLoading(true);
103
+
104
+ try {
105
+ await resetPassword(token, newPassword);
106
+ setSuccess(true);
107
+ } catch (err) {
108
+ if (err instanceof ApiRequestError) {
109
+ switch (err.error) {
110
+ case 'weak_password':
111
+ setError(err.message || t('passwordWeakError'));
112
+ break;
113
+ case 'invalid_token':
114
+ case 'token_expired':
115
+ setError(t('invalidOrExpiredLink'));
116
+ break;
117
+ case 'password_required':
118
+ setError(t('errorPasswordRequired'));
119
+ break;
120
+ default:
121
+ setError(err.message || t('errorUnexpected'));
122
+ }
123
+ } else {
124
+ setError(t('errorUnexpected'));
125
+ }
126
+ } finally {
127
+ setLoading(false);
128
+ }
129
+ }
130
+
131
+ if (success) {
132
+ return (
133
+ <div>
134
+ <CardTitle>{t('passwordResetSuccess')}</CardTitle>
135
+ <Alert variant="success">{t('passwordResetSuccessMessage')}</Alert>
136
+ <CardFooter>
137
+ <Link to="/login" className="text-sm font-medium text-primary hover:underline no-underline">
138
+ {t('signIn')}
139
+ </Link>
140
+ </CardFooter>
141
+ </div>
142
+ );
143
+ }
144
+
145
+ if (!token) {
146
+ return (
147
+ <div>
148
+ <CardTitle>{t('invalidLink')}</CardTitle>
149
+ <Alert variant="error">{t('invalidOrExpiredLink')}</Alert>
150
+ <CardFooter>
151
+ <Link to="/forgot-password" className="text-sm font-medium text-primary hover:underline no-underline">
152
+ {t('requestNewResetLink')}
153
+ </Link>
154
+ </CardFooter>
155
+ </div>
156
+ );
157
+ }
158
+
159
+ return (
160
+ <div>
161
+ <CardTitle>{t('setNewPassword')}</CardTitle>
162
+
163
+ {error && <Alert variant="error">{error}</Alert>}
164
+ {validationError && <Alert variant="error">{validationError}</Alert>}
165
+
166
+ <form onSubmit={handleSubmit}>
167
+ <div className="mb-4">
168
+ <Label htmlFor="newPassword">{t('newPassword')}</Label>
169
+ <Input
170
+ id="newPassword"
171
+ type="password"
172
+ value={newPassword}
173
+ onChange={(e) => setNewPassword(e.target.value)}
174
+ placeholder={t('newPasswordPlaceholder')}
175
+ autoComplete="new-password"
176
+ autoFocus
177
+ maxLength={256}
178
+ required
179
+ />
180
+ </div>
181
+
182
+ {newPassword.length > 0 && (
183
+ <ul className="list-none mb-4 p-3 bg-gray-50 dark:bg-gray-800/60 rounded-md">
184
+ {requirements.map((req) => (
185
+ <li key={req.label} className={`text-[13px] py-0.5 flex items-center gap-1.5 ${req.met ? 'text-green-800 dark:text-green-400' : 'text-red-800 dark:text-red-400'}`}>
186
+ {req.met ? <Check className="h-3.5 w-3.5 shrink-0" /> : <X className="h-3.5 w-3.5 shrink-0" />}
187
+ {req.label}
188
+ </li>
189
+ ))}
190
+ </ul>
191
+ )}
192
+
193
+ <div className="mb-4">
194
+ <Label htmlFor="confirmPassword">{t('confirmPassword')}</Label>
195
+ <Input
196
+ id="confirmPassword"
197
+ type="password"
198
+ value={confirmPassword}
199
+ onChange={(e) => setConfirmPassword(e.target.value)}
200
+ placeholder={t('confirmPasswordPlaceholder')}
201
+ autoComplete="new-password"
202
+ maxLength={256}
203
+ required
204
+ />
205
+ </div>
206
+
207
+ <Button type="submit" loading={loading} disabled={!allRequirementsMet}>
208
+ {loading ? t('resetting') : t('resetPassword')}
209
+ </Button>
210
+
211
+ <CardFooter>
212
+ <Link to="/login" className="text-sm font-medium text-primary hover:underline no-underline">
213
+ {t('backToSignIn')}
214
+ </Link>
215
+ </CardFooter>
216
+ </form>
217
+ </div>
218
+ );
219
+ }
package/src/styles.css ADDED
@@ -0,0 +1,33 @@
1
+ @import 'tailwindcss';
2
+
3
+ @custom-variant dark (&:where(.dark, .dark *));
4
+
5
+ @theme {
6
+ --color-primary: var(--brand-primary, #2563eb);
7
+ }
8
+
9
+ /*
10
+ * Default auth surface vars. Tenants can override any of these via
11
+ * branding.customCssUrl. Light values are declared at :root; dark-mode
12
+ * overrides are scoped to .dark so tenant CSS still wins when provided.
13
+ */
14
+ :root {
15
+ --brand-primary: #2563eb;
16
+ --auth-bg: #f3f4f6;
17
+ --auth-card-bg: #ffffff;
18
+ --auth-heading: #111827;
19
+ }
20
+
21
+ .dark {
22
+ color-scheme: dark;
23
+ --auth-bg: #030712;
24
+ --auth-card-bg: #111827;
25
+ --auth-heading: #f9fafb;
26
+ }
27
+
28
+ body {
29
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
30
+ 'Helvetica Neue', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
31
+ -webkit-font-smoothing: antialiased;
32
+ -moz-osx-font-smoothing: grayscale;
33
+ }
package/src/types.ts ADDED
@@ -0,0 +1,112 @@
1
+ export interface LoginResponse {
2
+ userId: string;
3
+ email: string;
4
+ name: string;
5
+ }
6
+
7
+ export interface ApiError {
8
+ error: string;
9
+ message?: string;
10
+ retryAfter?: number;
11
+ redirectUrl?: string;
12
+ }
13
+
14
+ export interface SessionResponse {
15
+ authenticated: boolean;
16
+ userId: string;
17
+ email: string;
18
+ name: string;
19
+ }
20
+
21
+ export interface SsoCheckResponse {
22
+ ssoRequired: boolean;
23
+ providerType?: string;
24
+ connectionId?: string;
25
+ redirectUrl?: string;
26
+ }
27
+
28
+ export interface ExternalProvider {
29
+ connectionId: string;
30
+ name: string;
31
+ loginUrl: string;
32
+ }
33
+
34
+ export interface ProvidersResponse {
35
+ providers: ExternalProvider[];
36
+ }
37
+
38
+ export interface PasswordPolicyRule {
39
+ rule: string;
40
+ value: number | null;
41
+ label: string;
42
+ }
43
+
44
+ export interface PasswordPolicyResponse {
45
+ rules: PasswordPolicyRule[];
46
+ }
47
+
48
+ // MFA types
49
+ export interface MfaLoginResponse {
50
+ mfaRequired?: boolean;
51
+ mfaSetupRequired?: boolean;
52
+ mfaAvailable?: boolean;
53
+ clientId?: string;
54
+ challengeId?: string;
55
+ setupToken?: string;
56
+ methods?: string[];
57
+ webAuthn?: PublicKeyCredentialRequestOptionsJSON;
58
+ userId?: string;
59
+ email?: string;
60
+ name?: string;
61
+ }
62
+
63
+ // WebAuthn types (simplified from WebAuthn L2 spec)
64
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
+ export type PublicKeyCredentialRequestOptionsJSON = any;
66
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
+ export type PublicKeyCredentialCreationOptionsJSON = any;
68
+
69
+ export interface MfaVerifyResponse {
70
+ userId: string;
71
+ email: string;
72
+ name: string;
73
+ }
74
+
75
+ export interface MfaStatusResponse {
76
+ enabled: boolean;
77
+ methods: MfaMethod[];
78
+ }
79
+
80
+ export interface MfaMethod {
81
+ id: string;
82
+ type: string;
83
+ name: string;
84
+ createdAt: string;
85
+ lastUsedAt: string | null;
86
+ isConsumed?: boolean | null;
87
+ }
88
+
89
+ export interface MfaTotpSetupResponse {
90
+ setupToken: string;
91
+ qrCodeDataUri: string;
92
+ manualKey: string;
93
+ }
94
+
95
+ export interface MfaRecoveryGenerateResponse {
96
+ codes: string[];
97
+ }
98
+
99
+ export interface MfaWebAuthnSetupResponse {
100
+ setupToken: string;
101
+ options: PublicKeyCredentialCreationOptionsJSON;
102
+ }
103
+
104
+ export interface MfaWebAuthnConfirmResponse {
105
+ success: boolean;
106
+ credentialId: string;
107
+ }
108
+
109
+ export interface RegisterResponse {
110
+ success: boolean;
111
+ userId: string;
112
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "types": ["vite/client"],
9
+ "skipLibCheck": true,
10
+
11
+ /* Bundler mode */
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "moduleDetection": "force",
16
+ "declaration": true,
17
+ "emitDeclarationOnly": true,
18
+ "declarationDir": "./dist",
19
+ "noEmit": false,
20
+ "jsx": "react-jsx",
21
+
22
+ /* Linting */
23
+ "strict": true,
24
+ "noUnusedLocals": true,
25
+ "noUnusedParameters": true,
26
+ "erasableSyntaxOnly": true,
27
+ "noFallthroughCasesInSwitch": true,
28
+ "noUncheckedSideEffectImports": true,
29
+
30
+ /* Path aliases */
31
+ "baseUrl": ".",
32
+ "paths": {
33
+ "@/*": ["./src/*"]
34
+ }
35
+ },
36
+ "include": ["src"]
37
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import tailwindcss from '@tailwindcss/vite'
4
+ import path from 'path'
5
+ import { fileURLToPath } from 'url'
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
8
+
9
+ // https://vite.dev/config/
10
+ export default defineConfig({
11
+ plugins: [react(), tailwindcss()],
12
+ resolve: {
13
+ alias: {
14
+ '@': path.resolve(__dirname, './src'),
15
+ },
16
+ },
17
+ build: {
18
+ lib: {
19
+ entry: path.resolve(__dirname, 'src/index.ts'),
20
+ formats: ['es'],
21
+ fileName: 'index',
22
+ },
23
+ rollupOptions: {
24
+ external: ['react', 'react-dom', 'react/jsx-runtime', 'react-router-dom'],
25
+ output: {
26
+ globals: {
27
+ react: 'React',
28
+ 'react-dom': 'ReactDOM',
29
+ },
30
+ },
31
+ },
32
+ cssCodeSplit: false,
33
+ },
34
+ server: {
35
+ proxy: {
36
+ '/api': {
37
+ target: 'http://localhost:5000',
38
+ changeOrigin: true,
39
+ },
40
+ '/saml': {
41
+ target: 'http://localhost:5000',
42
+ changeOrigin: true,
43
+ },
44
+ '/oidc': {
45
+ target: 'http://localhost:5000',
46
+ changeOrigin: true,
47
+ },
48
+ '/connect': {
49
+ target: 'http://localhost:5000',
50
+ changeOrigin: true,
51
+ },
52
+ },
53
+ },
54
+ })