create-fluxstack 1.0.22 → 1.1.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.
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Provedor de Contexto de Autenticação
3
+ * Context Provider React para gerenciar estado de autenticação
4
+ */
5
+
6
+ import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react'
7
+ import { CryptoAuthClient, SessionInfo, AuthConfig } from '../CryptoAuthClient'
8
+
9
+ export interface AuthContextValue {
10
+ client: CryptoAuthClient
11
+ session: SessionInfo | null
12
+ isAuthenticated: boolean
13
+ isAdmin: boolean
14
+ permissions: string[]
15
+ isLoading: boolean
16
+ error: string | null
17
+ login: () => Promise<void>
18
+ logout: () => Promise<void>
19
+ refresh: () => Promise<void>
20
+ }
21
+
22
+ const AuthContext = createContext<AuthContextValue | null>(null)
23
+
24
+ export interface AuthProviderProps {
25
+ children: ReactNode
26
+ config?: AuthConfig
27
+ onAuthChange?: (isAuthenticated: boolean, session: SessionInfo | null) => void
28
+ onError?: (error: string) => void
29
+ }
30
+
31
+ export const AuthProvider: React.FC<AuthProviderProps> = ({
32
+ children,
33
+ config = {},
34
+ onAuthChange,
35
+ onError
36
+ }) => {
37
+ const [client] = useState(() => new CryptoAuthClient({ ...config, autoInit: false }))
38
+ const [session, setSession] = useState<SessionInfo | null>(null)
39
+ const [isLoading, setIsLoading] = useState(true)
40
+ const [error, setError] = useState<string | null>(null)
41
+
42
+ const isAuthenticated = session !== null && client.isAuthenticated()
43
+ const isAdmin = session?.isAdmin || false
44
+ const permissions = session?.permissions || []
45
+
46
+ useEffect(() => {
47
+ initializeAuth()
48
+ }, [])
49
+
50
+ useEffect(() => {
51
+ onAuthChange?.(isAuthenticated, session)
52
+ }, [isAuthenticated, session, onAuthChange])
53
+
54
+ const initializeAuth = async () => {
55
+ setIsLoading(true)
56
+ setError(null)
57
+
58
+ try {
59
+ const currentSession = client.getSession()
60
+ if (currentSession && client.isAuthenticated()) {
61
+ setSession(currentSession)
62
+ } else {
63
+ // Tentar inicializar automaticamente se não houver sessão
64
+ try {
65
+ const newSession = await client.initialize()
66
+ setSession(newSession)
67
+ } catch (initError) {
68
+ // Falha na inicialização automática é normal se não houver sessão salva
69
+ console.debug('Inicialização automática falhou:', initError)
70
+ setSession(null)
71
+ }
72
+ }
73
+ } catch (err) {
74
+ const errorMessage = err instanceof Error ? err.message : 'Erro desconhecido'
75
+ setError(errorMessage)
76
+ onError?.(errorMessage)
77
+ console.error('Erro ao inicializar autenticação:', err)
78
+ } finally {
79
+ setIsLoading(false)
80
+ }
81
+ }
82
+
83
+ const login = async () => {
84
+ setIsLoading(true)
85
+ setError(null)
86
+
87
+ try {
88
+ const newSession = await client.createNewSession()
89
+ setSession(newSession)
90
+ } catch (err) {
91
+ const errorMessage = err instanceof Error ? err.message : 'Erro ao fazer login'
92
+ setError(errorMessage)
93
+ onError?.(errorMessage)
94
+ throw err
95
+ } finally {
96
+ setIsLoading(false)
97
+ }
98
+ }
99
+
100
+ const logout = async () => {
101
+ setIsLoading(true)
102
+ setError(null)
103
+
104
+ try {
105
+ await client.logout()
106
+ setSession(null)
107
+ } catch (err) {
108
+ const errorMessage = err instanceof Error ? err.message : 'Erro ao fazer logout'
109
+ setError(errorMessage)
110
+ onError?.(errorMessage)
111
+ // Mesmo com erro, limpar a sessão local
112
+ setSession(null)
113
+ } finally {
114
+ setIsLoading(false)
115
+ }
116
+ }
117
+
118
+ const refresh = async () => {
119
+ setIsLoading(true)
120
+ setError(null)
121
+
122
+ try {
123
+ // Verificar se a sessão atual ainda é válida
124
+ const currentSession = client.getSession()
125
+ if (currentSession && client.isAuthenticated()) {
126
+ // Tentar fazer uma requisição de teste para validar no servidor
127
+ const response = await client.fetch('/api/auth/session/info')
128
+ if (response.ok) {
129
+ const result = await response.json()
130
+ if (result.success && result.session) {
131
+ // Atualizar informações da sessão
132
+ const updatedSession = {
133
+ ...currentSession,
134
+ ...result.session,
135
+ lastUsed: new Date()
136
+ }
137
+ setSession(updatedSession)
138
+ } else {
139
+ // Sessão inválida no servidor
140
+ setSession(null)
141
+ }
142
+ } else {
143
+ // Erro na requisição, sessão pode estar inválida
144
+ setSession(null)
145
+ }
146
+ } else {
147
+ setSession(null)
148
+ }
149
+ } catch (err) {
150
+ const errorMessage = err instanceof Error ? err.message : 'Erro ao atualizar sessão'
151
+ setError(errorMessage)
152
+ onError?.(errorMessage)
153
+ setSession(null)
154
+ } finally {
155
+ setIsLoading(false)
156
+ }
157
+ }
158
+
159
+ const contextValue: AuthContextValue = {
160
+ client,
161
+ session,
162
+ isAuthenticated,
163
+ isAdmin,
164
+ permissions,
165
+ isLoading,
166
+ error,
167
+ login,
168
+ logout,
169
+ refresh
170
+ }
171
+
172
+ return (
173
+ <AuthContext.Provider value={contextValue}>
174
+ {children}
175
+ </AuthContext.Provider>
176
+ )
177
+ }
178
+
179
+ /**
180
+ * Hook para usar o contexto de autenticação
181
+ */
182
+ export const useAuth = (): AuthContextValue => {
183
+ const context = useContext(AuthContext)
184
+ if (!context) {
185
+ throw new Error('useAuth deve ser usado dentro de um AuthProvider')
186
+ }
187
+ return context
188
+ }
189
+
190
+ export default AuthProvider
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Componente de Botão de Login
3
+ * Componente React para autenticação criptográfica
4
+ */
5
+
6
+ import React, { useState, useEffect } from 'react'
7
+ import { CryptoAuthClient } from '../CryptoAuthClient'
8
+
9
+ export interface LoginButtonProps {
10
+ onLogin?: (session: any) => void
11
+ onLogout?: () => void
12
+ onError?: (error: string) => void
13
+ className?: string
14
+ loginText?: string
15
+ logoutText?: string
16
+ loadingText?: string
17
+ showPermissions?: boolean
18
+ authClient?: CryptoAuthClient
19
+ }
20
+
21
+ export const LoginButton: React.FC<LoginButtonProps> = ({
22
+ onLogin,
23
+ onLogout,
24
+ onError,
25
+ className = '',
26
+ loginText = 'Entrar',
27
+ logoutText = 'Sair',
28
+ loadingText = 'Carregando...',
29
+ showPermissions = false,
30
+ authClient
31
+ }) => {
32
+ const [client] = useState(() => authClient || new CryptoAuthClient())
33
+ const [isAuthenticated, setIsAuthenticated] = useState(false)
34
+ const [isLoading, setIsLoading] = useState(false)
35
+ const [session, setSession] = useState<any>(null)
36
+
37
+ useEffect(() => {
38
+ checkAuthStatus()
39
+ }, [])
40
+
41
+ const checkAuthStatus = async () => {
42
+ try {
43
+ const currentSession = client.getSession()
44
+ if (currentSession && client.isAuthenticated()) {
45
+ setIsAuthenticated(true)
46
+ setSession(currentSession)
47
+ } else {
48
+ setIsAuthenticated(false)
49
+ setSession(null)
50
+ }
51
+ } catch (error) {
52
+ console.error('Erro ao verificar status de autenticação:', error)
53
+ setIsAuthenticated(false)
54
+ setSession(null)
55
+ }
56
+ }
57
+
58
+ const handleLogin = async () => {
59
+ setIsLoading(true)
60
+ try {
61
+ const newSession = await client.initialize()
62
+ setIsAuthenticated(true)
63
+ setSession(newSession)
64
+ onLogin?.(newSession)
65
+ } catch (error) {
66
+ const errorMessage = error instanceof Error ? error.message : 'Erro desconhecido'
67
+ console.error('Erro ao fazer login:', error)
68
+ onError?.(errorMessage)
69
+ } finally {
70
+ setIsLoading(false)
71
+ }
72
+ }
73
+
74
+ const handleLogout = async () => {
75
+ setIsLoading(true)
76
+ try {
77
+ await client.logout()
78
+ setIsAuthenticated(false)
79
+ setSession(null)
80
+ onLogout?.()
81
+ } catch (error) {
82
+ const errorMessage = error instanceof Error ? error.message : 'Erro desconhecido'
83
+ console.error('Erro ao fazer logout:', error)
84
+ onError?.(errorMessage)
85
+ } finally {
86
+ setIsLoading(false)
87
+ }
88
+ }
89
+
90
+ const baseClassName = `
91
+ px-4 py-2 rounded-md font-medium transition-colors duration-200
92
+ focus:outline-none focus:ring-2 focus:ring-offset-2
93
+ disabled:opacity-50 disabled:cursor-not-allowed
94
+ `.trim()
95
+
96
+ if (isLoading) {
97
+ return (
98
+ <button
99
+ disabled
100
+ className={`${baseClassName} bg-gray-400 text-white cursor-not-allowed ${className}`}
101
+ >
102
+ {loadingText}
103
+ </button>
104
+ )
105
+ }
106
+
107
+ if (isAuthenticated) {
108
+ return (
109
+ <div className="flex items-center gap-3">
110
+ <div className="flex items-center gap-2">
111
+ <div className="w-2 h-2 bg-green-500 rounded-full"></div>
112
+ <span className="text-sm text-gray-600">
113
+ Autenticado
114
+ {session?.isAdmin && (
115
+ <span className="ml-1 px-2 py-0.5 bg-blue-100 text-blue-800 text-xs rounded-full">
116
+ Admin
117
+ </span>
118
+ )}
119
+ </span>
120
+ </div>
121
+
122
+ {showPermissions && session?.permissions && (
123
+ <div className="flex gap-1">
124
+ {session.permissions.map((permission: string) => (
125
+ <span
126
+ key={permission}
127
+ className="px-2 py-0.5 bg-gray-100 text-gray-700 text-xs rounded-full"
128
+ >
129
+ {permission}
130
+ </span>
131
+ ))}
132
+ </div>
133
+ )}
134
+
135
+ <button
136
+ onClick={handleLogout}
137
+ className={`${baseClassName} bg-red-600 hover:bg-red-700 text-white focus:ring-red-500 ${className}`}
138
+ >
139
+ {logoutText}
140
+ </button>
141
+ </div>
142
+ )
143
+ }
144
+
145
+ return (
146
+ <button
147
+ onClick={handleLogin}
148
+ className={`${baseClassName} bg-blue-600 hover:bg-blue-700 text-white focus:ring-blue-500 ${className}`}
149
+ >
150
+ {loginText}
151
+ </button>
152
+ )
153
+ }
154
+
155
+ export default LoginButton
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Componente de Rota Protegida
3
+ * Protege componentes que requerem autenticação
4
+ */
5
+
6
+ import React, { ReactNode } from 'react'
7
+ import { useAuth } from './AuthProvider'
8
+
9
+ export interface ProtectedRouteProps {
10
+ children: ReactNode
11
+ requireAdmin?: boolean
12
+ requiredPermissions?: string[]
13
+ fallback?: ReactNode
14
+ loadingComponent?: ReactNode
15
+ unauthorizedComponent?: ReactNode
16
+ }
17
+
18
+ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
19
+ children,
20
+ requireAdmin = false,
21
+ requiredPermissions = [],
22
+ fallback,
23
+ loadingComponent,
24
+ unauthorizedComponent
25
+ }) => {
26
+ const { isAuthenticated, isAdmin, permissions, isLoading, error } = useAuth()
27
+
28
+ // Componente de loading padrão
29
+ const defaultLoadingComponent = (
30
+ <div className="flex items-center justify-center p-8">
31
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
32
+ <span className="ml-3 text-gray-600">Verificando autenticação...</span>
33
+ </div>
34
+ )
35
+
36
+ // Componente de não autorizado padrão
37
+ const defaultUnauthorizedComponent = (
38
+ <div className="flex flex-col items-center justify-center p-8 bg-red-50 border border-red-200 rounded-lg">
39
+ <div className="text-red-600 mb-4">
40
+ <svg className="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
41
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
42
+ </svg>
43
+ </div>
44
+ <h3 className="text-lg font-semibold text-red-800 mb-2">Acesso Negado</h3>
45
+ <p className="text-red-600 text-center">
46
+ {!isAuthenticated
47
+ ? 'Você precisa estar autenticado para acessar esta página.'
48
+ : requireAdmin && !isAdmin
49
+ ? 'Você precisa de privilégios de administrador para acessar esta página.'
50
+ : 'Você não tem as permissões necessárias para acessar esta página.'
51
+ }
52
+ </p>
53
+ {error && (
54
+ <p className="text-red-500 text-sm mt-2">
55
+ Erro: {error}
56
+ </p>
57
+ )}
58
+ </div>
59
+ )
60
+
61
+ // Mostrar loading enquanto verifica autenticação
62
+ if (isLoading) {
63
+ return <>{loadingComponent || defaultLoadingComponent}</>
64
+ }
65
+
66
+ // Verificar se está autenticado
67
+ if (!isAuthenticated) {
68
+ return <>{unauthorizedComponent || fallback || defaultUnauthorizedComponent}</>
69
+ }
70
+
71
+ // Verificar se requer admin
72
+ if (requireAdmin && !isAdmin) {
73
+ return <>{unauthorizedComponent || fallback || defaultUnauthorizedComponent}</>
74
+ }
75
+
76
+ // Verificar permissões específicas
77
+ if (requiredPermissions.length > 0) {
78
+ const hasRequiredPermissions = requiredPermissions.every(permission =>
79
+ permissions.includes(permission) || permissions.includes('admin')
80
+ )
81
+
82
+ if (!hasRequiredPermissions) {
83
+ return <>{unauthorizedComponent || fallback || defaultUnauthorizedComponent}</>
84
+ }
85
+ }
86
+
87
+ // Usuário autorizado, renderizar children
88
+ return <>{children}</>
89
+ }
90
+
91
+ /**
92
+ * HOC para proteger componentes
93
+ */
94
+ export function withAuth<P extends object>(
95
+ Component: React.ComponentType<P>,
96
+ options: Omit<ProtectedRouteProps, 'children'> = {}
97
+ ) {
98
+ const WrappedComponent = (props: P) => (
99
+ <ProtectedRoute {...options}>
100
+ <Component {...props} />
101
+ </ProtectedRoute>
102
+ )
103
+
104
+ WrappedComponent.displayName = `withAuth(${Component.displayName || Component.name})`
105
+
106
+ return WrappedComponent
107
+ }
108
+
109
+ export default ProtectedRoute
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Componente de Informações da Sessão
3
+ * Exibe informações detalhadas sobre a sessão atual
4
+ */
5
+
6
+ import React, { useState } from 'react'
7
+ import { useAuth } from './AuthProvider'
8
+
9
+ export interface SessionInfoProps {
10
+ className?: string
11
+ showPrivateKey?: boolean
12
+ showFullSessionId?: boolean
13
+ compact?: boolean
14
+ }
15
+
16
+ export const SessionInfo: React.FC<SessionInfoProps> = ({
17
+ className = '',
18
+ showPrivateKey = false,
19
+ showFullSessionId = false,
20
+ compact = false
21
+ }) => {
22
+ const { session, isAuthenticated, isAdmin, permissions, isLoading } = useAuth()
23
+ const [showDetails, setShowDetails] = useState(!compact)
24
+
25
+ if (isLoading) {
26
+ return (
27
+ <div className={`animate-pulse ${className}`}>
28
+ <div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
29
+ <div className="h-4 bg-gray-200 rounded w-1/2"></div>
30
+ </div>
31
+ )
32
+ }
33
+
34
+ if (!isAuthenticated || !session) {
35
+ return (
36
+ <div className={`text-gray-500 ${className}`}>
37
+ <p>Não autenticado</p>
38
+ </div>
39
+ )
40
+ }
41
+
42
+ const formatDate = (date: Date) => {
43
+ return new Intl.DateTimeFormat('pt-BR', {
44
+ dateStyle: 'short',
45
+ timeStyle: 'medium'
46
+ }).format(date)
47
+ }
48
+
49
+ const truncateId = (id: string, length: number = 8) => {
50
+ return showFullSessionId ? id : `${id.substring(0, length)}...`
51
+ }
52
+
53
+ const copyToClipboard = async (text: string) => {
54
+ try {
55
+ await navigator.clipboard.writeText(text)
56
+ // Você pode adicionar um toast/notification aqui
57
+ } catch (err) {
58
+ console.error('Erro ao copiar para clipboard:', err)
59
+ }
60
+ }
61
+
62
+ if (compact) {
63
+ return (
64
+ <div className={`flex items-center gap-2 ${className}`}>
65
+ <div className="flex items-center gap-2">
66
+ <div className="w-2 h-2 bg-green-500 rounded-full"></div>
67
+ <span className="text-sm font-medium">
68
+ {truncateId(session.sessionId)}
69
+ </span>
70
+ {isAdmin && (
71
+ <span className="px-2 py-0.5 bg-blue-100 text-blue-800 text-xs rounded-full">
72
+ Admin
73
+ </span>
74
+ )}
75
+ </div>
76
+ <button
77
+ onClick={() => setShowDetails(!showDetails)}
78
+ className="text-gray-400 hover:text-gray-600 transition-colors"
79
+ >
80
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
81
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
82
+ </svg>
83
+ </button>
84
+
85
+ {showDetails && (
86
+ <div className="absolute z-10 mt-2 p-4 bg-white border border-gray-200 rounded-lg shadow-lg min-w-80">
87
+ <SessionDetails
88
+ session={session}
89
+ isAdmin={isAdmin}
90
+ permissions={permissions}
91
+ showPrivateKey={showPrivateKey}
92
+ showFullSessionId={showFullSessionId}
93
+ onCopy={copyToClipboard}
94
+ />
95
+ </div>
96
+ )}
97
+ </div>
98
+ )
99
+ }
100
+
101
+ return (
102
+ <div className={`bg-white border border-gray-200 rounded-lg p-4 ${className}`}>
103
+ <SessionDetails
104
+ session={session}
105
+ isAdmin={isAdmin}
106
+ permissions={permissions}
107
+ showPrivateKey={showPrivateKey}
108
+ showFullSessionId={showFullSessionId}
109
+ onCopy={copyToClipboard}
110
+ />
111
+ </div>
112
+ )
113
+ }
114
+
115
+ interface SessionDetailsProps {
116
+ session: any
117
+ isAdmin: boolean
118
+ permissions: string[]
119
+ showPrivateKey: boolean
120
+ showFullSessionId: boolean
121
+ onCopy: (text: string) => void
122
+ }
123
+
124
+ const SessionDetails: React.FC<SessionDetailsProps> = ({
125
+ session,
126
+ isAdmin,
127
+ permissions,
128
+ showPrivateKey,
129
+ showFullSessionId,
130
+ onCopy
131
+ }) => {
132
+ const formatDate = (date: Date) => {
133
+ return new Intl.DateTimeFormat('pt-BR', {
134
+ dateStyle: 'short',
135
+ timeStyle: 'medium'
136
+ }).format(date)
137
+ }
138
+
139
+ const CopyButton: React.FC<{ text: string }> = ({ text }) => (
140
+ <button
141
+ onClick={() => onCopy(text)}
142
+ className="ml-2 text-gray-400 hover:text-gray-600 transition-colors"
143
+ title="Copiar"
144
+ >
145
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
146
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
147
+ </svg>
148
+ </button>
149
+ )
150
+
151
+ return (
152
+ <div className="space-y-3">
153
+ <div className="flex items-center justify-between">
154
+ <h3 className="text-lg font-semibold text-gray-900">Informações da Sessão</h3>
155
+ <div className="flex items-center gap-2">
156
+ <div className="w-2 h-2 bg-green-500 rounded-full"></div>
157
+ <span className="text-sm text-green-600 font-medium">Ativo</span>
158
+ </div>
159
+ </div>
160
+
161
+ <div className="grid grid-cols-1 gap-3 text-sm">
162
+ <div>
163
+ <label className="block text-gray-600 font-medium mb-1">Session ID</label>
164
+ <div className="flex items-center">
165
+ <code className="bg-gray-100 px-2 py-1 rounded text-xs font-mono break-all">
166
+ {showFullSessionId ? session.sessionId : `${session.sessionId.substring(0, 16)}...`}
167
+ </code>
168
+ <CopyButton text={session.sessionId} />
169
+ </div>
170
+ </div>
171
+
172
+ <div>
173
+ <label className="block text-gray-600 font-medium mb-1">Chave Pública</label>
174
+ <div className="flex items-center">
175
+ <code className="bg-gray-100 px-2 py-1 rounded text-xs font-mono break-all">
176
+ {showFullSessionId ? session.publicKey : `${session.publicKey.substring(0, 16)}...`}
177
+ </code>
178
+ <CopyButton text={session.publicKey} />
179
+ </div>
180
+ </div>
181
+
182
+ {showPrivateKey && (
183
+ <div>
184
+ <label className="block text-red-600 font-medium mb-1">
185
+ Chave Privada
186
+ <span className="text-xs text-red-500 ml-1">(Confidencial)</span>
187
+ </label>
188
+ <div className="flex items-center">
189
+ <code className="bg-red-50 border border-red-200 px-2 py-1 rounded text-xs font-mono break-all">
190
+ {session.privateKey.substring(0, 16)}...
191
+ </code>
192
+ <CopyButton text={session.privateKey} />
193
+ </div>
194
+ </div>
195
+ )}
196
+
197
+ <div className="flex gap-4">
198
+ <div>
199
+ <label className="block text-gray-600 font-medium mb-1">Status</label>
200
+ <div className="flex items-center gap-2">
201
+ <span className={`px-2 py-1 rounded-full text-xs font-medium ${
202
+ isAdmin
203
+ ? 'bg-blue-100 text-blue-800'
204
+ : 'bg-green-100 text-green-800'
205
+ }`}>
206
+ {isAdmin ? 'Administrador' : 'Usuário'}
207
+ </span>
208
+ </div>
209
+ </div>
210
+
211
+ <div>
212
+ <label className="block text-gray-600 font-medium mb-1">Permissões</label>
213
+ <div className="flex flex-wrap gap-1">
214
+ {permissions.map((permission) => (
215
+ <span
216
+ key={permission}
217
+ className="px-2 py-0.5 bg-gray-100 text-gray-700 text-xs rounded-full"
218
+ >
219
+ {permission}
220
+ </span>
221
+ ))}
222
+ </div>
223
+ </div>
224
+ </div>
225
+
226
+ <div className="flex gap-4">
227
+ <div>
228
+ <label className="block text-gray-600 font-medium mb-1">Criado em</label>
229
+ <span className="text-gray-800">{formatDate(session.createdAt)}</span>
230
+ </div>
231
+
232
+ <div>
233
+ <label className="block text-gray-600 font-medium mb-1">Último uso</label>
234
+ <span className="text-gray-800">{formatDate(session.lastUsed)}</span>
235
+ </div>
236
+ </div>
237
+ </div>
238
+ </div>
239
+ )
240
+ }
241
+
242
+ export default SessionInfo