create-fluxstack 1.12.0 → 1.13.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/LLMD/INDEX.md +8 -1
- package/LLMD/agent.md +867 -0
- package/LLMD/config/environment-vars.md +30 -0
- package/LLMD/resources/live-auth.md +447 -0
- package/LLMD/resources/live-components.md +79 -21
- package/LLMD/resources/live-logging.md +158 -0
- package/LLMD/resources/live-upload.md +1 -1
- package/LLMD/resources/rest-auth.md +290 -0
- package/README.md +520 -340
- package/app/client/src/App.tsx +11 -0
- package/app/client/src/components/AppLayout.tsx +1 -0
- package/app/client/src/live/AuthDemo.tsx +332 -0
- package/app/client/src/live/RoomChatDemo.tsx +24 -105
- package/app/server/auth/AuthManager.ts +213 -0
- package/app/server/auth/DevAuthProvider.ts +66 -0
- package/app/server/auth/HashManager.ts +123 -0
- package/app/server/auth/JWTAuthProvider.example.ts +101 -0
- package/app/server/auth/RateLimiter.ts +106 -0
- package/app/server/auth/contracts.ts +192 -0
- package/app/server/auth/guards/SessionGuard.ts +167 -0
- package/app/server/auth/guards/TokenGuard.ts +202 -0
- package/app/server/auth/index.ts +174 -0
- package/app/server/auth/middleware.ts +163 -0
- package/app/server/auth/providers/InMemoryProvider.ts +162 -0
- package/app/server/auth/sessions/SessionManager.ts +164 -0
- package/app/server/cache/CacheManager.ts +81 -0
- package/app/server/cache/MemoryDriver.ts +112 -0
- package/app/server/cache/contracts.ts +49 -0
- package/app/server/cache/index.ts +42 -0
- package/app/server/index.ts +14 -0
- package/app/server/live/LiveAdminPanel.ts +173 -0
- package/app/server/live/LiveCounter.ts +1 -0
- package/app/server/live/LiveLocalCounter.ts +13 -8
- package/app/server/live/LiveProtectedChat.ts +150 -0
- package/app/server/live/LiveRoomChat.ts +45 -203
- package/app/server/routes/auth.routes.ts +278 -0
- package/app/server/routes/index.ts +2 -0
- package/config/index.ts +8 -0
- package/config/system/auth.config.ts +49 -0
- package/config/system/session.config.ts +33 -0
- package/core/client/LiveComponentsProvider.tsx +76 -5
- package/core/client/components/Live.tsx +2 -1
- package/core/client/hooks/useLiveComponent.ts +47 -4
- package/core/client/index.ts +2 -1
- package/core/framework/server.ts +36 -4
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +15 -8
- package/core/plugins/built-in/monitoring/index.ts +10 -3
- package/core/plugins/built-in/vite/index.ts +95 -18
- package/core/plugins/config.ts +5 -4
- package/core/plugins/discovery.ts +11 -2
- package/core/plugins/manager.ts +11 -5
- package/core/plugins/module-resolver.ts +1 -1
- package/core/plugins/registry.ts +53 -25
- package/core/server/live/ComponentRegistry.ts +79 -24
- package/core/server/live/LiveComponentPerformanceMonitor.ts +9 -8
- package/core/server/live/LiveLogger.ts +111 -0
- package/core/server/live/LiveRoomManager.ts +5 -4
- package/core/server/live/StateSignature.ts +644 -643
- package/core/server/live/auth/LiveAuthContext.ts +71 -0
- package/core/server/live/auth/LiveAuthManager.ts +304 -0
- package/core/server/live/auth/index.ts +19 -0
- package/core/server/live/auth/types.ts +179 -0
- package/core/server/live/auto-generated-components.ts +8 -2
- package/core/server/live/index.ts +16 -0
- package/core/server/live/websocket-plugin.ts +92 -16
- package/core/templates/create-project.ts +0 -3
- package/core/types/types.ts +133 -13
- package/core/utils/index.ts +17 -17
- package/core/utils/logger/index.ts +5 -2
- package/core/utils/version.ts +1 -1
- package/package.json +1 -8
- package/plugins/crypto-auth/index.ts +6 -0
- package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +58 -0
- package/plugins/crypto-auth/server/index.ts +24 -21
- package/rest-tests/README.md +57 -0
- package/rest-tests/auth-token.http +113 -0
- package/rest-tests/auth.http +112 -0
- package/rest-tests/rooms-token.http +69 -0
- package/rest-tests/users-token.http +62 -0
- package/.dockerignore +0 -81
- package/Dockerfile +0 -70
- package/LIVE_COMPONENTS_REVIEW.md +0 -781
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FluxStack Auth System
|
|
3
|
+
*
|
|
4
|
+
* Sistema de autenticação modular inspirado no Laravel.
|
|
5
|
+
* Guard + Provider pattern com session e token support.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { auth, guest, initAuth, getAuthManager } from '@server/auth'
|
|
9
|
+
*
|
|
10
|
+
* // Inicializar (no boot da app)
|
|
11
|
+
* initAuth()
|
|
12
|
+
*
|
|
13
|
+
* // Proteger rotas
|
|
14
|
+
* app.use(auth()).get('/me', ({ user }) => user.toJSON())
|
|
15
|
+
* app.use(guest()).post('/login', loginHandler)
|
|
16
|
+
*
|
|
17
|
+
* // Usar auth manager diretamente
|
|
18
|
+
* const manager = getAuthManager()
|
|
19
|
+
* const guard = manager.guard('session')
|
|
20
|
+
* const user = await guard.attempt({ email, password })
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// Contracts
|
|
25
|
+
export type {
|
|
26
|
+
Authenticatable,
|
|
27
|
+
Guard,
|
|
28
|
+
UserProvider,
|
|
29
|
+
RequestContext,
|
|
30
|
+
CookieOptions,
|
|
31
|
+
GuardConfig,
|
|
32
|
+
GuardFactory,
|
|
33
|
+
} from './contracts'
|
|
34
|
+
|
|
35
|
+
// Auth Manager
|
|
36
|
+
export { AuthManager } from './AuthManager'
|
|
37
|
+
export type { AuthManagerConfig, ProviderConfig } from './AuthManager'
|
|
38
|
+
|
|
39
|
+
// Hash
|
|
40
|
+
export { HashManager, Hash, getHashManager, setHashManager } from './HashManager'
|
|
41
|
+
export type { HashAlgorithm, HashOptions } from './HashManager'
|
|
42
|
+
|
|
43
|
+
// Rate Limiter
|
|
44
|
+
export { RateLimiter } from './RateLimiter'
|
|
45
|
+
|
|
46
|
+
// Guards
|
|
47
|
+
export { SessionGuard } from './guards/SessionGuard'
|
|
48
|
+
export { TokenGuard } from './guards/TokenGuard'
|
|
49
|
+
|
|
50
|
+
// Providers
|
|
51
|
+
export { InMemoryUserProvider, InMemoryUser } from './providers/InMemoryProvider'
|
|
52
|
+
|
|
53
|
+
// Sessions
|
|
54
|
+
export { SessionManager } from './sessions/SessionManager'
|
|
55
|
+
export type { SessionData, SessionConfig } from './sessions/SessionManager'
|
|
56
|
+
|
|
57
|
+
// Middleware
|
|
58
|
+
export { auth, guest, authOptional } from './middleware'
|
|
59
|
+
|
|
60
|
+
// ===== Boot =====
|
|
61
|
+
|
|
62
|
+
import { AuthManager } from './AuthManager'
|
|
63
|
+
import { HashManager, setHashManager } from './HashManager'
|
|
64
|
+
import { RateLimiter } from './RateLimiter'
|
|
65
|
+
import { SessionManager } from './sessions/SessionManager'
|
|
66
|
+
import { InMemoryUserProvider } from './providers/InMemoryProvider'
|
|
67
|
+
import { setAuthManagerForMiddleware, buildRequestContext } from './middleware'
|
|
68
|
+
import { cacheManager } from '@server/cache'
|
|
69
|
+
import { authConfig } from '@config/system/auth.config'
|
|
70
|
+
import { sessionConfig } from '@config/system/session.config'
|
|
71
|
+
|
|
72
|
+
let authManagerInstance: AuthManager | null = null
|
|
73
|
+
let rateLimiterInstance: RateLimiter | null = null
|
|
74
|
+
let sessionManagerInstance: SessionManager | null = null
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Inicializa o sistema de auth.
|
|
78
|
+
* Deve ser chamado uma vez no boot da aplicação.
|
|
79
|
+
*/
|
|
80
|
+
export function initAuth(): {
|
|
81
|
+
authManager: AuthManager
|
|
82
|
+
rateLimiter: RateLimiter
|
|
83
|
+
sessionManager: SessionManager
|
|
84
|
+
} {
|
|
85
|
+
// 1. Configurar Hash
|
|
86
|
+
const hashManager = new HashManager({
|
|
87
|
+
algorithm: authConfig.passwords.hashAlgorithm as 'bcrypt' | 'argon2id',
|
|
88
|
+
bcryptRounds: authConfig.passwords.bcryptRounds,
|
|
89
|
+
})
|
|
90
|
+
setHashManager(hashManager)
|
|
91
|
+
|
|
92
|
+
// 2. Criar Session Manager
|
|
93
|
+
const cache = cacheManager.driver()
|
|
94
|
+
sessionManagerInstance = new SessionManager(cache, {
|
|
95
|
+
lifetime: sessionConfig.lifetime,
|
|
96
|
+
cookieName: sessionConfig.cookieName,
|
|
97
|
+
httpOnly: sessionConfig.httpOnly,
|
|
98
|
+
secure: sessionConfig.secure,
|
|
99
|
+
sameSite: sessionConfig.sameSite as 'strict' | 'lax' | 'none',
|
|
100
|
+
path: sessionConfig.path,
|
|
101
|
+
domain: sessionConfig.domain || undefined,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// 3. Criar Rate Limiter
|
|
105
|
+
rateLimiterInstance = new RateLimiter(cache)
|
|
106
|
+
|
|
107
|
+
// 4. Criar Auth Manager
|
|
108
|
+
authManagerInstance = new AuthManager(
|
|
109
|
+
{
|
|
110
|
+
defaults: {
|
|
111
|
+
guard: authConfig.defaults.guard ?? 'session',
|
|
112
|
+
provider: authConfig.defaults.provider ?? 'memory',
|
|
113
|
+
},
|
|
114
|
+
guards: {
|
|
115
|
+
session: {
|
|
116
|
+
driver: 'session',
|
|
117
|
+
provider: 'memory',
|
|
118
|
+
},
|
|
119
|
+
token: {
|
|
120
|
+
driver: 'token',
|
|
121
|
+
provider: 'memory',
|
|
122
|
+
tokenTtl: authConfig.token.ttl,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
providers: {
|
|
126
|
+
memory: {
|
|
127
|
+
driver: 'memory',
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
sessionManagerInstance
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
// 5. Registrar InMemoryProvider como default
|
|
135
|
+
const inMemoryProvider = new InMemoryUserProvider()
|
|
136
|
+
authManagerInstance.registerProvider('memory', inMemoryProvider)
|
|
137
|
+
|
|
138
|
+
// 6. Conectar middleware
|
|
139
|
+
setAuthManagerForMiddleware(authManagerInstance)
|
|
140
|
+
|
|
141
|
+
console.log(`🔐 Auth system initialized (guard: ${authConfig.defaults.guard}, hash: ${authConfig.passwords.hashAlgorithm})`)
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
authManager: authManagerInstance,
|
|
145
|
+
rateLimiter: rateLimiterInstance,
|
|
146
|
+
sessionManager: sessionManagerInstance,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Retorna o AuthManager (deve ser chamado após initAuth) */
|
|
151
|
+
export function getAuthManager(): AuthManager {
|
|
152
|
+
if (!authManagerInstance) {
|
|
153
|
+
throw new Error('Auth not initialized. Call initAuth() first.')
|
|
154
|
+
}
|
|
155
|
+
return authManagerInstance
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Retorna o RateLimiter (deve ser chamado após initAuth) */
|
|
159
|
+
export function getRateLimiter(): RateLimiter {
|
|
160
|
+
if (!rateLimiterInstance) {
|
|
161
|
+
throw new Error('Auth not initialized. Call initAuth() first.')
|
|
162
|
+
}
|
|
163
|
+
return rateLimiterInstance
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Retorna o SessionManager (deve ser chamado após initAuth) */
|
|
167
|
+
export function getSessionManager(): SessionManager {
|
|
168
|
+
if (!sessionManagerInstance) {
|
|
169
|
+
throw new Error('Auth not initialized. Call initAuth() first.')
|
|
170
|
+
}
|
|
171
|
+
return sessionManagerInstance
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export { buildRequestContext }
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FluxStack Auth - Elysia Middleware
|
|
3
|
+
*
|
|
4
|
+
* Middlewares prontos para proteger rotas:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* // Rota protegida (requer login)
|
|
8
|
+
* app.use(auth()).get('/me', ({ user }) => user.toJSON())
|
|
9
|
+
*
|
|
10
|
+
* // Rota de guest (requer NÃO estar logado)
|
|
11
|
+
* app.use(guest()).post('/login', loginHandler)
|
|
12
|
+
*
|
|
13
|
+
* // Guard específico
|
|
14
|
+
* app.use(auth('api')).get('/api/data', handler)
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { Elysia } from 'elysia'
|
|
19
|
+
import type { AuthManager } from './AuthManager'
|
|
20
|
+
import type { Authenticatable, RequestContext } from './contracts'
|
|
21
|
+
|
|
22
|
+
/** Referência ao AuthManager (setado no boot) */
|
|
23
|
+
let authManagerRef: AuthManager | null = null
|
|
24
|
+
|
|
25
|
+
export function setAuthManagerForMiddleware(manager: AuthManager): void {
|
|
26
|
+
authManagerRef = manager
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Extrai RequestContext do context do Elysia.
|
|
31
|
+
*/
|
|
32
|
+
function buildRequestContext(ctx: any): RequestContext {
|
|
33
|
+
const headers: Record<string, string | undefined> = {}
|
|
34
|
+
if (ctx.headers) {
|
|
35
|
+
for (const [key, value] of Object.entries(ctx.headers)) {
|
|
36
|
+
headers[key] = value as string | undefined
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
headers,
|
|
42
|
+
cookie: ctx.cookie ?? {},
|
|
43
|
+
setCookie: (name: string, value: string, options?: any) => {
|
|
44
|
+
if (ctx.cookie) {
|
|
45
|
+
ctx.cookie[name].set({
|
|
46
|
+
value,
|
|
47
|
+
...options,
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
removeCookie: (name: string) => {
|
|
52
|
+
if (ctx.cookie) {
|
|
53
|
+
ctx.cookie[name].set({
|
|
54
|
+
value: '',
|
|
55
|
+
maxAge: 0,
|
|
56
|
+
path: '/',
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
ip: ctx.request?.headers?.get('x-forwarded-for')
|
|
61
|
+
?? ctx.request?.headers?.get('x-real-ip')
|
|
62
|
+
?? ctx.server?.requestIP?.(ctx.request)?.address
|
|
63
|
+
?? '127.0.0.1',
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Middleware que requer autenticação.
|
|
69
|
+
* Injeta `user` (Authenticatable) no context do Elysia.
|
|
70
|
+
*
|
|
71
|
+
* Retorna 401 se não autenticado.
|
|
72
|
+
*/
|
|
73
|
+
export function auth(guardName?: string) {
|
|
74
|
+
return new Elysia({ name: `auth-guard${guardName ? `-${guardName}` : ''}` })
|
|
75
|
+
.derive(async (ctx) => {
|
|
76
|
+
if (!authManagerRef) {
|
|
77
|
+
throw new Error('Auth system not initialized. Did you forget to call initAuth()?')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const requestContext = buildRequestContext(ctx)
|
|
81
|
+
const guard = authManagerRef.freshGuard(guardName, requestContext)
|
|
82
|
+
|
|
83
|
+
const user = await guard.user()
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
user: user as Authenticatable | null,
|
|
87
|
+
auth: guard,
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
.onBeforeHandle(async (ctx) => {
|
|
91
|
+
if (!(ctx as any).user) {
|
|
92
|
+
(ctx as any).set.status = 401
|
|
93
|
+
return {
|
|
94
|
+
success: false,
|
|
95
|
+
error: 'Unauthenticated',
|
|
96
|
+
message: 'You must be logged in to access this resource.',
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
.as('scoped')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Middleware que requer NÃO estar autenticado.
|
|
105
|
+
* Útil para rotas de login/register.
|
|
106
|
+
*
|
|
107
|
+
* Retorna 409 se já autenticado.
|
|
108
|
+
*/
|
|
109
|
+
export function guest(guardName?: string) {
|
|
110
|
+
return new Elysia({ name: `guest-guard${guardName ? `-${guardName}` : ''}` })
|
|
111
|
+
.derive(async (ctx) => {
|
|
112
|
+
if (!authManagerRef) {
|
|
113
|
+
throw new Error('Auth system not initialized. Did you forget to call initAuth()?')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const requestContext = buildRequestContext(ctx)
|
|
117
|
+
const guard = authManagerRef.freshGuard(guardName, requestContext)
|
|
118
|
+
|
|
119
|
+
const user = await guard.user()
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
user: user as Authenticatable | null,
|
|
123
|
+
auth: guard,
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
.onBeforeHandle(async (ctx) => {
|
|
127
|
+
if ((ctx as any).user) {
|
|
128
|
+
(ctx as any).set.status = 409
|
|
129
|
+
return {
|
|
130
|
+
success: false,
|
|
131
|
+
error: 'AlreadyAuthenticated',
|
|
132
|
+
message: 'You are already logged in.',
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
.as('scoped')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Middleware que resolve auth opcionalmente (não bloqueia).
|
|
141
|
+
* Útil para rotas que funcionam com ou sem login.
|
|
142
|
+
*/
|
|
143
|
+
export function authOptional(guardName?: string) {
|
|
144
|
+
return new Elysia({ name: `auth-optional${guardName ? `-${guardName}` : ''}` })
|
|
145
|
+
.derive(async (ctx) => {
|
|
146
|
+
if (!authManagerRef) {
|
|
147
|
+
return { user: null, auth: null }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const requestContext = buildRequestContext(ctx)
|
|
151
|
+
const guard = authManagerRef.freshGuard(guardName, requestContext)
|
|
152
|
+
|
|
153
|
+
const user = await guard.user()
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
user: user as Authenticatable | null,
|
|
157
|
+
auth: guard,
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
.as('scoped')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export { buildRequestContext }
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FluxStack Auth - In-Memory User Provider
|
|
3
|
+
*
|
|
4
|
+
* Provider que armazena usuários em memória.
|
|
5
|
+
* Ideal para desenvolvimento, demos e testes.
|
|
6
|
+
*
|
|
7
|
+
* Para produção, implemente UserProvider com seu ORM:
|
|
8
|
+
* ```ts
|
|
9
|
+
* class DrizzleUserProvider implements UserProvider {
|
|
10
|
+
* async retrieveById(id) {
|
|
11
|
+
* return db.select().from(users).where(eq(users.id, id)).get()
|
|
12
|
+
* }
|
|
13
|
+
* // ...
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Authenticatable, UserProvider } from '../contracts'
|
|
19
|
+
import { Hash } from '../HashManager'
|
|
20
|
+
|
|
21
|
+
// ===== In-Memory User Model =====
|
|
22
|
+
|
|
23
|
+
export class InMemoryUser implements Authenticatable {
|
|
24
|
+
id: number
|
|
25
|
+
name: string
|
|
26
|
+
email: string
|
|
27
|
+
passwordHash: string
|
|
28
|
+
rememberToken: string | null = null
|
|
29
|
+
createdAt: Date
|
|
30
|
+
|
|
31
|
+
constructor(data: {
|
|
32
|
+
id: number
|
|
33
|
+
name: string
|
|
34
|
+
email: string
|
|
35
|
+
passwordHash: string
|
|
36
|
+
createdAt?: Date
|
|
37
|
+
}) {
|
|
38
|
+
this.id = data.id
|
|
39
|
+
this.name = data.name
|
|
40
|
+
this.email = data.email
|
|
41
|
+
this.passwordHash = data.passwordHash
|
|
42
|
+
this.createdAt = data.createdAt ?? new Date()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getAuthId(): number {
|
|
46
|
+
return this.id
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getAuthIdField(): string {
|
|
50
|
+
return 'id'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getAuthPassword(): string {
|
|
54
|
+
return this.passwordHash
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getRememberToken(): string | null {
|
|
58
|
+
return this.rememberToken
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
setRememberToken(token: string | null): void {
|
|
62
|
+
this.rememberToken = token
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
toJSON(): Record<string, unknown> {
|
|
66
|
+
return {
|
|
67
|
+
id: this.id,
|
|
68
|
+
name: this.name,
|
|
69
|
+
email: this.email,
|
|
70
|
+
createdAt: this.createdAt.toISOString(),
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ===== Provider =====
|
|
76
|
+
|
|
77
|
+
export class InMemoryUserProvider implements UserProvider {
|
|
78
|
+
private users: InMemoryUser[] = []
|
|
79
|
+
private nextId = 1
|
|
80
|
+
|
|
81
|
+
async retrieveById(id: string | number): Promise<Authenticatable | null> {
|
|
82
|
+
return this.users.find(u => u.id === Number(id)) ?? null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async retrieveByCredentials(credentials: Record<string, unknown>): Promise<Authenticatable | null> {
|
|
86
|
+
// Buscar por qualquer campo EXCETO password
|
|
87
|
+
const { password: _, ...searchFields } = credentials
|
|
88
|
+
|
|
89
|
+
return this.users.find(user => {
|
|
90
|
+
return Object.entries(searchFields).every(([key, value]) => {
|
|
91
|
+
return (user as any)[key] === value
|
|
92
|
+
})
|
|
93
|
+
}) ?? null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async validateCredentials(user: Authenticatable, credentials: Record<string, unknown>): Promise<boolean> {
|
|
97
|
+
const password = credentials.password as string | undefined
|
|
98
|
+
if (!password) return false
|
|
99
|
+
|
|
100
|
+
const valid = await Hash.check(password, user.getAuthPassword())
|
|
101
|
+
|
|
102
|
+
// Rehash transparente se necessário
|
|
103
|
+
if (valid && Hash.needsRehash(user.getAuthPassword())) {
|
|
104
|
+
const newHash = await Hash.make(password)
|
|
105
|
+
// Atualizar hash in-memory
|
|
106
|
+
const memUser = user as InMemoryUser
|
|
107
|
+
memUser.passwordHash = newHash
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return valid
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async retrieveByToken(id: string | number, token: string): Promise<Authenticatable | null> {
|
|
114
|
+
const user = this.users.find(u => u.id === Number(id))
|
|
115
|
+
if (!user) return null
|
|
116
|
+
if (user.getRememberToken() !== token) return null
|
|
117
|
+
return user
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async updateRememberToken(user: Authenticatable, token: string | null): Promise<void> {
|
|
121
|
+
user.setRememberToken(token)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async createUser(data: Record<string, unknown>): Promise<Authenticatable> {
|
|
125
|
+
const name = data.name as string
|
|
126
|
+
const email = data.email as string
|
|
127
|
+
const password = data.password as string
|
|
128
|
+
|
|
129
|
+
if (!name || !email || !password) {
|
|
130
|
+
throw new Error('Name, email and password are required')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Verificar email duplicado
|
|
134
|
+
const existing = this.users.find(u => u.email === email)
|
|
135
|
+
if (existing) {
|
|
136
|
+
throw new Error('Email already in use')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const passwordHash = await Hash.make(password)
|
|
140
|
+
|
|
141
|
+
const user = new InMemoryUser({
|
|
142
|
+
id: this.nextId++,
|
|
143
|
+
name,
|
|
144
|
+
email,
|
|
145
|
+
passwordHash,
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
this.users.push(user)
|
|
149
|
+
return user
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Para testes: reset completo */
|
|
153
|
+
reset(): void {
|
|
154
|
+
this.users = []
|
|
155
|
+
this.nextId = 1
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Para testes: retorna todos os usuários */
|
|
159
|
+
getAll(): InMemoryUser[] {
|
|
160
|
+
return [...this.users]
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FluxStack Auth - Session Manager
|
|
3
|
+
*
|
|
4
|
+
* Gerencia sessões de usuários usando o cache system modular.
|
|
5
|
+
* Cada sessão é um registro no cache com TTL automático.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* const sessionId = await sessionManager.create({ userId: 1 })
|
|
9
|
+
* const data = await sessionManager.read(sessionId)
|
|
10
|
+
* await sessionManager.destroy(sessionId)
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { CacheDriver } from '@server/cache/contracts'
|
|
15
|
+
import { cacheManager } from '@server/cache'
|
|
16
|
+
|
|
17
|
+
export interface SessionData {
|
|
18
|
+
[key: string]: unknown
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SessionConfig {
|
|
22
|
+
/** Tempo de vida da sessão em segundos (default: 7200 = 2h) */
|
|
23
|
+
lifetime: number
|
|
24
|
+
/** Nome do cookie (default: 'fluxstack_session') */
|
|
25
|
+
cookieName: string
|
|
26
|
+
/** Cookie httpOnly (default: true) */
|
|
27
|
+
httpOnly: boolean
|
|
28
|
+
/** Cookie secure (default: false em dev, true em prod) */
|
|
29
|
+
secure: boolean
|
|
30
|
+
/** Cookie sameSite (default: 'lax') */
|
|
31
|
+
sameSite: 'strict' | 'lax' | 'none'
|
|
32
|
+
/** Cookie path (default: '/') */
|
|
33
|
+
path: string
|
|
34
|
+
/** Cookie domain (default: undefined = current domain) */
|
|
35
|
+
domain?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const DEFAULT_CONFIG: SessionConfig = {
|
|
39
|
+
lifetime: 7200,
|
|
40
|
+
cookieName: 'fluxstack_session',
|
|
41
|
+
httpOnly: true,
|
|
42
|
+
secure: false,
|
|
43
|
+
sameSite: 'lax',
|
|
44
|
+
path: '/',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class SessionManager {
|
|
48
|
+
private cache: CacheDriver
|
|
49
|
+
private config: SessionConfig
|
|
50
|
+
|
|
51
|
+
constructor(cache?: CacheDriver, config?: Partial<SessionConfig>) {
|
|
52
|
+
this.cache = cache ?? cacheManager.driver()
|
|
53
|
+
this.config = { ...DEFAULT_CONFIG, ...config }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Cria uma nova sessão e retorna o ID.
|
|
58
|
+
*/
|
|
59
|
+
async create(data: SessionData = {}): Promise<string> {
|
|
60
|
+
const sessionId = this.generateId()
|
|
61
|
+
await this.cache.set(
|
|
62
|
+
this.cacheKey(sessionId),
|
|
63
|
+
{ ...data, _createdAt: Date.now() },
|
|
64
|
+
this.config.lifetime
|
|
65
|
+
)
|
|
66
|
+
return sessionId
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Lê os dados de uma sessão.
|
|
71
|
+
*/
|
|
72
|
+
async read(sessionId: string): Promise<SessionData | null> {
|
|
73
|
+
return this.cache.get<SessionData>(this.cacheKey(sessionId))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Atualiza os dados de uma sessão (merge com dados existentes).
|
|
78
|
+
*/
|
|
79
|
+
async update(sessionId: string, data: SessionData): Promise<void> {
|
|
80
|
+
const existing = await this.read(sessionId)
|
|
81
|
+
if (!existing) return
|
|
82
|
+
|
|
83
|
+
await this.cache.set(
|
|
84
|
+
this.cacheKey(sessionId),
|
|
85
|
+
{ ...existing, ...data },
|
|
86
|
+
this.config.lifetime
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Define um valor específico na sessão.
|
|
92
|
+
*/
|
|
93
|
+
async put(sessionId: string, key: string, value: unknown): Promise<void> {
|
|
94
|
+
const existing = await this.read(sessionId)
|
|
95
|
+
if (!existing) return
|
|
96
|
+
|
|
97
|
+
existing[key] = value
|
|
98
|
+
await this.cache.set(
|
|
99
|
+
this.cacheKey(sessionId),
|
|
100
|
+
existing,
|
|
101
|
+
this.config.lifetime
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Remove um valor específico da sessão.
|
|
107
|
+
*/
|
|
108
|
+
async forget(sessionId: string, key: string): Promise<void> {
|
|
109
|
+
const existing = await this.read(sessionId)
|
|
110
|
+
if (!existing) return
|
|
111
|
+
|
|
112
|
+
delete existing[key]
|
|
113
|
+
await this.cache.set(
|
|
114
|
+
this.cacheKey(sessionId),
|
|
115
|
+
existing,
|
|
116
|
+
this.config.lifetime
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Destroi uma sessão.
|
|
122
|
+
*/
|
|
123
|
+
async destroy(sessionId: string): Promise<void> {
|
|
124
|
+
await this.cache.delete(this.cacheKey(sessionId))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Regenera o ID da sessão (mantém os dados).
|
|
129
|
+
* Importante após login para prevenir session fixation.
|
|
130
|
+
*/
|
|
131
|
+
async regenerate(oldSessionId: string): Promise<string> {
|
|
132
|
+
const data = await this.read(oldSessionId)
|
|
133
|
+
await this.destroy(oldSessionId)
|
|
134
|
+
|
|
135
|
+
const newSessionId = this.generateId()
|
|
136
|
+
if (data) {
|
|
137
|
+
await this.cache.set(
|
|
138
|
+
this.cacheKey(newSessionId),
|
|
139
|
+
data,
|
|
140
|
+
this.config.lifetime
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
return newSessionId
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Retorna a configuração de sessão */
|
|
147
|
+
getConfig(): SessionConfig {
|
|
148
|
+
return this.config
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Gera um session ID criptograficamente seguro */
|
|
152
|
+
private generateId(): string {
|
|
153
|
+
const bytes = new Uint8Array(32)
|
|
154
|
+
crypto.getRandomValues(bytes)
|
|
155
|
+
return Array.from(bytes)
|
|
156
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
157
|
+
.join('')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Chave no cache para a sessão */
|
|
161
|
+
private cacheKey(sessionId: string): string {
|
|
162
|
+
return `session:${sessionId}`
|
|
163
|
+
}
|
|
164
|
+
}
|