@riligar/auth-react 1.15.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -25,14 +25,17 @@ const getEnvVar = key => {
25
25
  };
26
26
  let API_BASE = (typeof window !== 'undefined' && window?.location ? getEnvVar('VITE_API_BASE') : null) || 'http://localhost:3000';
27
27
  let API_KEY = null;
28
+ let INTERNAL_MODE = false; // Modo interno: não exige API Key (para aplicações same-domain)
28
29
 
29
- // Permite configurar a API base e key externamente (chamado pelo AuthProvider)
30
+ // Permite configurar a API base, key e modo interno externamente (chamado pelo AuthProvider)
30
31
  function configure({
31
32
  apiUrl,
32
- apiKey
33
+ apiKey,
34
+ internal = false
33
35
  }) {
34
36
  if (apiUrl) API_BASE = apiUrl.replace(/\/$/, ''); // Remove trailing slash
35
37
  if (apiKey) API_KEY = apiKey;
38
+ INTERNAL_MODE = internal;
36
39
  }
37
40
 
38
41
  // helper fetch pré-configurado
@@ -54,8 +57,8 @@ async function api(route, opts = {}) {
54
57
  headers.Authorization = `Bearer ${token}`;
55
58
  }
56
59
 
57
- // Adiciona API Key se configurada
58
- if (API_KEY) {
60
+ // Adiciona API Key se configurada e não estiver em modo interno
61
+ if (API_KEY && !INTERNAL_MODE) {
59
62
  headers['X-API-Key'] = API_KEY;
60
63
  }
61
64
  const res = await fetch(url, {
@@ -276,11 +279,11 @@ const getSession = async () => {
276
279
  const listSessions = async () => {
277
280
  return await api('/auth/list-sessions');
278
281
  };
279
- const revokeSession = async token => {
280
- return await api('/auth/revoke-session', {
282
+ const revokeSession = async id => {
283
+ return await api('/auth/revoke-session-by-id', {
281
284
  method: 'POST',
282
285
  body: JSON.stringify({
283
- token
286
+ id
284
287
  })
285
288
  });
286
289
  };
@@ -342,7 +345,7 @@ const useAuthStore = zustand.create((set, get) => ({
342
345
  error: null,
343
346
  // Session management
344
347
  sessions: [],
345
- currentSessionToken: null,
348
+ currentSession: null,
346
349
  // Loading states granulares
347
350
  loadingStates: {
348
351
  signIn: false,
@@ -400,9 +403,14 @@ const useAuthStore = zustand.create((set, get) => ({
400
403
  // Precisamos buscar a sessão no servidor.
401
404
  if (!user) {
402
405
  try {
403
- const session = await getSession();
404
- if (session?.user) {
405
- user = session.user;
406
+ const sessionData = await getSession();
407
+ if (sessionData?.user) {
408
+ user = sessionData.user;
409
+ }
410
+ if (sessionData?.session) {
411
+ set({
412
+ currentSession: sessionData.session
413
+ });
406
414
  }
407
415
  } catch (sessionError) {
408
416
  console.warn('[AuthStore] Failed to fetch session:', sessionError);
@@ -475,8 +483,13 @@ const useAuthStore = zustand.create((set, get) => ({
475
483
 
476
484
  // Se não encontrou no token (sessão opaca), busca do servidor
477
485
  if (!user) {
478
- const session = await getSession();
479
- if (session?.user) user = session.user;
486
+ const sessionData = await getSession();
487
+ if (sessionData?.user) user = sessionData.user;
488
+ if (sessionData?.session) {
489
+ set({
490
+ currentSession: sessionData.session
491
+ });
492
+ }
480
493
  }
481
494
  set({
482
495
  user,
@@ -658,14 +671,14 @@ const useAuthStore = zustand.create((set, get) => ({
658
671
  /* Session */
659
672
  getSession: async () => {
660
673
  try {
661
- const session = await getSession();
662
- // Store current session token for comparison
663
- if (session?.session?.token) {
674
+ const sessionData = await getSession();
675
+ // Store current session for comparison (includes id)
676
+ if (sessionData?.session) {
664
677
  set({
665
- currentSessionToken: session.session.token
678
+ currentSession: sessionData.session
666
679
  });
667
680
  }
668
- return session;
681
+ return sessionData;
669
682
  } catch (err) {
670
683
  set({
671
684
  error: err
@@ -877,11 +890,14 @@ function AuthProvider({
877
890
  apiUrl,
878
891
  // URL base da API (OBRIGATÓRIA)
879
892
  apiKey,
880
- // API Key para header X-API-Key (OBRIGATÓRIA)
893
+ // API Key para header X-API-Key (obrigatória exceto em modo internal)
894
+ internal = false,
895
+ // Modo interno: não exige API Key (para aplicações same-domain como dashboard)
881
896
  onError // Callback de erro global
882
897
  }) {
883
898
  // Validação de props obrigatórias
884
- if (!apiKey) {
899
+ // apiKey só é obrigatória se não estiver em modo internal
900
+ if (!internal && !apiKey) {
885
901
  throw new Error('[@riligar/auth-react] apiKey é obrigatória no AuthProvider. ' + 'Obtenha sua API Key no dashboard em https://dashboard.myauth.click');
886
902
  }
887
903
  if (!apiUrl) {
@@ -891,15 +907,15 @@ function AuthProvider({
891
907
  const startRefresh = useAuthStore(s => s.startRefresh);
892
908
  const checkTokenValidity = useAuthStore(s => s.checkTokenValidity);
893
909
 
894
- // Configura SDK com apiUrl e apiKey
895
- // Configura SDK com apiUrl e apiKey
910
+ // Configura SDK com apiUrl, apiKey e modo interno
896
911
  // Usamos useMemo para garantir que a configuração ocorra ANTES dos efeitos dos componentes filhos
897
912
  react.useMemo(() => {
898
913
  configure({
899
914
  apiUrl,
900
- apiKey
915
+ apiKey,
916
+ internal
901
917
  });
902
- }, [apiUrl, apiKey]);
918
+ }, [apiUrl, apiKey, internal]);
903
919
  react.useEffect(() => {
904
920
  init();
905
921
  startRefresh();
@@ -917,8 +933,24 @@ function AuthProvider({
917
933
  checkTokenValidity();
918
934
  }
919
935
  };
936
+
937
+ // Escuta evento de sessão revogada (quando o usuário revoga sua própria sessão)
938
+ const handleSessionRevoked = () => {
939
+ // Limpa o usuário do store
940
+ useAuthStore.setState({
941
+ user: null,
942
+ currentSession: null,
943
+ sessions: []
944
+ });
945
+ // Dispara evento de logout para sincronizar entre abas
946
+ localStorage.setItem('auth:logout', Date.now());
947
+ };
920
948
  window.addEventListener('storage', handleStorageChange);
921
- return () => window.removeEventListener('storage', handleStorageChange);
949
+ window.addEventListener('auth:session-revoked', handleSessionRevoked);
950
+ return () => {
951
+ window.removeEventListener('storage', handleStorageChange);
952
+ window.removeEventListener('auth:session-revoked', handleSessionRevoked);
953
+ };
922
954
  }, [checkTokenValidity]);
923
955
 
924
956
  // Verifica validade do token periodicamente
@@ -1006,6 +1038,19 @@ const useUser = () => useAuthStore(shallow.useShallow(s => ({
1006
1038
  // Alias deprecado para backwards compatibility
1007
1039
  const useProfile = useUser;
1008
1040
 
1041
+ // Sessions Management Hook
1042
+ const useSessions = () => useAuthStore(shallow.useShallow(s => ({
1043
+ currentSession: s.currentSession,
1044
+ sessions: s.sessions,
1045
+ getSession: s.getSession,
1046
+ listSessions: s.listSessions,
1047
+ revokeSession: s.revokeSession,
1048
+ revokeOtherSessions: s.revokeOtherSessions,
1049
+ loadingListSessions: s.loadingStates.listSessions,
1050
+ loadingRevokeSession: s.loadingStates.revokeSession,
1051
+ error: s.error
1052
+ })));
1053
+
1009
1054
  // Application Logo Hook
1010
1055
  const useApplicationLogo = () => {
1011
1056
  const applicationInfo = useAuthStore(s => s.applicationInfo);
@@ -2035,6 +2080,7 @@ function UserProfile({
2035
2080
  showName = true,
2036
2081
  showEmail = true,
2037
2082
  showPassword = true,
2083
+ showSessions = true,
2038
2084
  // Customização
2039
2085
  labels = {},
2040
2086
  title = 'Account',
@@ -2062,6 +2108,111 @@ function UserProfile({
2062
2108
  loadingChangeEmail
2063
2109
  } = useProfile();
2064
2110
 
2111
+ // Hook para sessions
2112
+ const {
2113
+ currentSession,
2114
+ sessions,
2115
+ listSessions,
2116
+ getSession,
2117
+ revokeSession,
2118
+ revokeOtherSessions,
2119
+ loadingListSessions,
2120
+ loadingRevokeSession
2121
+ } = useSessions();
2122
+
2123
+ // Load sessions when opened (modal) or component mounts (card), and when sessions section is opened
2124
+ react.useEffect(() => {
2125
+ if (showSessions && (variant === 'card' || opened)) {
2126
+ // Fetch current session first to get the session ID, then list all sessions
2127
+ getSession().catch(err => console.warn('Failed to get current session:', err));
2128
+ listSessions().catch(err => console.warn('Failed to load sessions:', err));
2129
+ }
2130
+ }, [opened, showSessions, variant]);
2131
+
2132
+ // Helper to parse user agent string
2133
+ const parseUserAgent = ua => {
2134
+ if (!ua) return {
2135
+ browser: 'Unknown Browser',
2136
+ os: 'Unknown OS'
2137
+ };
2138
+ let browser = 'Unknown Browser';
2139
+ let os = 'Unknown OS';
2140
+
2141
+ // Detect browser
2142
+ if (ua.includes('Chrome') && !ua.includes('Edg')) browser = 'Chrome';else if (ua.includes('Firefox')) browser = 'Firefox';else if (ua.includes('Safari') && !ua.includes('Chrome')) browser = 'Safari';else if (ua.includes('Edg')) browser = 'Edge';else if (ua.includes('Opera') || ua.includes('OPR')) browser = 'Opera';
2143
+
2144
+ // Detect OS
2145
+ if (ua.includes('Windows')) os = 'Windows';else if (ua.includes('Mac OS')) os = 'macOS';else if (ua.includes('Linux')) os = 'Linux';else if (ua.includes('Android')) os = 'Android';else if (ua.includes('iPhone') || ua.includes('iPad')) os = 'iOS';
2146
+ return {
2147
+ browser,
2148
+ os
2149
+ };
2150
+ };
2151
+
2152
+ // Session handlers
2153
+ const handleRevokeSession = async sessionId => {
2154
+ // Check if revoking current session
2155
+ const isCurrentSession = sessionId === currentSession?.id || sessions.length === 1;
2156
+
2157
+ // If revoking current session, handle logout immediately after revocation
2158
+ if (isCurrentSession) {
2159
+ try {
2160
+ await revokeSession(sessionId);
2161
+ } catch (error) {
2162
+ // Even if it fails, we're revoking our own session, so just logout
2163
+ // The server already revoked our session
2164
+ }
2165
+ // Clear auth state and redirect
2166
+ localStorage.removeItem('auth:token');
2167
+ window.dispatchEvent(new CustomEvent('auth:session-revoked'));
2168
+ if (variant === 'modal') onClose?.();
2169
+ return;
2170
+ }
2171
+
2172
+ // Revoking another session
2173
+ try {
2174
+ await revokeSession(sessionId);
2175
+ notifications.notifications.show({
2176
+ title: labels.successTitle || 'Sucesso',
2177
+ message: labels.sessionRevoked || 'Sessão encerrada com sucesso',
2178
+ color: 'green'
2179
+ });
2180
+ } catch (error) {
2181
+ // Check for 401 error (our SDK uses error.res, axios uses error.response)
2182
+ const status = error.res?.status || error.response?.status;
2183
+ if (status === 401) {
2184
+ // This means our session was revoked, not the target one - do logout
2185
+ localStorage.removeItem('auth:token');
2186
+ window.dispatchEvent(new CustomEvent('auth:session-revoked'));
2187
+ if (variant === 'modal') onClose?.();
2188
+ return;
2189
+ }
2190
+ notifications.notifications.show({
2191
+ title: labels.errorTitle || 'Erro',
2192
+ message: error.message || labels.sessionRevokeFailed || 'Falha ao encerrar sessão',
2193
+ color: 'red'
2194
+ });
2195
+ onError?.(error);
2196
+ }
2197
+ };
2198
+ const handleRevokeOtherSessions = async () => {
2199
+ try {
2200
+ await revokeOtherSessions();
2201
+ notifications.notifications.show({
2202
+ title: labels.successTitle || 'Sucesso',
2203
+ message: labels.otherSessionsRevoked || 'Todas as outras sessões foram encerradas',
2204
+ color: 'green'
2205
+ });
2206
+ } catch (error) {
2207
+ notifications.notifications.show({
2208
+ title: labels.errorTitle || 'Erro',
2209
+ message: error.message || labels.otherSessionsRevokeFailed || 'Falha ao encerrar sessões',
2210
+ color: 'red'
2211
+ });
2212
+ onError?.(error);
2213
+ }
2214
+ };
2215
+
2065
2216
  // Password form
2066
2217
  const passwordForm = form.useForm({
2067
2218
  initialValues: {
@@ -2593,7 +2744,7 @@ function UserProfile({
2593
2744
  })]
2594
2745
  })]
2595
2746
  })]
2596
- }), showPassword && /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
2747
+ }), (showPassword || showSessions) && /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
2597
2748
  mb: "md",
2598
2749
  children: [/*#__PURE__*/jsxRuntime.jsx(SectionHeader, {
2599
2750
  icon: iconsReact.IconShield,
@@ -2601,69 +2752,196 @@ function UserProfile({
2601
2752
  description: labels.securityDescription || 'Protect your account'
2602
2753
  }), /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
2603
2754
  gap: "sm",
2604
- children: [/*#__PURE__*/jsxRuntime.jsx(SettingRow, {
2605
- label: labels.password || 'Password',
2606
- action: labels.change || 'Change',
2607
- actionLabel: labels.changePassword || 'Change your password',
2608
- onClick: () => handleToggleSection('password'),
2609
- expanded: editingSection === 'password',
2610
- children: /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
2611
- gap: "xs",
2612
- children: [/*#__PURE__*/jsxRuntime.jsx(iconsReact.IconKey, {
2613
- size: 16,
2614
- color: "var(--mantine-color-dimmed)"
2615
- }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
2616
- size: "sm",
2617
- c: "dimmed",
2618
- children: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"
2619
- })]
2620
- })
2621
- }), /*#__PURE__*/jsxRuntime.jsx(core.Collapse, {
2622
- in: editingSection === 'password',
2623
- children: /*#__PURE__*/jsxRuntime.jsx(core.Card, {
2624
- p: "md",
2625
- withBorder: true,
2626
- children: /*#__PURE__*/jsxRuntime.jsx("form", {
2627
- onSubmit: passwordForm.onSubmit(handleChangePassword),
2628
- children: /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
2629
- gap: "sm",
2630
- children: [/*#__PURE__*/jsxRuntime.jsx(core.PasswordInput, {
2631
- label: labels.currentPassword || 'Senha Atual',
2632
- placeholder: labels.currentPasswordPlaceholder || 'Digite sua senha atual',
2633
- ...passwordForm.getInputProps('currentPassword')
2634
- }), /*#__PURE__*/jsxRuntime.jsx(core.PasswordInput, {
2635
- label: labels.newPassword || 'Nova Senha',
2636
- placeholder: labels.newPasswordPlaceholder || 'Digite a nova senha',
2637
- description: labels.passwordDescription || 'Mínimo 8 caracteres',
2638
- ...passwordForm.getInputProps('newPassword')
2639
- }), /*#__PURE__*/jsxRuntime.jsx(core.PasswordInput, {
2640
- label: labels.confirmPassword || 'Confirmar Nova Senha',
2641
- placeholder: labels.confirmPasswordPlaceholder || 'Confirme a nova senha',
2642
- ...passwordForm.getInputProps('confirmPassword')
2643
- }), /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
2644
- justify: "flex-end",
2645
- gap: "xs",
2646
- children: [/*#__PURE__*/jsxRuntime.jsx(core.Button, {
2647
- variant: "default",
2648
- size: "xs",
2649
- onClick: () => handleToggleSection('password'),
2650
- leftSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconX, {
2651
- size: 14
2652
- }),
2653
- children: labels.cancel || 'Cancelar'
2654
- }), /*#__PURE__*/jsxRuntime.jsx(core.Button, {
2655
- type: "submit",
2656
- size: "xs",
2657
- loading: loadingChangePassword,
2658
- leftSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconCheck, {
2659
- size: 14
2660
- }),
2661
- children: labels.save || 'Salvar'
2755
+ children: [showPassword && /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
2756
+ children: [/*#__PURE__*/jsxRuntime.jsx(SettingRow, {
2757
+ label: labels.password || 'Password',
2758
+ action: labels.change || 'Change',
2759
+ actionLabel: labels.changePassword || 'Change your password',
2760
+ onClick: () => handleToggleSection('password'),
2761
+ expanded: editingSection === 'password',
2762
+ children: /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
2763
+ gap: "xs",
2764
+ children: [/*#__PURE__*/jsxRuntime.jsx(iconsReact.IconKey, {
2765
+ size: 16,
2766
+ color: "var(--mantine-color-dimmed)"
2767
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
2768
+ size: "sm",
2769
+ c: "dimmed",
2770
+ children: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"
2771
+ })]
2772
+ })
2773
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Collapse, {
2774
+ in: editingSection === 'password',
2775
+ children: /*#__PURE__*/jsxRuntime.jsx(core.Card, {
2776
+ p: "md",
2777
+ withBorder: true,
2778
+ children: /*#__PURE__*/jsxRuntime.jsx("form", {
2779
+ onSubmit: passwordForm.onSubmit(handleChangePassword),
2780
+ children: /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
2781
+ gap: "sm",
2782
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.PasswordInput, {
2783
+ label: labels.currentPassword || 'Senha Atual',
2784
+ placeholder: labels.currentPasswordPlaceholder || 'Digite sua senha atual',
2785
+ ...passwordForm.getInputProps('currentPassword')
2786
+ }), /*#__PURE__*/jsxRuntime.jsx(core.PasswordInput, {
2787
+ label: labels.newPassword || 'Nova Senha',
2788
+ placeholder: labels.newPasswordPlaceholder || 'Digite a nova senha',
2789
+ description: labels.passwordDescription || 'Mínimo 8 caracteres',
2790
+ ...passwordForm.getInputProps('newPassword')
2791
+ }), /*#__PURE__*/jsxRuntime.jsx(core.PasswordInput, {
2792
+ label: labels.confirmPassword || 'Confirmar Nova Senha',
2793
+ placeholder: labels.confirmPasswordPlaceholder || 'Confirme a nova senha',
2794
+ ...passwordForm.getInputProps('confirmPassword')
2795
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
2796
+ justify: "flex-end",
2797
+ gap: "xs",
2798
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Button, {
2799
+ variant: "default",
2800
+ size: "xs",
2801
+ onClick: () => handleToggleSection('password'),
2802
+ leftSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconX, {
2803
+ size: 14
2804
+ }),
2805
+ children: labels.cancel || 'Cancelar'
2806
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Button, {
2807
+ type: "submit",
2808
+ size: "xs",
2809
+ loading: loadingChangePassword,
2810
+ leftSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconCheck, {
2811
+ size: 14
2812
+ }),
2813
+ children: labels.save || 'Salvar'
2814
+ })]
2662
2815
  })]
2663
- })]
2816
+ })
2664
2817
  })
2665
2818
  })
2666
- })
2819
+ })]
2820
+ }), showSessions && /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
2821
+ children: [/*#__PURE__*/jsxRuntime.jsx(SettingRow, {
2822
+ label: labels.sessions || 'Sessions',
2823
+ action: editingSection === 'sessions' ? labels.close || 'Close' : labels.manage || 'Manage',
2824
+ actionLabel: labels.manageSessions || 'Manage your active sessions',
2825
+ onClick: () => handleToggleSection('sessions'),
2826
+ expanded: editingSection === 'sessions',
2827
+ children: /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
2828
+ gap: "xs",
2829
+ children: [/*#__PURE__*/jsxRuntime.jsx(iconsReact.IconDevices, {
2830
+ size: 16,
2831
+ color: "var(--mantine-color-dimmed)"
2832
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
2833
+ size: "sm",
2834
+ c: "dimmed",
2835
+ children: sessions.length > 0 ? `${sessions.length} active session${sessions.length > 1 ? 's' : ''}` : loadingListSessions ? 'Loading...' : 'No sessions'
2836
+ })]
2837
+ })
2838
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Collapse, {
2839
+ in: editingSection === 'sessions',
2840
+ children: /*#__PURE__*/jsxRuntime.jsx(core.Card, {
2841
+ p: "md",
2842
+ withBorder: true,
2843
+ children: /*#__PURE__*/jsxRuntime.jsx(core.Stack, {
2844
+ gap: "md",
2845
+ children: loadingListSessions ? /*#__PURE__*/jsxRuntime.jsx(core.Text, {
2846
+ size: "sm",
2847
+ c: "dimmed",
2848
+ ta: "center",
2849
+ children: labels.loadingSessions || 'Carregando sessões...'
2850
+ }) : sessions.length === 0 ? /*#__PURE__*/jsxRuntime.jsx(core.Text, {
2851
+ size: "sm",
2852
+ c: "dimmed",
2853
+ ta: "center",
2854
+ children: labels.noSessionsFound || 'Nenhuma sessão encontrada'
2855
+ }) : /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
2856
+ children: [sessions.map(sessionItem => {
2857
+ const isCurrentSession = sessionItem.id === currentSession?.id;
2858
+ const deviceInfo = parseUserAgent(sessionItem.userAgent);
2859
+ const createdDate = new Date(sessionItem.createdAt);
2860
+ return /*#__PURE__*/jsxRuntime.jsx(core.Card, {
2861
+ p: "sm",
2862
+ style: {
2863
+ borderColor: isCurrentSession ? 'var(--mantine-color-blue-5)' : undefined,
2864
+ backgroundColor: isCurrentSession ? 'var(--mantine-color-blue-light)' : undefined
2865
+ },
2866
+ children: /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
2867
+ justify: "space-between",
2868
+ wrap: "nowrap",
2869
+ children: [/*#__PURE__*/jsxRuntime.jsxs(core.Group, {
2870
+ gap: "sm",
2871
+ wrap: "nowrap",
2872
+ style: {
2873
+ flex: 1
2874
+ },
2875
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.ThemeIcon, {
2876
+ size: 36,
2877
+ variant: "light",
2878
+ color: isCurrentSession ? 'blue' : 'gray',
2879
+ children: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconDeviceMobile, {
2880
+ size: 18
2881
+ })
2882
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
2883
+ style: {
2884
+ flex: 1
2885
+ },
2886
+ children: [/*#__PURE__*/jsxRuntime.jsxs(core.Group, {
2887
+ gap: "xs",
2888
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Text, {
2889
+ size: "sm",
2890
+ fw: 500,
2891
+ children: deviceInfo.browser
2892
+ }), isCurrentSession && /*#__PURE__*/jsxRuntime.jsx(core.Badge, {
2893
+ size: "xs",
2894
+ variant: "filled",
2895
+ color: "blue",
2896
+ children: labels.thisDevice || 'Este dispositivo'
2897
+ })]
2898
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Text, {
2899
+ size: "xs",
2900
+ c: "dimmed",
2901
+ children: [deviceInfo.os, " \u2022 ", sessionItem.ipAddress || labels.unknownIP || 'IP desconhecido']
2902
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Text, {
2903
+ size: "xs",
2904
+ c: "dimmed",
2905
+ children: [labels.createdAt || 'Criada em', " ", createdDate.toLocaleDateString('pt-BR'), " ", labels.at || 'às', ' ', createdDate.toLocaleTimeString('pt-BR', {
2906
+ hour: '2-digit',
2907
+ minute: '2-digit'
2908
+ })]
2909
+ })]
2910
+ })]
2911
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Tooltip, {
2912
+ label: isCurrentSession ? labels.signOutAndEnd || 'Encerrar e sair' : labels.endSession || 'Encerrar sessão',
2913
+ children: /*#__PURE__*/jsxRuntime.jsx(core.Button, {
2914
+ variant: "light",
2915
+ color: "red",
2916
+ size: "xs",
2917
+ onClick: () => handleRevokeSession(sessionItem.id),
2918
+ loading: loadingRevokeSession,
2919
+ leftSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconLogout, {
2920
+ size: 14
2921
+ }),
2922
+ children: labels.end || 'Encerrar'
2923
+ })
2924
+ })]
2925
+ })
2926
+ }, sessionItem.id);
2927
+ }), sessions.length > 1 && /*#__PURE__*/jsxRuntime.jsx(core.Group, {
2928
+ justify: "flex-end",
2929
+ children: /*#__PURE__*/jsxRuntime.jsx(core.Button, {
2930
+ variant: "light",
2931
+ color: "red",
2932
+ size: "xs",
2933
+ onClick: handleRevokeOtherSessions,
2934
+ loading: loadingRevokeSession,
2935
+ leftSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconLogout, {
2936
+ size: 14
2937
+ }),
2938
+ children: labels.endOtherSessions || 'Encerrar todas as outras sessões'
2939
+ })
2940
+ })]
2941
+ })
2942
+ })
2943
+ })
2944
+ })]
2667
2945
  })]
2668
2946
  })]
2669
2947
  })]
@@ -2905,6 +3183,7 @@ exports.useMagicLink = useMagicLink;
2905
3183
  exports.usePasswordReset = usePasswordReset;
2906
3184
  exports.useProfile = useUser;
2907
3185
  exports.useSession = useSession;
3186
+ exports.useSessions = useSessions;
2908
3187
  exports.useSignIn = useSignIn;
2909
3188
  exports.useSignOut = useSignOut;
2910
3189
  exports.useSignUp = useSignUp;