@atzentis/auth-react 0.0.12 → 0.0.14

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/index.js CHANGED
@@ -1,8 +1,17 @@
1
- import { AuthClient } from '@atzentis/auth-sdk';
2
- import { createContext, useState, useCallback, useMemo, useContext } from 'react';
3
- import { jsx } from 'react/jsx-runtime';
1
+ import { AuthClient, AuthError, AuthErrorCode } from '@atzentis/auth-sdk';
2
+ import { createContext, useState, useCallback, useMemo, useContext, useEffect } from 'react';
3
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
+ import { DropdownMenu, DropdownMenuTrigger, Button, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, Card, DropdownMenuLabel } from '@atzentis/ui-shadcn';
5
+ import { FormSelect, FormInput, OTPInput, FormPasswordInput } from '@atzentis/ui-shared';
6
+ import { zodResolver } from '@hookform/resolvers/zod';
7
+ import { useForm } from 'react-hook-form';
8
+ import { z } from 'zod';
4
9
 
5
10
  var AuthContext = createContext(null);
11
+ var LocalizationContext = createContext({
12
+ overrides: null,
13
+ localizeErrors: true
14
+ });
6
15
  function AuthProvider({
7
16
  apiKey,
8
17
  baseUrl,
@@ -15,7 +24,9 @@ function AuthProvider({
15
24
  onNewDeviceAlert,
16
25
  children,
17
26
  initialUser,
18
- storage
27
+ storage,
28
+ localization,
29
+ localizeErrors = true
19
30
  }) {
20
31
  const [client] = useState(() => {
21
32
  const handleSessionExpired = () => {
@@ -58,7 +69,376 @@ function AuthProvider({
58
69
  }),
59
70
  [client, user, session, isLoading, error, clearError]
60
71
  );
61
- return /* @__PURE__ */ jsx(AuthContext.Provider, { value, children });
72
+ const localizationValue = useMemo(
73
+ () => ({ overrides: localization ?? null, localizeErrors }),
74
+ [localization, localizeErrors]
75
+ );
76
+ return /* @__PURE__ */ jsx(AuthContext.Provider, { value, children: /* @__PURE__ */ jsx(LocalizationContext.Provider, { value: localizationValue, children }) });
77
+ }
78
+
79
+ // src/localization/error-codes.ts
80
+ var AUTH_ERROR_CODES = {
81
+ INVALID_CREDENTIALS: "Invalid email or password",
82
+ EMAIL_NOT_VERIFIED: "Email address is not verified",
83
+ TWO_FACTOR_REQUIRED: "Two-factor authentication is required",
84
+ INVALID_PHONE_NUMBER: "Invalid phone number",
85
+ OTP_EXPIRED: "Verification code has expired",
86
+ OTP_INVALID: "Invalid verification code",
87
+ OTP_MAX_ATTEMPTS: "Too many verification attempts",
88
+ PHONE_NUMBER_IN_USE: "Phone number is already in use",
89
+ FRAUD_DETECTED: "Suspicious activity detected",
90
+ USERNAME_TAKEN: "Username is already taken",
91
+ USERNAME_INVALID: "Invalid username",
92
+ USERNAME_RESERVED: "This username is reserved",
93
+ DEVICE_NOT_FOUND: "Device not found",
94
+ LOGIN_EVENT_NOT_FOUND: "Login event not found",
95
+ REAUTH_REQUIRED: "Please sign in again to continue",
96
+ MAX_SESSIONS_EXCEEDED: "Maximum number of sessions exceeded",
97
+ CAPTCHA_REQUIRED: "CAPTCHA verification is required",
98
+ CAPTCHA_INVALID: "CAPTCHA verification failed",
99
+ PASSWORD_BREACHED: "This password has been found in a data breach",
100
+ SESSION_EXPIRED: "Session expired",
101
+ TOKEN_EXPIRED: "Token has expired",
102
+ INVALID_TOKEN: "Invalid token",
103
+ UNAUTHORIZED_ROLE: "You do not have permission to perform this action",
104
+ ORGANIZATION_NOT_FOUND: "Organization not found",
105
+ PERMISSION_DENIED: "Permission denied",
106
+ RATE_LIMITED: "Too many requests. Please try again later.",
107
+ NETWORK_ERROR: "Network error. Please check your connection.",
108
+ TIMEOUT: "Request timed out. Please try again.",
109
+ USER_NOT_FOUND: "User not found",
110
+ USER_ALREADY_EXISTS: "A user with this email already exists",
111
+ INVALID_EMAIL: "Invalid email address",
112
+ WEAK_PASSWORD: "Password is too weak",
113
+ ORG_LIMIT_EXCEEDED: "Organization limit exceeded",
114
+ MEMBER_LIMIT_EXCEEDED: "Member limit exceeded",
115
+ KEY_NOT_FOUND: "API key not found",
116
+ KEY_REVOKED: "API key has been revoked",
117
+ KEY_EXPIRED: "API key has expired"
118
+ };
119
+
120
+ // src/localization/auth-localization.ts
121
+ var authLocalization = {
122
+ // --- General / Shared ---
123
+ /** @default "Continue" */
124
+ CONTINUE: "Continue",
125
+ /** @default "Cancel" */
126
+ CANCEL: "Cancel",
127
+ /** @default "Save" */
128
+ SAVE: "Save",
129
+ /** @default "Delete" */
130
+ DELETE: "Delete",
131
+ /** @default "Update" */
132
+ UPDATE: "Update",
133
+ /** @default "Remove" */
134
+ REMOVE: "Remove",
135
+ /** @default "Done" */
136
+ DONE: "Done",
137
+ /** @default "Go back" */
138
+ GO_BACK: "Go back",
139
+ /** @default "Loading..." */
140
+ LOADING: "Loading...",
141
+ /** @default "Load more" */
142
+ LOAD_MORE: "Load more",
143
+ /** @default "Unknown" */
144
+ UNKNOWN: "Unknown",
145
+ /** @default "Request failed" */
146
+ REQUEST_FAILED: "Request failed",
147
+ /** @default "Updated successfully" */
148
+ UPDATED_SUCCESSFULLY: "Updated successfully",
149
+ // --- Sign In (LoginForm) ---
150
+ /** @default "Sign in" */
151
+ SIGN_IN: "Sign in",
152
+ /** @default "Sign in" */
153
+ SIGN_IN_ACTION: "Sign in",
154
+ /** @default "Enter your email below to sign in" */
155
+ SIGN_IN_DESCRIPTION: "Enter your email below to sign in",
156
+ /** @default "Enter your username or email to sign in" */
157
+ SIGN_IN_USERNAME_DESCRIPTION: "Enter your username or email to sign in",
158
+ /** @default "Username or email" */
159
+ SIGN_IN_USERNAME_PLACEHOLDER: "Username or email",
160
+ /** @default "Signing in..." */
161
+ SIGNING_IN: "Signing in...",
162
+ /** @default "m@example.com" */
163
+ EMAIL_PLACEHOLDER: "m@example.com",
164
+ /** @default "Password" */
165
+ PASSWORD_PLACEHOLDER: "Password",
166
+ /** @default "Forgot your password?" */
167
+ FORGOT_PASSWORD_LINK: "Forgot your password?",
168
+ /** @default "Use phone instead" */
169
+ USE_PHONE_INSTEAD: "Use phone instead",
170
+ /** @default "Use email instead" */
171
+ USE_EMAIL_INSTEAD: "Use email instead",
172
+ /** @default "Or continue with" */
173
+ OR_CONTINUE_WITH: "Or continue with",
174
+ /** @default "Sign in with" */
175
+ SIGN_IN_WITH: "Sign in with",
176
+ /** @default "Don't have an account?" */
177
+ DONT_HAVE_AN_ACCOUNT: "Don't have an account?",
178
+ /** @default "Remember me" */
179
+ REMEMBER_ME: "Remember me",
180
+ // --- Sign Up (SignupForm) ---
181
+ /** @default "Sign up" */
182
+ SIGN_UP: "Sign up",
183
+ /** @default "Create account" */
184
+ SIGN_UP_ACTION: "Create account",
185
+ /** @default "Enter your information to create an account" */
186
+ SIGN_UP_DESCRIPTION: "Enter your information to create an account",
187
+ /** @default "Check your email for the verification link." */
188
+ SIGN_UP_EMAIL: "Check your email for the verification link.",
189
+ /** @default "Name" */
190
+ NAME_PLACEHOLDER: "Name",
191
+ /** @default "Please enter your full name." */
192
+ NAME_DESCRIPTION: "Please enter your full name.",
193
+ /** @default "Username" */
194
+ USERNAME_PLACEHOLDER: "Username",
195
+ /** @default "Username" */
196
+ USERNAME: "Username",
197
+ /** @default "Confirm password" */
198
+ CONFIRM_PASSWORD_PLACEHOLDER: "Confirm password",
199
+ /** @default "Already have an account?" */
200
+ ALREADY_HAVE_AN_ACCOUNT: "Already have an account?",
201
+ /** @default "I accept the terms and conditions" */
202
+ ACCEPT_TERMS: "I accept the terms and conditions",
203
+ /** @default "You must accept the terms" */
204
+ ACCEPT_TERMS_ERROR: "You must accept the terms",
205
+ /** @default "By continuing, you agree to the" */
206
+ BY_CONTINUING_YOU_AGREE: "By continuing, you agree to the",
207
+ /** @default "Terms of Service" */
208
+ TERMS_OF_SERVICE: "Terms of Service",
209
+ /** @default "Privacy Policy" */
210
+ PRIVACY_POLICY: "Privacy Policy",
211
+ // --- Phone Login (PhoneLoginForm) ---
212
+ /** @default "Phone number" */
213
+ PHONE_NUMBER_PLACEHOLDER: "Phone number",
214
+ /** @default "Country code" */
215
+ COUNTRY_CODE: "Country code",
216
+ /** @default "Send code" */
217
+ SEND_VERIFICATION_CODE: "Send code",
218
+ /** @default "Sending code..." */
219
+ SENDING_VERIFICATION_CODE: "Sending code...",
220
+ /** @default "One-Time Password" */
221
+ ONE_TIME_PASSWORD: "One-Time Password",
222
+ /** @default "Verification code" */
223
+ VERIFICATION_CODE_LABEL: "Verification code",
224
+ /** @default "Verify" */
225
+ VERIFY: "Verify",
226
+ /** @default "Verifying..." */
227
+ VERIFYING: "Verifying...",
228
+ /** @default "Code expires in" */
229
+ CODE_EXPIRES_IN: "Code expires in",
230
+ /** @default "Resend code" */
231
+ RESEND_CODE: "Resend code",
232
+ /** @default "Sign in with password" */
233
+ USE_PASSWORD_INSTEAD: "Sign in with password",
234
+ // --- OAuth ---
235
+ /** @default "Continue with" */
236
+ CONTINUE_WITH: "Continue with",
237
+ /** @default "Redirecting..." */
238
+ REDIRECTING: "Redirecting...",
239
+ // --- Magic Link ---
240
+ /** @default "Magic Link" */
241
+ MAGIC_LINK: "Magic Link",
242
+ /** @default "Send magic link" */
243
+ MAGIC_LINK_ACTION: "Send magic link",
244
+ /** @default "Enter your email to receive a magic link" */
245
+ MAGIC_LINK_DESCRIPTION: "Enter your email to receive a magic link",
246
+ /** @default "We sent a magic link to" */
247
+ MAGIC_LINK_EMAIL: "We sent a magic link to",
248
+ /** @default "Check your inbox and click the link to sign in." */
249
+ MAGIC_LINK_HINT: "Check your inbox and click the link to sign in.",
250
+ /** @default "Sending..." */
251
+ SENDING: "Sending...",
252
+ /** @default "Resend" */
253
+ RESEND: "Resend",
254
+ /** @default "Resending..." */
255
+ RESENDING: "Resending...",
256
+ // --- Password Management ---
257
+ /** @default "Password" */
258
+ PASSWORD: "Password",
259
+ /** @default "New Password" */
260
+ NEW_PASSWORD: "New Password",
261
+ /** @default "Current Password" */
262
+ CURRENT_PASSWORD: "Current Password",
263
+ /** @default "Change Password" */
264
+ CHANGE_PASSWORD: "Change Password",
265
+ /** @default "Enter your current and new password" */
266
+ CHANGE_PASSWORD_DESCRIPTION: "Enter your current and new password",
267
+ /** @default "Password has been changed" */
268
+ CHANGE_PASSWORD_SUCCESS: "Password has been changed",
269
+ /** @default "Forgot Password" */
270
+ FORGOT_PASSWORD: "Forgot Password",
271
+ /** @default "Send reset link" */
272
+ FORGOT_PASSWORD_ACTION: "Send reset link",
273
+ /** @default "Enter your email to reset your password" */
274
+ FORGOT_PASSWORD_DESCRIPTION: "Enter your email to reset your password",
275
+ /** @default "Check your email for the reset link" */
276
+ FORGOT_PASSWORD_EMAIL: "Check your email for the reset link",
277
+ /** @default "Reset Password" */
278
+ RESET_PASSWORD: "Reset Password",
279
+ /** @default "Save new password" */
280
+ RESET_PASSWORD_ACTION: "Save new password",
281
+ /** @default "Password reset successfully" */
282
+ RESET_PASSWORD_SUCCESS: "Password reset successfully",
283
+ /** @default "Set Password" */
284
+ SET_PASSWORD: "Set Password",
285
+ /** @default "Click the button to receive an email to set your password" */
286
+ SET_PASSWORD_DESCRIPTION: "Click the button to receive an email to set your password",
287
+ // --- Devices & Sessions ---
288
+ /** @default "Current Session" */
289
+ CURRENT_SESSION: "Current Session",
290
+ /** @default "Current device" */
291
+ CURRENT_DEVICE: "Current device",
292
+ /** @default "Sessions" */
293
+ SESSIONS: "Sessions",
294
+ /** @default "Manage your active sessions" */
295
+ SESSIONS_DESCRIPTION: "Manage your active sessions",
296
+ /** @default "Trust" */
297
+ TRUST_DEVICE: "Trust",
298
+ /** @default "Untrust" */
299
+ UNTRUST_DEVICE: "Untrust",
300
+ /** @default "Loading devices..." */
301
+ LOADING_DEVICES: "Loading devices...",
302
+ /** @default "No devices found" */
303
+ NO_DEVICES: "No devices found",
304
+ /** @default "Last used:" */
305
+ LAST_USED: "Last used:",
306
+ /** @default "IP:" */
307
+ IP_LABEL: "IP:",
308
+ /** @default "Security" */
309
+ SECURITY: "Security",
310
+ /** @default "Session not fresh. Please sign in again." */
311
+ SESSION_NOT_FRESH: "Session not fresh. Please sign in again.",
312
+ // --- Login Activity & Risk ---
313
+ /** @default "Login Activity" */
314
+ LOGIN_ACTIVITY: "Login Activity",
315
+ /** @default "Recent login attempts" */
316
+ LOGIN_ACTIVITY_DESCRIPTION: "Recent login attempts",
317
+ /** @default "Low" */
318
+ RISK_LOW: "Low",
319
+ /** @default "Medium" */
320
+ RISK_MEDIUM: "Medium",
321
+ /** @default "High" */
322
+ RISK_HIGH: "High",
323
+ /** @default "This was me" */
324
+ MARK_RECOGNIZED: "This was me",
325
+ /** @default "Not me" */
326
+ REPORT_SUSPICIOUS: "Not me",
327
+ /** @default "Loading activity..." */
328
+ LOADING_ACTIVITY: "Loading activity...",
329
+ /** @default "No login activity found" */
330
+ NO_ACTIVITY: "No login activity found",
331
+ // --- Accounts & Switching ---
332
+ /** @default "Accounts" */
333
+ ACCOUNTS: "Accounts",
334
+ /** @default "Manage signed in accounts" */
335
+ ACCOUNTS_DESCRIPTION: "Manage signed in accounts",
336
+ /** @default "Add account" */
337
+ ADD_ACCOUNT: "Add account",
338
+ /** @default "Switch Account" */
339
+ SWITCH_ACCOUNT: "Switch Account",
340
+ /** @default "Select account" */
341
+ SELECT_ACCOUNT: "Select account",
342
+ /** @default "Personal Account" */
343
+ PERSONAL_ACCOUNT: "Personal Account",
344
+ /** @default "Sign out" */
345
+ SIGN_OUT: "Sign out",
346
+ // --- Organizations ---
347
+ /** @default "Organization" */
348
+ ORGANIZATION: "Organization",
349
+ /** @default "Organizations" */
350
+ ORGANIZATIONS: "Organizations",
351
+ /** @default "Manage your organizations" */
352
+ ORGANIZATIONS_DESCRIPTION: "Manage your organizations",
353
+ /** @default "Select organization" */
354
+ SELECT_ORGANIZATION: "Select organization",
355
+ /** @default "Create organization" */
356
+ CREATE_ORGANIZATION: "Create organization",
357
+ /** @default "Name" */
358
+ ORGANIZATION_NAME: "Name",
359
+ /** @default "Acme Inc." */
360
+ ORGANIZATION_NAME_PLACEHOLDER: "Acme Inc.",
361
+ /** @default "Leave Organization" */
362
+ LEAVE_ORGANIZATION: "Leave Organization",
363
+ /** @default "Delete Organization" */
364
+ DELETE_ORGANIZATION: "Delete Organization",
365
+ /** @default "Members" */
366
+ MEMBERS: "Members",
367
+ /** @default "Invite Member" */
368
+ INVITE_MEMBER: "Invite Member",
369
+ /** @default "Remove Member" */
370
+ REMOVE_MEMBER: "Remove Member",
371
+ /** @default "Role" */
372
+ ROLE: "Role",
373
+ /** @default "Admin" */
374
+ ADMIN: "Admin",
375
+ /** @default "Member" */
376
+ MEMBER: "Member",
377
+ /** @default "Owner" */
378
+ OWNER: "Owner",
379
+ // --- User Menu ---
380
+ /** @default "Settings" */
381
+ SETTINGS: "Settings",
382
+ /** @default "Account" */
383
+ ACCOUNT: "Account",
384
+ /** @default "Avatar" */
385
+ AVATAR: "Avatar",
386
+ /** @default "Click avatar to upload a custom one" */
387
+ AVATAR_DESCRIPTION: "Click avatar to upload a custom one",
388
+ /** @default "Delete Avatar" */
389
+ DELETE_AVATAR: "Delete Avatar",
390
+ /** @default "Upload Avatar" */
391
+ UPLOAD_AVATAR: "Upload Avatar",
392
+ /** @default "Email" */
393
+ EMAIL: "Email",
394
+ /** @default "Name" */
395
+ NAME: "Name",
396
+ // --- Two-Factor ---
397
+ /** @default "Two-Factor" */
398
+ TWO_FACTOR: "Two-Factor",
399
+ /** @default "Two-Factor Authentication" */
400
+ TWO_FACTOR_PROMPT: "Two-Factor Authentication",
401
+ /** @default "Enter your one-time password" */
402
+ TWO_FACTOR_DESCRIPTION: "Enter your one-time password",
403
+ /** @default "Verify code" */
404
+ TWO_FACTOR_ACTION: "Verify code",
405
+ /** @default "Two-factor authentication has been enabled" */
406
+ TWO_FACTOR_ENABLED: "Two-factor authentication has been enabled",
407
+ /** @default "Two-factor authentication has been disabled" */
408
+ TWO_FACTOR_DISABLED: "Two-factor authentication has been disabled",
409
+ /** @default "Enable Two-Factor" */
410
+ ENABLE_TWO_FACTOR: "Enable Two-Factor",
411
+ /** @default "Disable Two-Factor" */
412
+ DISABLE_TWO_FACTOR: "Disable Two-Factor",
413
+ /** @default "Backup Codes" */
414
+ BACKUP_CODES: "Backup Codes",
415
+ /** @default "Save these codes in a secure place" */
416
+ BACKUP_CODES_DESCRIPTION: "Save these codes in a secure place",
417
+ // --- Validation ---
418
+ /** @default "Passwords do not match" */
419
+ PASSWORDS_DO_NOT_MATCH: "Passwords do not match",
420
+ /** @default "Password is required" */
421
+ PASSWORD_REQUIRED: "Password is required",
422
+ /** @default "Email address is required" */
423
+ EMAIL_REQUIRED: "Email address is required",
424
+ /** @default "is required" */
425
+ IS_REQUIRED: "is required",
426
+ /** @default "is invalid" */
427
+ IS_INVALID: "is invalid",
428
+ // --- Error Codes ---
429
+ ...AUTH_ERROR_CODES
430
+ };
431
+ function getLocalizedError(error, localization, localizeErrors) {
432
+ if (!localizeErrors) {
433
+ if (error instanceof Error && error.message) return error.message;
434
+ return authLocalization.REQUEST_FAILED;
435
+ }
436
+ if (error instanceof AuthError) {
437
+ const errorCode = error.code;
438
+ if (localization?.[errorCode]) return localization[errorCode];
439
+ }
440
+ if (error instanceof Error && error.message) return error.message;
441
+ return localization?.REQUEST_FAILED ?? authLocalization.REQUEST_FAILED;
62
442
  }
63
443
  function useAuthContext() {
64
444
  const context = useContext(AuthContext);
@@ -68,7 +448,87 @@ function useAuthContext() {
68
448
  return context;
69
449
  }
70
450
 
71
- // src/use-auth.ts
451
+ // src/hooks/use-api-keys.ts
452
+ function useApiKeys() {
453
+ const { client } = useAuthContext();
454
+ const [apiKeys, setApiKeys] = useState([]);
455
+ const [isLoading, setIsLoading] = useState(false);
456
+ const [error, setError] = useState(null);
457
+ const refresh = useCallback(async () => {
458
+ setIsLoading(true);
459
+ setError(null);
460
+ try {
461
+ const response = await client.apiKeys.list();
462
+ setApiKeys(response.data);
463
+ } catch (err) {
464
+ setError(err);
465
+ } finally {
466
+ setIsLoading(false);
467
+ }
468
+ }, [client]);
469
+ const create = useCallback(
470
+ async (data) => {
471
+ const response = await client.apiKeys.create(data);
472
+ await refresh();
473
+ return response;
474
+ },
475
+ [client, refresh]
476
+ );
477
+ const get = useCallback(
478
+ async (keyId) => {
479
+ return client.apiKeys.get(keyId);
480
+ },
481
+ [client]
482
+ );
483
+ const update = useCallback(
484
+ async (keyId, data) => {
485
+ const updated = await client.apiKeys.update(keyId, data);
486
+ await refresh();
487
+ return updated;
488
+ },
489
+ [client, refresh]
490
+ );
491
+ const rotate = useCallback(
492
+ async (keyId) => {
493
+ const response = await client.apiKeys.rotate(keyId);
494
+ await refresh();
495
+ return response;
496
+ },
497
+ [client, refresh]
498
+ );
499
+ const revoke = useCallback(
500
+ async (keyId) => {
501
+ await client.apiKeys.revoke(keyId);
502
+ await refresh();
503
+ },
504
+ [client, refresh]
505
+ );
506
+ const validate = useCallback(
507
+ async (keyString) => {
508
+ return client.apiKeys.validate(keyString);
509
+ },
510
+ [client]
511
+ );
512
+ const getUsage = useCallback(
513
+ async (keyId) => {
514
+ return client.apiKeys.getUsage(keyId);
515
+ },
516
+ [client]
517
+ );
518
+ return {
519
+ apiKeys,
520
+ isLoading,
521
+ error,
522
+ refresh,
523
+ create,
524
+ get,
525
+ update,
526
+ rotate,
527
+ revoke,
528
+ validate,
529
+ getUsage
530
+ };
531
+ }
72
532
  function useAuth() {
73
533
  const { client, user, isAuthenticated, isLoading, error, setUser, clearError } = useAuthContext();
74
534
  const login = useCallback(
@@ -79,6 +539,14 @@ function useAuth() {
79
539
  },
80
540
  [client, setUser]
81
541
  );
542
+ const loginWithUsername = useCallback(
543
+ async (credentials) => {
544
+ const response = await client.loginWithUsername(credentials);
545
+ setUser(response.user);
546
+ return response;
547
+ },
548
+ [client, setUser]
549
+ );
82
550
  const signup = useCallback(
83
551
  async (data) => {
84
552
  const response = await client.signup(data);
@@ -87,18 +555,411 @@ function useAuth() {
87
555
  },
88
556
  [client, setUser]
89
557
  );
558
+ const logout = useCallback(async () => {
559
+ await client.logout();
560
+ setUser(null);
561
+ }, [client, setUser]);
562
+ const logoutAllDevices = useCallback(async () => {
563
+ await client.logoutAllDevices();
564
+ setUser(null);
565
+ }, [client, setUser]);
566
+ const getOAuthUrl = useCallback(
567
+ (config) => {
568
+ return client.getOAuthUrl(config);
569
+ },
570
+ [client]
571
+ );
572
+ const verifyOAuthCode = useCallback(
573
+ async (request) => {
574
+ const response = await client.verifyOAuthCode(request);
575
+ setUser(response.user);
576
+ return response;
577
+ },
578
+ [client, setUser]
579
+ );
580
+ const sendMagicLink = useCallback(
581
+ async (request) => {
582
+ await client.sendMagicLink(request);
583
+ },
584
+ [client]
585
+ );
586
+ const verifyMagicLink = useCallback(
587
+ async (request) => {
588
+ const response = await client.verifyMagicLink(request);
589
+ setUser(response.user);
590
+ return response;
591
+ },
592
+ [client, setUser]
593
+ );
594
+ const isUsernameAvailable = useCallback(
595
+ async (username) => {
596
+ return client.isUsernameAvailable(username);
597
+ },
598
+ [client]
599
+ );
600
+ const getAccessToken = useCallback(async () => {
601
+ return client.getAccessToken();
602
+ }, [client]);
603
+ const refreshToken = useCallback(async () => {
604
+ return client.refreshToken();
605
+ }, [client]);
606
+ const getAuthHeader = useCallback(async () => {
607
+ const token = await client.getAccessToken();
608
+ if (token) {
609
+ return { Authorization: `Bearer ${token}` };
610
+ }
611
+ return {};
612
+ }, [client]);
90
613
  return {
91
614
  user,
92
615
  isAuthenticated,
93
616
  isLoading,
94
617
  error,
95
618
  login,
619
+ loginWithUsername,
96
620
  signup,
621
+ logout,
622
+ logoutAllDevices,
623
+ getOAuthUrl,
624
+ verifyOAuthCode,
625
+ sendMagicLink,
626
+ verifyMagicLink,
627
+ isUsernameAvailable,
628
+ getAccessToken,
629
+ refreshToken,
630
+ getAuthHeader,
97
631
  clearError
98
632
  };
99
633
  }
634
+ function useAuthLocalization() {
635
+ const ctx = useContext(LocalizationContext);
636
+ const localization = ctx.overrides ? { ...authLocalization, ...ctx.overrides } : authLocalization;
637
+ return { localization, localizeErrors: ctx.localizeErrors };
638
+ }
639
+ function useDevices() {
640
+ const { client } = useAuthContext();
641
+ const [devices, setDevices] = useState([]);
642
+ const [currentDevice, setCurrentDevice] = useState(null);
643
+ const [isLoading, setIsLoading] = useState(false);
644
+ const [error, setError] = useState(null);
645
+ const refresh = useCallback(async () => {
646
+ setIsLoading(true);
647
+ setError(null);
648
+ try {
649
+ const response = await client.devices.list();
650
+ setDevices(response.data);
651
+ const current = response.data.find((d) => d.isCurrent) ?? null;
652
+ setCurrentDevice(current);
653
+ } catch (err) {
654
+ setError(err);
655
+ } finally {
656
+ setIsLoading(false);
657
+ }
658
+ }, [client]);
659
+ const trustDevice = useCallback(
660
+ async (id) => {
661
+ await client.devices.trust(id);
662
+ await refresh();
663
+ },
664
+ [client, refresh]
665
+ );
666
+ const untrustDevice = useCallback(
667
+ async (id) => {
668
+ await client.devices.untrust(id);
669
+ await refresh();
670
+ },
671
+ [client, refresh]
672
+ );
673
+ const removeDevice = useCallback(
674
+ async (id) => {
675
+ await client.devices.remove(id);
676
+ await refresh();
677
+ },
678
+ [client, refresh]
679
+ );
680
+ const removeAllOtherDevices = useCallback(async () => {
681
+ await client.devices.removeAllOthers();
682
+ await refresh();
683
+ }, [client, refresh]);
684
+ return {
685
+ devices,
686
+ currentDevice,
687
+ isLoading,
688
+ error,
689
+ refresh,
690
+ trustDevice,
691
+ untrustDevice,
692
+ removeDevice,
693
+ removeAllOtherDevices
694
+ };
695
+ }
696
+ function useLoginActivity(options = {}) {
697
+ const { client } = useAuthContext();
698
+ const [events, setEvents] = useState([]);
699
+ const [total, setTotal] = useState(0);
700
+ const [isLoading, setIsLoading] = useState(false);
701
+ const [error, setError] = useState(null);
702
+ const [filterOptions, setFilterOptions] = useState({
703
+ limit: options.limit ?? 50,
704
+ offset: 0
705
+ });
706
+ const hasMore = events.length < total;
707
+ const refresh = useCallback(async () => {
708
+ setIsLoading(true);
709
+ setError(null);
710
+ try {
711
+ const response = await client.loginActivity.list(filterOptions);
712
+ setEvents(response.data);
713
+ setTotal(response.total);
714
+ } catch (err) {
715
+ setError(err);
716
+ } finally {
717
+ setIsLoading(false);
718
+ }
719
+ }, [client, filterOptions]);
720
+ const loadMore = useCallback(async () => {
721
+ setIsLoading(true);
722
+ setError(null);
723
+ try {
724
+ const response = await client.loginActivity.list({
725
+ ...filterOptions,
726
+ offset: events.length
727
+ });
728
+ setEvents((prev) => [...prev, ...response.data]);
729
+ setTotal(response.total);
730
+ } catch (err) {
731
+ setError(err);
732
+ } finally {
733
+ setIsLoading(false);
734
+ }
735
+ }, [client, filterOptions, events.length]);
736
+ const filter = useCallback(
737
+ (newOptions) => {
738
+ const merged = { ...filterOptions, ...newOptions, offset: 0 };
739
+ setFilterOptions(merged);
740
+ setEvents([]);
741
+ setTotal(0);
742
+ },
743
+ [filterOptions]
744
+ );
745
+ const markRecognized = useCallback(
746
+ async (eventId) => {
747
+ await client.loginActivity.markRecognized(eventId);
748
+ },
749
+ [client]
750
+ );
751
+ const reportSuspicious = useCallback(
752
+ async (eventId) => {
753
+ return client.loginActivity.reportSuspicious(eventId);
754
+ },
755
+ [client]
756
+ );
757
+ return {
758
+ events,
759
+ total,
760
+ hasMore,
761
+ isLoading,
762
+ error,
763
+ refresh,
764
+ loadMore,
765
+ filter,
766
+ markRecognized,
767
+ reportSuspicious
768
+ };
769
+ }
770
+ function useOrganizations() {
771
+ const { client } = useAuthContext();
772
+ const [organizations, setOrganizations] = useState([]);
773
+ const [isLoading, setIsLoading] = useState(false);
774
+ const [error, setError] = useState(null);
775
+ const refresh = useCallback(async () => {
776
+ setIsLoading(true);
777
+ setError(null);
778
+ try {
779
+ const response = await client.organizations.list();
780
+ setOrganizations(response.data);
781
+ } catch (err) {
782
+ setError(err);
783
+ } finally {
784
+ setIsLoading(false);
785
+ }
786
+ }, [client]);
787
+ const create = useCallback(
788
+ async (data) => {
789
+ const org = await client.organizations.create(data);
790
+ await refresh();
791
+ return org;
792
+ },
793
+ [client, refresh]
794
+ );
795
+ const update = useCallback(
796
+ async (orgId, data) => {
797
+ const org = await client.organizations.update(orgId, data);
798
+ await refresh();
799
+ return org;
800
+ },
801
+ [client, refresh]
802
+ );
803
+ const deleteOrg = useCallback(
804
+ async (orgId) => {
805
+ await client.organizations.delete(orgId);
806
+ await refresh();
807
+ },
808
+ [client, refresh]
809
+ );
810
+ const listMembers = useCallback(
811
+ async (orgId, options) => {
812
+ return client.organizations.listMembers(orgId, options);
813
+ },
814
+ [client]
815
+ );
816
+ const updateMember = useCallback(
817
+ async (orgId, userId, data) => {
818
+ return client.organizations.updateMember(orgId, userId, data);
819
+ },
820
+ [client]
821
+ );
822
+ const removeMember = useCallback(
823
+ async (orgId, userId) => {
824
+ await client.organizations.removeMember(orgId, userId);
825
+ },
826
+ [client]
827
+ );
828
+ const inviteMember = useCallback(
829
+ async (orgId, data) => {
830
+ return client.organizations.inviteMember(orgId, data);
831
+ },
832
+ [client]
833
+ );
834
+ const listInvitations = useCallback(
835
+ async (orgId, options) => {
836
+ return client.organizations.listInvitations(orgId, options);
837
+ },
838
+ [client]
839
+ );
840
+ const resendInvitation = useCallback(
841
+ async (orgId, invitationId) => {
842
+ await client.organizations.resendInvitation(orgId, invitationId);
843
+ },
844
+ [client]
845
+ );
846
+ const cancelInvitation = useCallback(
847
+ async (orgId, invitationId) => {
848
+ await client.organizations.cancelInvitation(orgId, invitationId);
849
+ },
850
+ [client]
851
+ );
852
+ const acceptInvitation = useCallback(
853
+ async (data) => {
854
+ await client.organizations.acceptInvitation(data);
855
+ },
856
+ [client]
857
+ );
858
+ const declineInvitation = useCallback(
859
+ async (data) => {
860
+ await client.organizations.declineInvitation(data);
861
+ },
862
+ [client]
863
+ );
864
+ const leave = useCallback(
865
+ async (orgId) => {
866
+ await client.organizations.leave(orgId);
867
+ await refresh();
868
+ },
869
+ [client, refresh]
870
+ );
871
+ return {
872
+ organizations,
873
+ isLoading,
874
+ error,
875
+ refresh,
876
+ create,
877
+ update,
878
+ delete: deleteOrg,
879
+ listMembers,
880
+ updateMember,
881
+ removeMember,
882
+ inviteMember,
883
+ listInvitations,
884
+ resendInvitation,
885
+ cancelInvitation,
886
+ acceptInvitation,
887
+ declineInvitation,
888
+ leave
889
+ };
890
+ }
891
+ function usePhone() {
892
+ const { client, setUser } = useAuthContext();
893
+ const [isSending, setIsSending] = useState(false);
894
+ const [isVerifying, setIsVerifying] = useState(false);
895
+ const [codeSent, setCodeSent] = useState(false);
896
+ const [expiresIn, setExpiresIn] = useState(null);
897
+ const [error, setError] = useState(null);
898
+ const sendOTP = useCallback(
899
+ async (phoneNumber) => {
900
+ setIsSending(true);
901
+ setError(null);
902
+ try {
903
+ const response = await client.phone.sendOTP({ phoneNumber });
904
+ setCodeSent(true);
905
+ setExpiresIn(response.expiresIn);
906
+ return response;
907
+ } catch (err) {
908
+ setError(err);
909
+ throw err;
910
+ } finally {
911
+ setIsSending(false);
912
+ }
913
+ },
914
+ [client]
915
+ );
916
+ const verify = useCallback(
917
+ async (phoneNumber, code) => {
918
+ setIsVerifying(true);
919
+ setError(null);
920
+ try {
921
+ const response = await client.phone.verify({ phoneNumber, code });
922
+ setUser(response.user);
923
+ return response;
924
+ } catch (err) {
925
+ setError(err);
926
+ throw err;
927
+ } finally {
928
+ setIsVerifying(false);
929
+ }
930
+ },
931
+ [client, setUser]
932
+ );
933
+ const signIn = useCallback(
934
+ async (phoneNumber, password) => {
935
+ setIsVerifying(true);
936
+ setError(null);
937
+ try {
938
+ const response = await client.phone.signIn({ phoneNumber, password });
939
+ setUser(response.user);
940
+ return response;
941
+ } catch (err) {
942
+ setError(err);
943
+ throw err;
944
+ } finally {
945
+ setIsVerifying(false);
946
+ }
947
+ },
948
+ [client, setUser]
949
+ );
950
+ return {
951
+ isSending,
952
+ isVerifying,
953
+ codeSent,
954
+ expiresIn,
955
+ error,
956
+ sendOTP,
957
+ verify,
958
+ signIn
959
+ };
960
+ }
100
961
 
101
- // src/use-session.ts
962
+ // src/hooks/use-session.ts
102
963
  function useSession() {
103
964
  const { user, session, isAuthenticated, isLoading } = useAuthContext();
104
965
  return {
@@ -108,7 +969,1214 @@ function useSession() {
108
969
  isLoading
109
970
  };
110
971
  }
972
+ function useSessions() {
973
+ const { client, session, setUser } = useAuthContext();
974
+ const [sessions, setSessions] = useState([]);
975
+ const [deviceSessions, setDeviceSessions] = useState([]);
976
+ const [isLoading, setIsLoading] = useState(false);
977
+ const [error, setError] = useState(null);
978
+ const refresh = useCallback(async () => {
979
+ setIsLoading(true);
980
+ setError(null);
981
+ try {
982
+ const [sessionsResponse, deviceSessionsResponse] = await Promise.all([
983
+ client.sessions.list(),
984
+ client.sessions.listDeviceSessions()
985
+ ]);
986
+ setSessions(sessionsResponse.data);
987
+ setDeviceSessions(deviceSessionsResponse);
988
+ } catch (err) {
989
+ setError(err);
990
+ } finally {
991
+ setIsLoading(false);
992
+ }
993
+ }, [client]);
994
+ const revokeSession = useCallback(
995
+ async (id) => {
996
+ await client.sessions.revoke(id);
997
+ await refresh();
998
+ },
999
+ [client, refresh]
1000
+ );
1001
+ const revokeAllSessions = useCallback(
1002
+ async (options) => {
1003
+ await client.sessions.revokeAll(options);
1004
+ await refresh();
1005
+ },
1006
+ [client, refresh]
1007
+ );
1008
+ const switchAccount = useCallback(
1009
+ async (sessionToken) => {
1010
+ const response = await client.sessions.setActive({ sessionToken });
1011
+ setUser(response.user);
1012
+ },
1013
+ [client, setUser]
1014
+ );
1015
+ const revokeDeviceSession = useCallback(
1016
+ async (sessionToken) => {
1017
+ await client.sessions.revokeDeviceSession({ sessionToken });
1018
+ await refresh();
1019
+ },
1020
+ [client, refresh]
1021
+ );
1022
+ return {
1023
+ session,
1024
+ sessions,
1025
+ deviceSessions,
1026
+ isLoading,
1027
+ error,
1028
+ refresh,
1029
+ revokeSession,
1030
+ revokeAllSessions,
1031
+ switchAccount,
1032
+ revokeDeviceSession
1033
+ };
1034
+ }
1035
+ function useUser() {
1036
+ const { client, user, setUser } = useAuthContext();
1037
+ const [isLoading, setIsLoading] = useState(false);
1038
+ const [error, setError] = useState(null);
1039
+ const updateProfile = useCallback(
1040
+ async (data) => {
1041
+ setIsLoading(true);
1042
+ setError(null);
1043
+ try {
1044
+ const updated = await client.users.update(data);
1045
+ setUser(updated);
1046
+ return updated;
1047
+ } catch (err) {
1048
+ setError(err);
1049
+ throw err;
1050
+ } finally {
1051
+ setIsLoading(false);
1052
+ }
1053
+ },
1054
+ [client, setUser]
1055
+ );
1056
+ const changePassword = useCallback(
1057
+ async (currentPassword, newPassword) => {
1058
+ await client.users.changePassword({ currentPassword, newPassword });
1059
+ },
1060
+ [client]
1061
+ );
1062
+ const setupTwoFactor = useCallback(async () => {
1063
+ return client.users.setupTwoFactor();
1064
+ }, [client]);
1065
+ const verifyTwoFactorSetup = useCallback(
1066
+ async (code) => {
1067
+ await client.users.verifyTwoFactorSetup({ code });
1068
+ },
1069
+ [client]
1070
+ );
1071
+ const disableTwoFactor = useCallback(
1072
+ async (password) => {
1073
+ await client.users.disableTwoFactor({ password });
1074
+ },
1075
+ [client]
1076
+ );
1077
+ const deleteAccount = useCallback(
1078
+ async (password, reason) => {
1079
+ await client.users.deleteAccount({ password, reason });
1080
+ setUser(null);
1081
+ },
1082
+ [client, setUser]
1083
+ );
1084
+ const listConnectedProviders = useCallback(async () => {
1085
+ return client.users.listConnectedProviders();
1086
+ }, [client]);
1087
+ const connectOAuthProvider = useCallback(
1088
+ async (data) => {
1089
+ return client.users.connectOAuthProvider(data);
1090
+ },
1091
+ [client]
1092
+ );
1093
+ const disconnectOAuthProvider = useCallback(
1094
+ async (provider) => {
1095
+ await client.users.disconnectOAuthProvider(provider);
1096
+ },
1097
+ [client]
1098
+ );
1099
+ const addPhoneNumber = useCallback(
1100
+ async (phoneNumber) => {
1101
+ await client.users.addPhoneNumber({ phoneNumber });
1102
+ },
1103
+ [client]
1104
+ );
1105
+ const verifyPhoneNumber = useCallback(
1106
+ async (code) => {
1107
+ await client.users.verifyPhoneNumber({ code });
1108
+ },
1109
+ [client]
1110
+ );
1111
+ const removePhoneNumber = useCallback(
1112
+ async (password) => {
1113
+ await client.users.removePhoneNumber({ password });
1114
+ },
1115
+ [client]
1116
+ );
1117
+ return {
1118
+ user,
1119
+ isLoading,
1120
+ error,
1121
+ updateProfile,
1122
+ changePassword,
1123
+ setupTwoFactor,
1124
+ verifyTwoFactorSetup,
1125
+ disableTwoFactor,
1126
+ deleteAccount,
1127
+ listConnectedProviders,
1128
+ connectOAuthProvider,
1129
+ disconnectOAuthProvider,
1130
+ addPhoneNumber,
1131
+ verifyPhoneNumber,
1132
+ removePhoneNumber
1133
+ };
1134
+ }
1135
+
1136
+ // src/helpers/initials.ts
1137
+ function getInitials(name) {
1138
+ return name.split(" ").map((w) => w[0]).join("").toUpperCase().slice(0, 2);
1139
+ }
1140
+ function AccountSwitcher({
1141
+ showAddAccount = false,
1142
+ onSwitch,
1143
+ onAddAccount,
1144
+ maxAccounts = 5,
1145
+ className
1146
+ }) {
1147
+ const { deviceSessions, switchAccount, isLoading, refresh } = useSessions();
1148
+ const { localization } = useAuthLocalization();
1149
+ useEffect(() => {
1150
+ refresh();
1151
+ }, [refresh]);
1152
+ const handleSwitch = useCallback(
1153
+ async (sessionToken, user) => {
1154
+ await switchAccount(sessionToken);
1155
+ onSwitch?.(user);
1156
+ },
1157
+ [switchAccount, onSwitch]
1158
+ );
1159
+ const visibleSessions = deviceSessions.slice(0, maxAccounts);
1160
+ const activeSession = deviceSessions.find((s) => s.isActive);
1161
+ return /* @__PURE__ */ jsx("div", { className, children: /* @__PURE__ */ jsxs(DropdownMenu, { children: [
1162
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "outline", disabled: isLoading, children: activeSession ? activeSession.user.name : localization.SELECT_ACCOUNT }) }),
1163
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", children: [
1164
+ visibleSessions.map((session) => /* @__PURE__ */ jsx(
1165
+ DropdownMenuItem,
1166
+ {
1167
+ onClick: () => handleSwitch(session.sessionToken, session.user),
1168
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1169
+ /* @__PURE__ */ jsx("span", { className: "flex h-8 w-8 items-center justify-center rounded-full bg-gray-200 text-xs font-medium", children: session.user.avatar ? /* @__PURE__ */ jsx("img", { src: session.user.avatar, alt: "", className: "h-8 w-8 rounded-full" }) : getInitials(session.user.name) }),
1170
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
1171
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: session.user.name }),
1172
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-500", children: session.user.email })
1173
+ ] }),
1174
+ session.isActive && /* @__PURE__ */ jsx("span", { className: "ml-auto text-sm", children: "\u2713" })
1175
+ ] })
1176
+ },
1177
+ session.sessionToken
1178
+ )),
1179
+ showAddAccount && /* @__PURE__ */ jsxs(Fragment, { children: [
1180
+ /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
1181
+ /* @__PURE__ */ jsx(DropdownMenuItem, { onClick: onAddAccount, children: localization.ADD_ACCOUNT })
1182
+ ] })
1183
+ ] })
1184
+ ] }) });
1185
+ }
1186
+ function SessionGuard({
1187
+ children,
1188
+ fallback,
1189
+ loadingFallback = null,
1190
+ freshRequired = false,
1191
+ freshThreshold = 300,
1192
+ requiredRole,
1193
+ className
1194
+ }) {
1195
+ const { user, session, isAuthenticated, isLoading } = useSession();
1196
+ if (isLoading) {
1197
+ return className ? /* @__PURE__ */ jsx("div", { className, children: loadingFallback }) : /* @__PURE__ */ jsx(Fragment, { children: loadingFallback });
1198
+ }
1199
+ if (!isAuthenticated) {
1200
+ return className ? /* @__PURE__ */ jsx("div", { className, children: fallback }) : /* @__PURE__ */ jsx(Fragment, { children: fallback });
1201
+ }
1202
+ if (freshRequired && session) {
1203
+ const sessionAge = (Date.now() - new Date(session.createdAt).getTime()) / 1e3;
1204
+ if (sessionAge > freshThreshold) {
1205
+ return className ? /* @__PURE__ */ jsx("div", { className, children: fallback }) : /* @__PURE__ */ jsx(Fragment, { children: fallback });
1206
+ }
1207
+ }
1208
+ if (requiredRole && user) {
1209
+ const userRole = user.role;
1210
+ if (userRole !== requiredRole) {
1211
+ return className ? /* @__PURE__ */ jsx("div", { className, children: fallback }) : /* @__PURE__ */ jsx(Fragment, { children: fallback });
1212
+ }
1213
+ }
1214
+ return className ? /* @__PURE__ */ jsx("div", { className, children }) : /* @__PURE__ */ jsx(Fragment, { children });
1215
+ }
1216
+ var DEFAULT_DEVICE_ICONS = {
1217
+ mobile: "\u{1F4F1}",
1218
+ desktop: "\u{1F4BB}",
1219
+ tablet: "\u{1F4DF}"
1220
+ };
1221
+ function DeviceList({
1222
+ showTrustToggle = false,
1223
+ showRemoveAction = false,
1224
+ onDeviceTrusted,
1225
+ onDeviceUntrusted,
1226
+ onDeviceRemoved,
1227
+ renderDeviceIcon,
1228
+ className
1229
+ }) {
1230
+ const { devices, isLoading, error, refresh, trustDevice, untrustDevice, removeDevice } = useDevices();
1231
+ const { localization } = useAuthLocalization();
1232
+ useEffect(() => {
1233
+ refresh();
1234
+ }, [refresh]);
1235
+ const handleTrustToggle = useCallback(
1236
+ async (device) => {
1237
+ if (device.isTrusted) {
1238
+ await untrustDevice(device.id);
1239
+ onDeviceUntrusted?.(device);
1240
+ } else {
1241
+ await trustDevice(device.id);
1242
+ onDeviceTrusted?.(device);
1243
+ }
1244
+ },
1245
+ [trustDevice, untrustDevice, onDeviceTrusted, onDeviceUntrusted]
1246
+ );
1247
+ const handleRemove = useCallback(
1248
+ async (device) => {
1249
+ await removeDevice(device.id);
1250
+ onDeviceRemoved?.(device);
1251
+ },
1252
+ [removeDevice, onDeviceRemoved]
1253
+ );
1254
+ if (isLoading && devices.length === 0) {
1255
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsx("div", { className: "p-6 text-center text-sm text-gray-500", children: localization.LOADING_DEVICES }) });
1256
+ }
1257
+ if (error) {
1258
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsx("div", { className: "p-6 text-center text-sm text-red-600", children: error.message }) });
1259
+ }
1260
+ if (devices.length === 0) {
1261
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsx("div", { className: "p-6 text-center text-sm text-gray-500", children: localization.NO_DEVICES }) });
1262
+ }
1263
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsx("div", { className: "p-6 space-y-4", children: devices.map((device) => /* @__PURE__ */ jsxs(
1264
+ "div",
1265
+ {
1266
+ className: "flex items-start justify-between gap-4 rounded border p-4",
1267
+ children: [
1268
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
1269
+ /* @__PURE__ */ jsx("span", { className: "text-2xl", children: renderDeviceIcon ? renderDeviceIcon(device) : DEFAULT_DEVICE_ICONS[device.deviceType] ?? "\u{1F4BB}" }),
1270
+ /* @__PURE__ */ jsxs("div", { children: [
1271
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1272
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: device.name }),
1273
+ device.isCurrent && /* @__PURE__ */ jsx("span", { className: "rounded bg-blue-100 px-2 py-0.5 text-xs text-blue-700", children: localization.CURRENT_DEVICE })
1274
+ ] }),
1275
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-gray-500", children: [
1276
+ device.browser,
1277
+ " \xB7 ",
1278
+ device.os
1279
+ ] }),
1280
+ (device.lastCity || device.lastCountry) && /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500", children: [device.lastCity, device.lastCountry].filter(Boolean).join(", ") }),
1281
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-400", children: [
1282
+ localization.LAST_USED,
1283
+ " ",
1284
+ device.lastUsedAt,
1285
+ " \xB7 ",
1286
+ localization.IP_LABEL,
1287
+ " ",
1288
+ device.lastIp
1289
+ ] })
1290
+ ] })
1291
+ ] }),
1292
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1293
+ showTrustToggle && /* @__PURE__ */ jsx(
1294
+ Button,
1295
+ {
1296
+ type: "button",
1297
+ variant: "outline",
1298
+ size: "sm",
1299
+ onClick: () => handleTrustToggle(device),
1300
+ children: device.isTrusted ? localization.UNTRUST_DEVICE : localization.TRUST_DEVICE
1301
+ }
1302
+ ),
1303
+ showRemoveAction && /* @__PURE__ */ jsx(
1304
+ Button,
1305
+ {
1306
+ type: "button",
1307
+ variant: "destructive",
1308
+ size: "sm",
1309
+ disabled: device.isCurrent,
1310
+ onClick: () => handleRemove(device),
1311
+ children: localization.REMOVE
1312
+ }
1313
+ )
1314
+ ] })
1315
+ ]
1316
+ },
1317
+ device.id
1318
+ )) }) });
1319
+ }
1320
+ function LoadingBoundary({
1321
+ children,
1322
+ spinner = /* @__PURE__ */ jsx("div", { children: "Loading..." }),
1323
+ delay = 0,
1324
+ className
1325
+ }) {
1326
+ const { isLoading } = useSession();
1327
+ const [showSpinner, setShowSpinner] = useState(delay === 0 && isLoading);
1328
+ useEffect(() => {
1329
+ if (!isLoading) {
1330
+ setShowSpinner(false);
1331
+ return;
1332
+ }
1333
+ if (delay === 0) {
1334
+ setShowSpinner(true);
1335
+ return;
1336
+ }
1337
+ const timer = setTimeout(() => {
1338
+ setShowSpinner(true);
1339
+ }, delay);
1340
+ return () => clearTimeout(timer);
1341
+ }, [isLoading, delay]);
1342
+ if (isLoading && showSpinner) {
1343
+ return className ? /* @__PURE__ */ jsx("div", { className, children: spinner }) : /* @__PURE__ */ jsx(Fragment, { children: spinner });
1344
+ }
1345
+ if (isLoading) {
1346
+ return null;
1347
+ }
1348
+ return /* @__PURE__ */ jsx(Fragment, { children });
1349
+ }
1350
+ function SubmitButton({ loading, loadingLabel, actionLabel, className }) {
1351
+ return /* @__PURE__ */ jsx(Button, { type: "submit", disabled: loading, className: className ?? "w-full", children: loading ? loadingLabel : actionLabel });
1352
+ }
1353
+
1354
+ // src/helpers/risk-utils.ts
1355
+ function getRiskLevel(score) {
1356
+ if (score < 30) return "low";
1357
+ if (score < 60) return "medium";
1358
+ return "high";
1359
+ }
1360
+ var RISK_COLORS = {
1361
+ low: "text-green-600 bg-green-50",
1362
+ medium: "text-yellow-600 bg-yellow-50",
1363
+ high: "text-red-600 bg-red-50"
1364
+ };
1365
+ function LoginActivityFeed({
1366
+ limit = 20,
1367
+ showRiskBadge = false,
1368
+ showReportActions = false,
1369
+ filterByStatus,
1370
+ onSuspiciousReported,
1371
+ className
1372
+ }) {
1373
+ const {
1374
+ events,
1375
+ hasMore,
1376
+ isLoading,
1377
+ error,
1378
+ refresh,
1379
+ loadMore,
1380
+ filter,
1381
+ markRecognized,
1382
+ reportSuspicious
1383
+ } = useLoginActivity({ limit });
1384
+ const { localization } = useAuthLocalization();
1385
+ useEffect(() => {
1386
+ if (filterByStatus) {
1387
+ filter({ status: filterByStatus, limit });
1388
+ }
1389
+ refresh();
1390
+ }, [refresh, filter, filterByStatus, limit]);
1391
+ const handleMarkRecognized = useCallback(
1392
+ async (event) => {
1393
+ await markRecognized(event.id);
1394
+ },
1395
+ [markRecognized]
1396
+ );
1397
+ const handleReportSuspicious = useCallback(
1398
+ async (event) => {
1399
+ await reportSuspicious(event.id);
1400
+ onSuspiciousReported?.(event);
1401
+ },
1402
+ [reportSuspicious, onSuspiciousReported]
1403
+ );
1404
+ if (isLoading && events.length === 0) {
1405
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsx("div", { className: "p-6 text-center text-sm text-gray-500", children: localization.LOADING_ACTIVITY }) });
1406
+ }
1407
+ if (error) {
1408
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsx("div", { className: "p-6 text-center text-sm text-red-600", children: error.message }) });
1409
+ }
1410
+ if (events.length === 0) {
1411
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsx("div", { className: "p-6 text-center text-sm text-gray-500", children: localization.NO_ACTIVITY }) });
1412
+ }
1413
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs("div", { className: "p-6 space-y-4", children: [
1414
+ events.map((event) => {
1415
+ const riskLevel = getRiskLevel(event.riskScore);
1416
+ return /* @__PURE__ */ jsxs(
1417
+ "div",
1418
+ {
1419
+ className: "flex items-start justify-between gap-4 rounded border p-4",
1420
+ children: [
1421
+ /* @__PURE__ */ jsxs("div", { children: [
1422
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1423
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: event.deviceName }),
1424
+ /* @__PURE__ */ jsx("span", { className: "rounded bg-gray-100 px-2 py-0.5 text-xs text-gray-600", children: event.status }),
1425
+ showRiskBadge && /* @__PURE__ */ jsx("span", { className: `rounded px-2 py-0.5 text-xs ${RISK_COLORS[riskLevel]}`, children: riskLevel })
1426
+ ] }),
1427
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-gray-500", children: [
1428
+ event.method,
1429
+ " \xB7 ",
1430
+ event.ip
1431
+ ] }),
1432
+ (event.city || event.country) && /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500", children: [event.city, event.country].filter(Boolean).join(", ") }),
1433
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: event.createdAt })
1434
+ ] }),
1435
+ showReportActions && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1436
+ /* @__PURE__ */ jsx(
1437
+ Button,
1438
+ {
1439
+ type: "button",
1440
+ variant: "outline",
1441
+ size: "sm",
1442
+ onClick: () => handleMarkRecognized(event),
1443
+ children: localization.MARK_RECOGNIZED
1444
+ }
1445
+ ),
1446
+ /* @__PURE__ */ jsx(
1447
+ Button,
1448
+ {
1449
+ type: "button",
1450
+ variant: "destructive",
1451
+ size: "sm",
1452
+ onClick: () => handleReportSuspicious(event),
1453
+ children: localization.REPORT_SUSPICIOUS
1454
+ }
1455
+ )
1456
+ ] })
1457
+ ]
1458
+ },
1459
+ event.id
1460
+ );
1461
+ }),
1462
+ hasMore && /* @__PURE__ */ jsx("div", { className: "text-center", children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", disabled: isLoading, onClick: loadMore, children: isLoading ? localization.LOADING : localization.LOAD_MORE }) })
1463
+ ] }) });
1464
+ }
1465
+ function toAuthError(error) {
1466
+ if (error instanceof AuthError) return error;
1467
+ const message = error instanceof Error ? error.message : String(error);
1468
+ return new AuthError({
1469
+ code: AuthErrorCode.NetworkError,
1470
+ message,
1471
+ statusCode: 0
1472
+ });
1473
+ }
1474
+ var credentialLoginSchema = z.object({
1475
+ identity: z.string().min(1, "Email or username is required"),
1476
+ password: z.string().min(1, "Password is required"),
1477
+ captchaToken: z.string().optional()
1478
+ });
1479
+ var phoneLoginSchema = z.object({
1480
+ countryCode: z.string(),
1481
+ phoneNumber: z.string().min(1, "Phone number is required")
1482
+ });
1483
+ var otpVerifySchema = z.object({
1484
+ code: z.string().length(6, "Code must be 6 digits")
1485
+ });
1486
+
1487
+ // src/components/shared/utils.ts
1488
+ function isEmail(value) {
1489
+ return value.includes("@");
1490
+ }
1491
+ var PROVIDER_NAMES = {
1492
+ google: "Google",
1493
+ github: "GitHub",
1494
+ apple: "Apple",
1495
+ microsoft: "Microsoft",
1496
+ atzentis: "Atzentis"
1497
+ };
1498
+ function OAuthButton({
1499
+ provider,
1500
+ redirectUri,
1501
+ state,
1502
+ variant = "outline",
1503
+ size = "md",
1504
+ className
1505
+ }) {
1506
+ const { getOAuthUrl } = useAuth();
1507
+ const { localization } = useAuthLocalization();
1508
+ const [isLoading, setIsLoading] = useState(false);
1509
+ const sizeMap = {
1510
+ sm: "sm",
1511
+ md: "default",
1512
+ lg: "lg"
1513
+ };
1514
+ const providerName = PROVIDER_NAMES[provider];
1515
+ const handleClick = useCallback(async () => {
1516
+ setIsLoading(true);
1517
+ try {
1518
+ const url = await getOAuthUrl({ provider, redirectUri, state: state ?? "" });
1519
+ window.location.href = url;
1520
+ } catch {
1521
+ setIsLoading(false);
1522
+ }
1523
+ }, [getOAuthUrl, provider, redirectUri, state]);
1524
+ return /* @__PURE__ */ jsx(
1525
+ Button,
1526
+ {
1527
+ type: "button",
1528
+ variant: variant === "filled" ? "default" : "outline",
1529
+ size: sizeMap[size],
1530
+ disabled: isLoading,
1531
+ onClick: handleClick,
1532
+ "aria-label": `${localization.SIGN_IN_WITH} ${providerName}`,
1533
+ className,
1534
+ children: isLoading ? localization.REDIRECTING : `${localization.CONTINUE_WITH} ${providerName}`
1535
+ }
1536
+ );
1537
+ }
1538
+ var phoneNumberSchema = z.object({
1539
+ countryCode: z.string(),
1540
+ phoneNumber: z.string().min(1, "Phone number is required")
1541
+ });
1542
+ var otpVerifySchema2 = z.object({
1543
+ code: z.string().length(6, "Code must be 6 digits")
1544
+ });
1545
+ var phonePasswordSchema = z.object({
1546
+ countryCode: z.string(),
1547
+ phoneNumber: z.string().min(1, "Phone number is required"),
1548
+ password: z.string().min(1, "Password is required")
1549
+ });
1550
+
1551
+ // src/components/shared/country-codes.ts
1552
+ var COUNTRY_CODES = [
1553
+ { code: "+30", country: "GR", name: "Greece", flag: "\u{1F1EC}\u{1F1F7}" },
1554
+ { code: "+49", country: "DE", name: "Germany", flag: "\u{1F1E9}\u{1F1EA}" },
1555
+ { code: "+1", country: "US", name: "United States", flag: "\u{1F1FA}\u{1F1F8}" },
1556
+ { code: "+44", country: "GB", name: "United Kingdom", flag: "\u{1F1EC}\u{1F1E7}" },
1557
+ { code: "+33", country: "FR", name: "France", flag: "\u{1F1EB}\u{1F1F7}" },
1558
+ { code: "+39", country: "IT", name: "Italy", flag: "\u{1F1EE}\u{1F1F9}" },
1559
+ { code: "+34", country: "ES", name: "Spain", flag: "\u{1F1EA}\u{1F1F8}" },
1560
+ { code: "+31", country: "NL", name: "Netherlands", flag: "\u{1F1F3}\u{1F1F1}" },
1561
+ { code: "+43", country: "AT", name: "Austria", flag: "\u{1F1E6}\u{1F1F9}" },
1562
+ { code: "+41", country: "CH", name: "Switzerland", flag: "\u{1F1E8}\u{1F1ED}" },
1563
+ { code: "+357", country: "CY", name: "Cyprus", flag: "\u{1F1E8}\u{1F1FE}" },
1564
+ { code: "+90", country: "TR", name: "Turkey", flag: "\u{1F1F9}\u{1F1F7}" }
1565
+ ];
1566
+ function PhoneLoginForm({
1567
+ onSuccess,
1568
+ onError,
1569
+ allowedCountryCodes = ["+30", "+49"],
1570
+ showPasswordFallback = false,
1571
+ className
1572
+ }) {
1573
+ const { sendOTP, verify, signIn, isSending, isVerifying, expiresIn } = usePhone();
1574
+ const { localization, localizeErrors } = useAuthLocalization();
1575
+ const [step, setStep] = useState("phone");
1576
+ const [formError, setFormError] = useState(null);
1577
+ const [fullPhoneNumber, setFullPhoneNumber] = useState("");
1578
+ const filteredCountryCodes = useMemo(
1579
+ () => COUNTRY_CODES.filter((c) => allowedCountryCodes.includes(c.code)),
1580
+ [allowedCountryCodes]
1581
+ );
1582
+ const phoneForm = useForm({
1583
+ resolver: zodResolver(phoneNumberSchema),
1584
+ defaultValues: {
1585
+ countryCode: filteredCountryCodes[0]?.code ?? "+30",
1586
+ phoneNumber: ""
1587
+ }
1588
+ });
1589
+ const otpForm = useForm({
1590
+ resolver: zodResolver(otpVerifySchema2),
1591
+ defaultValues: { code: "" }
1592
+ });
1593
+ const [passwordValue, setPasswordValue] = useState("");
1594
+ const countryCodeOptions = filteredCountryCodes.map((c) => ({
1595
+ value: c.code,
1596
+ label: `${c.flag} ${c.name} (${c.code})`
1597
+ }));
1598
+ const handlePhoneSubmit = useCallback(
1599
+ async (data) => {
1600
+ setFormError(null);
1601
+ const phone = `${data.countryCode}${data.phoneNumber}`;
1602
+ setFullPhoneNumber(phone);
1603
+ try {
1604
+ await sendOTP(phone);
1605
+ setStep("otp");
1606
+ } catch (err) {
1607
+ setFormError(getLocalizedError(err, localization, localizeErrors));
1608
+ onError?.(toAuthError(err));
1609
+ }
1610
+ },
1611
+ [sendOTP, onError, localization, localizeErrors]
1612
+ );
1613
+ const handleOtpSubmit = useCallback(
1614
+ async (data) => {
1615
+ setFormError(null);
1616
+ try {
1617
+ const response = await verify(fullPhoneNumber, data.code);
1618
+ onSuccess(response.user);
1619
+ } catch (err) {
1620
+ setFormError(getLocalizedError(err, localization, localizeErrors));
1621
+ onError?.(toAuthError(err));
1622
+ }
1623
+ },
1624
+ [verify, fullPhoneNumber, onSuccess, onError, localization, localizeErrors]
1625
+ );
1626
+ const handleOtpComplete = useCallback(
1627
+ (code) => {
1628
+ otpForm.setValue("code", code);
1629
+ otpForm.handleSubmit(handleOtpSubmit)();
1630
+ },
1631
+ [otpForm, handleOtpSubmit]
1632
+ );
1633
+ const handlePasswordSubmit = useCallback(
1634
+ async (e) => {
1635
+ e.preventDefault();
1636
+ setFormError(null);
1637
+ try {
1638
+ const response = await signIn(fullPhoneNumber, passwordValue);
1639
+ onSuccess(response.user);
1640
+ } catch (err) {
1641
+ setFormError(getLocalizedError(err, localization, localizeErrors));
1642
+ onError?.(toAuthError(err));
1643
+ }
1644
+ },
1645
+ [signIn, fullPhoneNumber, passwordValue, onSuccess, onError, localization, localizeErrors]
1646
+ );
1647
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs("div", { className: "p-6 space-y-4", children: [
1648
+ step === "phone" && /* @__PURE__ */ jsxs("form", { onSubmit: phoneForm.handleSubmit(handlePhoneSubmit), className: "space-y-4", children: [
1649
+ /* @__PURE__ */ jsx(
1650
+ FormSelect,
1651
+ {
1652
+ label: localization.COUNTRY_CODE,
1653
+ options: countryCodeOptions,
1654
+ value: phoneForm.watch("countryCode"),
1655
+ onValueChange: (value) => phoneForm.setValue("countryCode", value)
1656
+ }
1657
+ ),
1658
+ /* @__PURE__ */ jsx(
1659
+ FormInput,
1660
+ {
1661
+ label: localization.PHONE_NUMBER_PLACEHOLDER,
1662
+ id: "phone-number",
1663
+ type: "tel",
1664
+ error: phoneForm.formState.errors.phoneNumber?.message,
1665
+ ...phoneForm.register("phoneNumber")
1666
+ }
1667
+ ),
1668
+ formError && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-red-600", children: formError }),
1669
+ /* @__PURE__ */ jsx(
1670
+ SubmitButton,
1671
+ {
1672
+ loading: isSending,
1673
+ loadingLabel: localization.SENDING_VERIFICATION_CODE,
1674
+ actionLabel: localization.SEND_VERIFICATION_CODE
1675
+ }
1676
+ )
1677
+ ] }),
1678
+ step === "otp" && /* @__PURE__ */ jsxs("form", { onSubmit: otpForm.handleSubmit(handleOtpSubmit), className: "space-y-4", children: [
1679
+ /* @__PURE__ */ jsxs("label", { className: "block text-sm font-medium", children: [
1680
+ localization.VERIFICATION_CODE_LABEL,
1681
+ /* @__PURE__ */ jsx(OTPInput, { length: 6, onComplete: handleOtpComplete, disabled: isVerifying })
1682
+ ] }),
1683
+ expiresIn !== null && /* @__PURE__ */ jsxs("p", { className: "text-sm text-gray-500", children: [
1684
+ localization.CODE_EXPIRES_IN,
1685
+ " ",
1686
+ expiresIn,
1687
+ "s"
1688
+ ] }),
1689
+ formError && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-red-600", children: formError }),
1690
+ /* @__PURE__ */ jsx(
1691
+ SubmitButton,
1692
+ {
1693
+ loading: isVerifying,
1694
+ loadingLabel: localization.VERIFYING,
1695
+ actionLabel: localization.VERIFY
1696
+ }
1697
+ ),
1698
+ showPasswordFallback && /* @__PURE__ */ jsx(
1699
+ Button,
1700
+ {
1701
+ variant: "link",
1702
+ type: "button",
1703
+ className: "p-0 h-auto text-sm",
1704
+ onClick: () => setStep("password"),
1705
+ children: localization.USE_PASSWORD_INSTEAD
1706
+ }
1707
+ )
1708
+ ] }),
1709
+ step === "password" && /* @__PURE__ */ jsxs("form", { onSubmit: handlePasswordSubmit, className: "space-y-4", children: [
1710
+ /* @__PURE__ */ jsx(
1711
+ FormPasswordInput,
1712
+ {
1713
+ label: localization.PASSWORD,
1714
+ id: "phone-password",
1715
+ value: passwordValue,
1716
+ onChange: (e) => setPasswordValue(e.target.value)
1717
+ }
1718
+ ),
1719
+ formError && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-red-600", children: formError }),
1720
+ /* @__PURE__ */ jsx(
1721
+ SubmitButton,
1722
+ {
1723
+ loading: isVerifying,
1724
+ loadingLabel: localization.SIGNING_IN,
1725
+ actionLabel: localization.SIGN_IN
1726
+ }
1727
+ )
1728
+ ] })
1729
+ ] }) });
1730
+ }
1731
+ function LoginForm({
1732
+ onSuccess,
1733
+ onError,
1734
+ enablePhone = false,
1735
+ enableForgotPassword = false,
1736
+ onForgotPasswordClick,
1737
+ className,
1738
+ ...oauthProps
1739
+ }) {
1740
+ const oauthProviders = "oauthProviders" in oauthProps ? oauthProps.oauthProviders : void 0;
1741
+ const oauthRedirectUri = "oauthRedirectUri" in oauthProps ? oauthProps.oauthRedirectUri : "";
1742
+ const { login, loginWithUsername } = useAuth();
1743
+ const { localization, localizeErrors } = useAuthLocalization();
1744
+ const [mode, setMode] = useState("credential");
1745
+ const [formError, setFormError] = useState(null);
1746
+ const [isSubmitting, setIsSubmitting] = useState(false);
1747
+ const {
1748
+ register,
1749
+ handleSubmit,
1750
+ watch,
1751
+ formState: { errors }
1752
+ } = useForm({
1753
+ resolver: zodResolver(credentialLoginSchema),
1754
+ defaultValues: { identity: "", password: "" }
1755
+ });
1756
+ const identityValue = watch("identity");
1757
+ const onSubmit = useCallback(
1758
+ async (data) => {
1759
+ setFormError(null);
1760
+ setIsSubmitting(true);
1761
+ try {
1762
+ if (isEmail(data.identity)) {
1763
+ const response = await login({
1764
+ email: data.identity,
1765
+ password: data.password,
1766
+ captchaToken: data.captchaToken
1767
+ });
1768
+ onSuccess(response.user);
1769
+ } else {
1770
+ const response = await loginWithUsername({
1771
+ username: data.identity,
1772
+ password: data.password,
1773
+ captchaToken: data.captchaToken
1774
+ });
1775
+ onSuccess(response.user);
1776
+ }
1777
+ } catch (err) {
1778
+ setFormError(getLocalizedError(err, localization, localizeErrors));
1779
+ onError?.(toAuthError(err));
1780
+ } finally {
1781
+ setIsSubmitting(false);
1782
+ }
1783
+ },
1784
+ [login, loginWithUsername, onSuccess, onError, localization, localizeErrors]
1785
+ );
1786
+ const handleSwitchToPhone = useCallback(() => {
1787
+ setMode("phone");
1788
+ setFormError(null);
1789
+ }, []);
1790
+ const handleSwitchToCredential = useCallback(() => {
1791
+ setMode("credential");
1792
+ setFormError(null);
1793
+ }, []);
1794
+ if (mode === "phone") {
1795
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs("div", { className: "p-6 space-y-4", children: [
1796
+ /* @__PURE__ */ jsx(PhoneLoginForm, { onSuccess, onError }),
1797
+ /* @__PURE__ */ jsx(
1798
+ Button,
1799
+ {
1800
+ variant: "link",
1801
+ type: "button",
1802
+ className: "p-0 h-auto text-sm",
1803
+ onClick: handleSwitchToCredential,
1804
+ children: localization.USE_EMAIL_INSTEAD
1805
+ }
1806
+ )
1807
+ ] }) });
1808
+ }
1809
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit(onSubmit), className: "p-6 space-y-4", children: [
1810
+ /* @__PURE__ */ jsx(
1811
+ FormInput,
1812
+ {
1813
+ label: localization.SIGN_IN_USERNAME_PLACEHOLDER,
1814
+ id: "login-identity",
1815
+ type: "text",
1816
+ autoComplete: "username",
1817
+ error: errors.identity?.message,
1818
+ ...register("identity")
1819
+ }
1820
+ ),
1821
+ identityValue && /* @__PURE__ */ jsx(
1822
+ FormPasswordInput,
1823
+ {
1824
+ label: localization.PASSWORD,
1825
+ id: "login-password",
1826
+ autoComplete: "current-password",
1827
+ error: errors.password?.message,
1828
+ ...register("password")
1829
+ }
1830
+ ),
1831
+ formError && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-red-600", children: formError }),
1832
+ /* @__PURE__ */ jsx(
1833
+ SubmitButton,
1834
+ {
1835
+ loading: isSubmitting,
1836
+ loadingLabel: localization.SIGNING_IN,
1837
+ actionLabel: localization.SIGN_IN
1838
+ }
1839
+ ),
1840
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-sm", children: [
1841
+ enableForgotPassword && /* @__PURE__ */ jsx(
1842
+ Button,
1843
+ {
1844
+ variant: "link",
1845
+ type: "button",
1846
+ className: "p-0 h-auto",
1847
+ onClick: onForgotPasswordClick,
1848
+ children: localization.FORGOT_PASSWORD_LINK
1849
+ }
1850
+ ),
1851
+ enablePhone && /* @__PURE__ */ jsx(
1852
+ Button,
1853
+ {
1854
+ variant: "link",
1855
+ type: "button",
1856
+ className: "p-0 h-auto",
1857
+ onClick: handleSwitchToPhone,
1858
+ children: localization.USE_PHONE_INSTEAD
1859
+ }
1860
+ )
1861
+ ] }),
1862
+ oauthProviders && oauthProviders.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
1863
+ /* @__PURE__ */ jsxs("div", { className: "relative flex items-center py-2", children: [
1864
+ /* @__PURE__ */ jsx("div", { className: "flex-grow border-t border-gray-300" }),
1865
+ /* @__PURE__ */ jsx("span", { className: "mx-4 text-sm text-gray-500", children: localization.OR_CONTINUE_WITH }),
1866
+ /* @__PURE__ */ jsx("div", { className: "flex-grow border-t border-gray-300" })
1867
+ ] }),
1868
+ /* @__PURE__ */ jsx("div", { className: "space-y-2", children: oauthProviders.map((provider) => /* @__PURE__ */ jsx(OAuthButton, { provider, redirectUri: oauthRedirectUri }, provider)) })
1869
+ ] })
1870
+ ] }) });
1871
+ }
1872
+ var magicLinkSchema = z.object({
1873
+ email: z.string().email("Invalid email address")
1874
+ });
1875
+ function maskEmail(email) {
1876
+ const [local, domain] = email.split("@");
1877
+ if (!local || !domain) return email;
1878
+ const masked = local.length <= 2 ? `${local[0]}***` : `${local[0]}***${local[local.length - 1]}`;
1879
+ return `${masked}@${domain}`;
1880
+ }
1881
+ function MagicLinkForm({ redirectUrl, onSuccess, onError, className }) {
1882
+ const { sendMagicLink } = useAuth();
1883
+ const { localization, localizeErrors } = useAuthLocalization();
1884
+ const [sent, setSent] = useState(false);
1885
+ const [sentEmail, setSentEmail] = useState("");
1886
+ const [isSubmitting, setIsSubmitting] = useState(false);
1887
+ const [formError, setFormError] = useState(null);
1888
+ const {
1889
+ register,
1890
+ handleSubmit,
1891
+ formState: { errors },
1892
+ getValues
1893
+ } = useForm({
1894
+ resolver: zodResolver(magicLinkSchema),
1895
+ defaultValues: { email: "" }
1896
+ });
1897
+ const onSubmit = useCallback(
1898
+ async (data) => {
1899
+ setFormError(null);
1900
+ setIsSubmitting(true);
1901
+ try {
1902
+ await sendMagicLink({ email: data.email, redirectUrl });
1903
+ setSent(true);
1904
+ setSentEmail(data.email);
1905
+ onSuccess?.();
1906
+ } catch (err) {
1907
+ setFormError(getLocalizedError(err, localization, localizeErrors));
1908
+ onError?.(toAuthError(err));
1909
+ } finally {
1910
+ setIsSubmitting(false);
1911
+ }
1912
+ },
1913
+ [sendMagicLink, redirectUrl, onSuccess, onError, localization, localizeErrors]
1914
+ );
1915
+ const handleResend = useCallback(async () => {
1916
+ const email = getValues("email");
1917
+ setFormError(null);
1918
+ setIsSubmitting(true);
1919
+ try {
1920
+ await sendMagicLink({ email, redirectUrl });
1921
+ } catch (err) {
1922
+ setFormError(getLocalizedError(err, localization, localizeErrors));
1923
+ } finally {
1924
+ setIsSubmitting(false);
1925
+ }
1926
+ }, [sendMagicLink, redirectUrl, getValues, localization, localizeErrors]);
1927
+ if (sent) {
1928
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs("div", { className: "p-6 space-y-4 text-center", children: [
1929
+ /* @__PURE__ */ jsxs("p", { className: "text-sm", children: [
1930
+ localization.MAGIC_LINK_EMAIL,
1931
+ " ",
1932
+ /* @__PURE__ */ jsx("strong", { children: maskEmail(sentEmail) })
1933
+ ] }),
1934
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500", children: localization.MAGIC_LINK_HINT }),
1935
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", disabled: isSubmitting, onClick: handleResend, children: isSubmitting ? localization.RESENDING : localization.RESEND }),
1936
+ formError && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-red-600", children: formError })
1937
+ ] }) });
1938
+ }
1939
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit(onSubmit), className: "p-6 space-y-4", children: [
1940
+ /* @__PURE__ */ jsx(
1941
+ FormInput,
1942
+ {
1943
+ label: localization.EMAIL,
1944
+ id: "magic-link-email",
1945
+ type: "email",
1946
+ autoComplete: "email",
1947
+ error: errors.email?.message,
1948
+ ...register("email")
1949
+ }
1950
+ ),
1951
+ formError && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-red-600", children: formError }),
1952
+ /* @__PURE__ */ jsx(
1953
+ SubmitButton,
1954
+ {
1955
+ loading: isSubmitting,
1956
+ loadingLabel: localization.SENDING,
1957
+ actionLabel: localization.MAGIC_LINK_ACTION
1958
+ }
1959
+ )
1960
+ ] }) });
1961
+ }
1962
+ var signupSchema = z.object({
1963
+ email: z.string().email("Invalid email address"),
1964
+ password: z.string().min(8, "Password must be at least 8 characters"),
1965
+ name: z.string().min(1, "Name is required"),
1966
+ username: z.string().optional(),
1967
+ phoneNumber: z.string().optional(),
1968
+ organizationName: z.string().optional(),
1969
+ acceptTerms: z.boolean(),
1970
+ captchaToken: z.string().optional()
1971
+ });
1972
+ function SignupForm({
1973
+ onSuccess,
1974
+ onError,
1975
+ showUsernameField = false,
1976
+ showPhoneField = false,
1977
+ organizationName,
1978
+ acceptTermsRequired = false,
1979
+ onLoginClick,
1980
+ className
1981
+ }) {
1982
+ const { signup } = useAuth();
1983
+ const { localization, localizeErrors } = useAuthLocalization();
1984
+ const [formError, setFormError] = useState(null);
1985
+ const [isSubmitting, setIsSubmitting] = useState(false);
1986
+ const {
1987
+ register,
1988
+ handleSubmit,
1989
+ formState: { errors }
1990
+ } = useForm({
1991
+ resolver: zodResolver(
1992
+ acceptTermsRequired ? signupSchema.refine((data) => data.acceptTerms === true, {
1993
+ message: localization.ACCEPT_TERMS_ERROR,
1994
+ path: ["acceptTerms"]
1995
+ }) : signupSchema
1996
+ ),
1997
+ defaultValues: {
1998
+ email: "",
1999
+ password: "",
2000
+ name: "",
2001
+ username: "",
2002
+ phoneNumber: "",
2003
+ organizationName: organizationName ?? "",
2004
+ acceptTerms: false
2005
+ }
2006
+ });
2007
+ const onSubmit = useCallback(
2008
+ async (data) => {
2009
+ setFormError(null);
2010
+ setIsSubmitting(true);
2011
+ try {
2012
+ const response = await signup({
2013
+ email: data.email,
2014
+ password: data.password,
2015
+ name: data.name,
2016
+ username: data.username,
2017
+ organizationName: data.organizationName,
2018
+ acceptTerms: data.acceptTerms
2019
+ });
2020
+ onSuccess(response.user);
2021
+ } catch (err) {
2022
+ setFormError(getLocalizedError(err, localization, localizeErrors));
2023
+ onError?.(toAuthError(err));
2024
+ } finally {
2025
+ setIsSubmitting(false);
2026
+ }
2027
+ },
2028
+ [signup, onSuccess, onError, localization, localizeErrors]
2029
+ );
2030
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit(onSubmit), className: "p-6 space-y-4", children: [
2031
+ /* @__PURE__ */ jsx(
2032
+ FormInput,
2033
+ {
2034
+ label: localization.NAME,
2035
+ id: "signup-name",
2036
+ type: "text",
2037
+ autoComplete: "name",
2038
+ error: errors.name?.message,
2039
+ ...register("name")
2040
+ }
2041
+ ),
2042
+ /* @__PURE__ */ jsx(
2043
+ FormInput,
2044
+ {
2045
+ label: localization.EMAIL,
2046
+ id: "signup-email",
2047
+ type: "email",
2048
+ autoComplete: "email",
2049
+ error: errors.email?.message,
2050
+ ...register("email")
2051
+ }
2052
+ ),
2053
+ /* @__PURE__ */ jsx(
2054
+ FormPasswordInput,
2055
+ {
2056
+ label: localization.PASSWORD,
2057
+ id: "signup-password",
2058
+ autoComplete: "new-password",
2059
+ error: errors.password?.message,
2060
+ ...register("password")
2061
+ }
2062
+ ),
2063
+ showUsernameField && /* @__PURE__ */ jsx(
2064
+ FormInput,
2065
+ {
2066
+ label: localization.USERNAME,
2067
+ id: "signup-username",
2068
+ type: "text",
2069
+ autoComplete: "username",
2070
+ error: errors.username?.message,
2071
+ ...register("username")
2072
+ }
2073
+ ),
2074
+ showPhoneField && /* @__PURE__ */ jsx(
2075
+ FormInput,
2076
+ {
2077
+ label: localization.PHONE_NUMBER_PLACEHOLDER,
2078
+ id: "signup-phone",
2079
+ type: "tel",
2080
+ autoComplete: "tel",
2081
+ error: errors.phoneNumber?.message,
2082
+ ...register("phoneNumber")
2083
+ }
2084
+ ),
2085
+ acceptTermsRequired && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2086
+ /* @__PURE__ */ jsx(
2087
+ "input",
2088
+ {
2089
+ id: "signup-terms",
2090
+ type: "checkbox",
2091
+ "aria-invalid": errors.acceptTerms ? true : void 0,
2092
+ ...register("acceptTerms")
2093
+ }
2094
+ ),
2095
+ /* @__PURE__ */ jsx("label", { htmlFor: "signup-terms", className: "text-sm", children: localization.ACCEPT_TERMS }),
2096
+ errors.acceptTerms && /* @__PURE__ */ jsx("p", { className: "text-sm text-red-600", children: errors.acceptTerms.message })
2097
+ ] }),
2098
+ formError && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-red-600", children: formError }),
2099
+ /* @__PURE__ */ jsx(
2100
+ SubmitButton,
2101
+ {
2102
+ loading: isSubmitting,
2103
+ loadingLabel: localization.SIGNING_IN,
2104
+ actionLabel: localization.SIGN_UP_ACTION
2105
+ }
2106
+ ),
2107
+ onLoginClick && /* @__PURE__ */ jsxs("p", { className: "text-sm text-center", children: [
2108
+ localization.ALREADY_HAVE_AN_ACCOUNT,
2109
+ " ",
2110
+ /* @__PURE__ */ jsx(
2111
+ Button,
2112
+ {
2113
+ variant: "link",
2114
+ type: "button",
2115
+ className: "p-0 h-auto text-sm",
2116
+ onClick: onLoginClick,
2117
+ children: localization.SIGN_IN
2118
+ }
2119
+ )
2120
+ ] })
2121
+ ] }) });
2122
+ }
2123
+ function OrganizationMenu({
2124
+ showCreateNew = false,
2125
+ currentOrgId,
2126
+ onSwitch,
2127
+ onCreateNew,
2128
+ className
2129
+ }) {
2130
+ const { organizations, isLoading, refresh } = useOrganizations();
2131
+ const { localization } = useAuthLocalization();
2132
+ useEffect(() => {
2133
+ refresh();
2134
+ }, [refresh]);
2135
+ const currentOrg = organizations.find((o) => o.id === currentOrgId);
2136
+ return /* @__PURE__ */ jsx("div", { className, children: /* @__PURE__ */ jsxs(DropdownMenu, { children: [
2137
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "outline", disabled: isLoading, children: currentOrg ? /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
2138
+ currentOrg.logo ? /* @__PURE__ */ jsx("img", { src: currentOrg.logo, alt: "", className: "h-5 w-5 rounded" }) : /* @__PURE__ */ jsx("span", { className: "flex h-5 w-5 items-center justify-center rounded bg-gray-200 text-xs", children: getInitials(currentOrg.name) }),
2139
+ currentOrg.name
2140
+ ] }) : localization.SELECT_ORGANIZATION }) }),
2141
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", children: [
2142
+ organizations.map((org) => /* @__PURE__ */ jsx(DropdownMenuItem, { onClick: () => onSwitch?.(org), children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2143
+ org.logo ? /* @__PURE__ */ jsx("img", { src: org.logo, alt: "", className: "h-5 w-5 rounded" }) : /* @__PURE__ */ jsx("span", { className: "flex h-5 w-5 items-center justify-center rounded bg-gray-200 text-xs", children: getInitials(org.name) }),
2144
+ /* @__PURE__ */ jsx("span", { children: org.name }),
2145
+ org.id === currentOrgId && /* @__PURE__ */ jsx("span", { className: "ml-auto", children: "\u2713" })
2146
+ ] }) }, org.id)),
2147
+ showCreateNew && /* @__PURE__ */ jsxs(Fragment, { children: [
2148
+ /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
2149
+ /* @__PURE__ */ jsx(DropdownMenuItem, { onClick: onCreateNew, children: localization.CREATE_ORGANIZATION })
2150
+ ] })
2151
+ ] })
2152
+ ] }) });
2153
+ }
2154
+ function UserMenu({ showOrganization = false, menuItems, className }) {
2155
+ const { user } = useSession();
2156
+ const { logout } = useAuth();
2157
+ const { localization } = useAuthLocalization();
2158
+ const handleSignOut = useCallback(async () => {
2159
+ await logout();
2160
+ }, [logout]);
2161
+ if (!user) return null;
2162
+ return /* @__PURE__ */ jsx("div", { className, children: /* @__PURE__ */ jsxs(DropdownMenu, { children: [
2163
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", children: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
2164
+ user.avatar ? /* @__PURE__ */ jsx("img", { src: user.avatar, alt: "", className: "h-8 w-8 rounded-full" }) : /* @__PURE__ */ jsx("span", { className: "flex h-8 w-8 items-center justify-center rounded-full bg-gray-200 text-xs font-medium", children: getInitials(user.name) }),
2165
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: user.name })
2166
+ ] }) }) }),
2167
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", children: [
2168
+ /* @__PURE__ */ jsx(DropdownMenuLabel, { children: /* @__PURE__ */ jsxs("div", { children: [
2169
+ /* @__PURE__ */ jsx("p", { className: "text-sm", children: user.email }),
2170
+ showOrganization && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500", children: localization.ORGANIZATION })
2171
+ ] }) }),
2172
+ /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
2173
+ menuItems?.map((item) => /* @__PURE__ */ jsx(DropdownMenuItem, { onClick: item.onClick, children: item.label }, item.label)),
2174
+ menuItems && menuItems.length > 0 && /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
2175
+ /* @__PURE__ */ jsx(DropdownMenuItem, { onClick: handleSignOut, children: localization.SIGN_OUT })
2176
+ ] })
2177
+ ] }) });
2178
+ }
111
2179
 
112
- export { AuthContext, AuthProvider, useAuth, useAuthContext, useSession };
2180
+ export { AUTH_ERROR_CODES, AccountSwitcher, AuthContext, AuthProvider, DeviceList, LoadingBoundary, LocalizationContext, LoginActivityFeed, LoginForm, MagicLinkForm, OAuthButton, OrganizationMenu, PhoneLoginForm, RISK_COLORS, SessionGuard, SignupForm, UserMenu, authLocalization, credentialLoginSchema, getInitials, getLocalizedError, getRiskLevel, magicLinkSchema, otpVerifySchema, phoneLoginSchema, phoneNumberSchema, otpVerifySchema2 as phoneOtpVerifySchema, phonePasswordSchema, signupSchema, toAuthError, useApiKeys, useAuth, useAuthContext, useAuthLocalization, useDevices, useLoginActivity, useOrganizations, usePhone, useSession, useSessions, useUser };
113
2181
  //# sourceMappingURL=index.js.map
114
2182
  //# sourceMappingURL=index.js.map