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.
- package/app/server/live/register-components.ts +6 -26
- package/core/build/bundler.ts +53 -5
- package/core/build/flux-plugins-generator.ts +315 -0
- package/core/build/index.ts +1 -3
- package/core/build/live-components-generator.ts +231 -0
- package/core/build/optimizer.ts +2 -54
- package/core/cli/index.ts +2 -1
- package/core/config/env.ts +3 -1
- package/core/config/schema.ts +0 -3
- package/core/framework/server.ts +33 -1
- package/core/plugins/built-in/static/index.ts +24 -10
- package/core/plugins/manager.ts +51 -8
- package/core/utils/helpers.ts +9 -3
- package/fluxstack.config.ts +4 -10
- package/package.json +4 -3
- package/plugins/crypto-auth/README.md +238 -0
- package/plugins/crypto-auth/client/CryptoAuthClient.ts +325 -0
- package/plugins/crypto-auth/client/components/AuthProvider.tsx +190 -0
- package/plugins/crypto-auth/client/components/LoginButton.tsx +155 -0
- package/plugins/crypto-auth/client/components/ProtectedRoute.tsx +109 -0
- package/plugins/crypto-auth/client/components/SessionInfo.tsx +242 -0
- package/plugins/crypto-auth/client/components/index.ts +15 -0
- package/plugins/crypto-auth/client/index.ts +12 -0
- package/plugins/crypto-auth/index.ts +230 -0
- package/plugins/crypto-auth/package.json +65 -0
- package/plugins/crypto-auth/plugin.json +29 -0
- package/plugins/crypto-auth/server/AuthMiddleware.ts +237 -0
- package/plugins/crypto-auth/server/CryptoAuthService.ts +293 -0
- package/plugins/crypto-auth/server/index.ts +9 -0
- package/vite.config.ts +16 -0
|
@@ -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
|