@gzl10/baserow 1.2.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/CHANGELOG.md +435 -0
- package/README.md +847 -0
- package/dist/index.d.ts +8749 -0
- package/dist/index.js +11167 -0
- package/dist/index.js.map +1 -0
- package/package.json +91 -0
- package/src/BaserowClient.ts +501 -0
- package/src/ClientWithCreds.ts +545 -0
- package/src/ClientWithCredsWs.ts +852 -0
- package/src/ClientWithToken.ts +171 -0
- package/src/contexts/DatabaseClientContext.ts +114 -0
- package/src/contexts/DatabaseContext.ts +870 -0
- package/src/contexts/DatabaseTokenContext.ts +331 -0
- package/src/contexts/FieldContext.ts +399 -0
- package/src/contexts/RowContext.ts +99 -0
- package/src/contexts/TableClientContext.ts +291 -0
- package/src/contexts/TableContext.ts +1247 -0
- package/src/contexts/TableOnlyContext.ts +74 -0
- package/src/contexts/WorkspaceContext.ts +490 -0
- package/src/express/errors.ts +260 -0
- package/src/express/index.ts +69 -0
- package/src/express/middleware.ts +225 -0
- package/src/express/serializers.ts +314 -0
- package/src/index.ts +247 -0
- package/src/presets/performance.ts +262 -0
- package/src/services/AuthService.ts +472 -0
- package/src/services/DatabaseService.ts +246 -0
- package/src/services/DatabaseTokenService.ts +186 -0
- package/src/services/FieldService.ts +1543 -0
- package/src/services/RowService.ts +982 -0
- package/src/services/SchemaControlService.ts +420 -0
- package/src/services/TableService.ts +781 -0
- package/src/services/WorkspaceService.ts +113 -0
- package/src/services/core/BaseAuthClient.ts +111 -0
- package/src/services/core/BaseClient.ts +107 -0
- package/src/services/core/BaseService.ts +71 -0
- package/src/services/core/HttpService.ts +115 -0
- package/src/services/core/ValidationService.ts +149 -0
- package/src/types/auth.ts +177 -0
- package/src/types/core.ts +91 -0
- package/src/types/errors.ts +105 -0
- package/src/types/fields.ts +456 -0
- package/src/types/index.ts +222 -0
- package/src/types/requests.ts +333 -0
- package/src/types/responses.ts +50 -0
- package/src/types/schema.ts +446 -0
- package/src/types/tokens.ts +36 -0
- package/src/types.ts +11 -0
- package/src/utils/auth.ts +174 -0
- package/src/utils/axios.ts +647 -0
- package/src/utils/field-cache.ts +164 -0
- package/src/utils/httpFactory.ts +66 -0
- package/src/utils/jwt-decoder.ts +188 -0
- package/src/utils/jwtTokens.ts +50 -0
- package/src/utils/performance.ts +105 -0
- package/src/utils/prisma-mapper.ts +961 -0
- package/src/utils/validation.ts +463 -0
- package/src/validators/schema.ts +419 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import { AxiosResponse } from 'axios'
|
|
2
|
+
import { HttpService } from './core/HttpService'
|
|
3
|
+
import { HttpClient } from '../utils/axios'
|
|
4
|
+
import {
|
|
5
|
+
LoginResponse,
|
|
6
|
+
RefreshTokenRequest,
|
|
7
|
+
RefreshTokenResponse,
|
|
8
|
+
BaserowError,
|
|
9
|
+
BaserowAuthTokenInvalidError,
|
|
10
|
+
BaserowReLoginRequiredError,
|
|
11
|
+
BaserowCredentials,
|
|
12
|
+
Logger
|
|
13
|
+
} from '../types'
|
|
14
|
+
import { validateString } from '../utils/validation'
|
|
15
|
+
import { decodeJwtExpiry } from '../utils/jwt-decoder'
|
|
16
|
+
|
|
17
|
+
export class AuthService extends HttpService {
|
|
18
|
+
private accessToken?: string
|
|
19
|
+
private refreshToken?: string
|
|
20
|
+
private tokenExpiry?: Date
|
|
21
|
+
private keepAliveTimer?: NodeJS.Timeout
|
|
22
|
+
private credentials?: BaserowCredentials
|
|
23
|
+
|
|
24
|
+
constructor(http: HttpClient, logger?: Logger) {
|
|
25
|
+
super(http, logger)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Realizar login con credenciales y obtener JWT tokens
|
|
30
|
+
*
|
|
31
|
+
* Autentica al usuario con Baserow y almacena los tokens de acceso y refresh en memoria.
|
|
32
|
+
* Si keep-alive está habilitado, también almacena las credenciales para re-login automático.
|
|
33
|
+
*
|
|
34
|
+
* @param credentials - Email y password del usuario Baserow
|
|
35
|
+
* @returns Respuesta de login con tokens y datos del usuario
|
|
36
|
+
* @throws {BaserowError} Si las credenciales son inválidas (401)
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* const response = await authService.login({
|
|
41
|
+
* email: 'user@example.com',
|
|
42
|
+
* password: 'securepassword'
|
|
43
|
+
* })
|
|
44
|
+
* console.log(`Logged in as: ${response.user.username}`)
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @since 1.0.0
|
|
48
|
+
*/
|
|
49
|
+
async login(credentials: BaserowCredentials): Promise<LoginResponse> {
|
|
50
|
+
validateString(credentials.email, 'email')
|
|
51
|
+
validateString(credentials.password, 'password')
|
|
52
|
+
|
|
53
|
+
this.logger?.info?.(`🔐 Attempting login for user: ${credentials.email}`)
|
|
54
|
+
|
|
55
|
+
const loginData = {
|
|
56
|
+
username: credentials.email,
|
|
57
|
+
password: credentials.password
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const response = await this.http.post<LoginResponse>('/user/token-auth/', loginData)
|
|
62
|
+
|
|
63
|
+
// Almacenar tokens en memoria
|
|
64
|
+
this.accessToken = response.access_token
|
|
65
|
+
this.refreshToken = response.refresh_token
|
|
66
|
+
|
|
67
|
+
// Almacenar credenciales para keep-alive (re-login automático)
|
|
68
|
+
this.credentials = credentials
|
|
69
|
+
|
|
70
|
+
// Decodificar expiración desde el claim 'exp' del JWT (estándar)
|
|
71
|
+
this.tokenExpiry = decodeJwtExpiry(response.access_token, this.logger)
|
|
72
|
+
if (!this.tokenExpiry) {
|
|
73
|
+
this.logger?.warn?.('⚠️ No se pudo decodificar exp del JWT, usando fallback de 10 min')
|
|
74
|
+
this.tokenExpiry = new Date(Date.now() + 10 * 60 * 1000) // Fallback conservador
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Actualizar el token en HttpClient
|
|
78
|
+
this.http.setAuthToken(response.access_token)
|
|
79
|
+
|
|
80
|
+
this.logger?.info?.(`✅ Login successful for user: ${response.user.username} (ID: ${response.user.id})`)
|
|
81
|
+
this.logger?.debug?.(`Access token expires at: ${this.tokenExpiry.toISOString()}`)
|
|
82
|
+
this.logger?.debug?.(`Refresh token stored (expires ~7 days from now)`)
|
|
83
|
+
|
|
84
|
+
return response
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error instanceof BaserowError && error.status === 401) {
|
|
87
|
+
this.logger?.error?.(`❌ Login failed: Invalid credentials for ${credentials.email}`)
|
|
88
|
+
throw new BaserowError('Invalid credentials', 401, 'INVALID_CREDENTIALS')
|
|
89
|
+
}
|
|
90
|
+
this.logger?.error?.('❌ Login failed with unexpected error:', error)
|
|
91
|
+
throw error
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Renovar access token usando refresh token
|
|
97
|
+
*
|
|
98
|
+
* IMPORTANTE: En Baserow, el refresh token NO se renueva al hacer refresh (ROTATE_REFRESH_TOKENS=False).
|
|
99
|
+
* Solo se obtiene un nuevo access_token válido por 10 minutos. El refresh_token expira 7 días después
|
|
100
|
+
* del login original y NO se extiende con actividad.
|
|
101
|
+
*
|
|
102
|
+
* Para backends 24/7, usar keep-alive con re-login automático en lugar de depender de refresh token.
|
|
103
|
+
*
|
|
104
|
+
* @returns Nuevo access token válido
|
|
105
|
+
* @throws {BaserowReLoginRequiredError} Si el refresh token expiró (401) o no está disponible
|
|
106
|
+
* @throws {BaserowAuthTokenInvalidError} Si el refresh token es inválido (400)
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```typescript
|
|
110
|
+
* try {
|
|
111
|
+
* const newToken = await authService.refreshAccessToken()
|
|
112
|
+
* console.log('Token renovado exitosamente')
|
|
113
|
+
* } catch (error) {
|
|
114
|
+
* if (error instanceof BaserowReLoginRequiredError) {
|
|
115
|
+
* console.log('Refresh token expirado, se requiere re-login')
|
|
116
|
+
* await authService.login(credentials)
|
|
117
|
+
* }
|
|
118
|
+
* }
|
|
119
|
+
* ```
|
|
120
|
+
*
|
|
121
|
+
* @since 1.0.0
|
|
122
|
+
*/
|
|
123
|
+
async refreshAccessToken(): Promise<string> {
|
|
124
|
+
if (!this.refreshToken) {
|
|
125
|
+
this.logger?.error?.('❌ No refresh token available - tokens may have been lost')
|
|
126
|
+
throw new BaserowReLoginRequiredError('tokens_lost', {
|
|
127
|
+
message: 'No refresh token available. This can happen after process restart or if tokens were manually cleared.'
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const refreshData: RefreshTokenRequest = {
|
|
132
|
+
refresh_token: this.refreshToken
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
this.logger?.debug?.('🔄 Attempting to refresh access token...')
|
|
137
|
+
|
|
138
|
+
const response = await this.http.post<RefreshTokenResponse>('/user/token-refresh/', refreshData)
|
|
139
|
+
|
|
140
|
+
// Actualizar tokens almacenados
|
|
141
|
+
this.accessToken = response.access_token
|
|
142
|
+
// El refresh token se mantiene igual, no se renueva
|
|
143
|
+
|
|
144
|
+
// Decodificar expiración desde el claim 'exp' del JWT (estándar)
|
|
145
|
+
this.tokenExpiry = decodeJwtExpiry(response.access_token, this.logger)
|
|
146
|
+
if (!this.tokenExpiry) {
|
|
147
|
+
this.logger?.warn?.('⚠️ No se pudo decodificar exp del JWT, usando fallback de 10 min')
|
|
148
|
+
this.tokenExpiry = new Date(Date.now() + 10 * 60 * 1000) // Fallback conservador
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Actualizar el token en HttpClient
|
|
152
|
+
this.http.setAuthToken(response.access_token)
|
|
153
|
+
|
|
154
|
+
this.logger?.info?.('✅ Refresh token successful - new access token obtained')
|
|
155
|
+
this.logger?.debug?.(`New token expires at: ${this.tokenExpiry.toISOString()}`)
|
|
156
|
+
|
|
157
|
+
return response.access_token
|
|
158
|
+
} catch (error) {
|
|
159
|
+
// Si el refresh falla, limpiar tokens
|
|
160
|
+
this.clearTokens()
|
|
161
|
+
|
|
162
|
+
// Proporcionar mensajes de error específicos y accionables
|
|
163
|
+
if (error instanceof BaserowError) {
|
|
164
|
+
if (error.status === 401) {
|
|
165
|
+
// 401 en refresh significa que el refresh token expiró (típicamente después de ~7 días de inactividad)
|
|
166
|
+
this.logger?.error?.('❌ Refresh token has EXPIRED - Re-authentication required')
|
|
167
|
+
this.logger?.error?.(' Refresh tokens expire after ~7 days of inactivity in Baserow')
|
|
168
|
+
this.logger?.error?.(' User must login again with email/password')
|
|
169
|
+
|
|
170
|
+
throw new BaserowReLoginRequiredError('refresh_token_expired', {
|
|
171
|
+
originalError: error.message,
|
|
172
|
+
hint: 'Baserow refresh tokens expire after approximately 7 days. User must re-authenticate with credentials.'
|
|
173
|
+
})
|
|
174
|
+
} else if (error.status === 400) {
|
|
175
|
+
this.logger?.error?.('❌ Refresh token invalid or corrupted')
|
|
176
|
+
throw new BaserowAuthTokenInvalidError('Refresh token is invalid or corrupted', {
|
|
177
|
+
originalStatus: error.status,
|
|
178
|
+
originalCode: error.code
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this.logger?.error?.('❌ Refresh token failed with unexpected error:', error)
|
|
184
|
+
throw error
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Verificar si el token actual ha expirado
|
|
190
|
+
*
|
|
191
|
+
* Considera el token expirado si:
|
|
192
|
+
* - No hay token almacenado
|
|
193
|
+
* - No hay fecha de expiración conocida
|
|
194
|
+
* - Expira en los próximos 5 minutos (margen de seguridad)
|
|
195
|
+
*
|
|
196
|
+
* @returns `true` si el token está expirado o próximo a expirar, `false` si es válido
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```typescript
|
|
200
|
+
* if (authService.isTokenExpired()) {
|
|
201
|
+
* await authService.refreshAccessToken()
|
|
202
|
+
* }
|
|
203
|
+
* ```
|
|
204
|
+
*
|
|
205
|
+
* @since 1.0.0
|
|
206
|
+
*/
|
|
207
|
+
isTokenExpired(): boolean {
|
|
208
|
+
if (!this.tokenExpiry || !this.accessToken) {
|
|
209
|
+
return true
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Verificar si expira en los próximos 5 minutos
|
|
213
|
+
const fiveMinutesFromNow = new Date(Date.now() + 5 * 60 * 1000)
|
|
214
|
+
return this.tokenExpiry <= fiveMinutesFromNow
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Obtener token válido, renovándolo automáticamente si ha expirado
|
|
219
|
+
*
|
|
220
|
+
* Verifica la expiración del token y lo renueva automáticamente si es necesario.
|
|
221
|
+
* Ideal para usar antes de realizar requests a la API.
|
|
222
|
+
*
|
|
223
|
+
* @returns Access token válido
|
|
224
|
+
* @throws {BaserowError} Si no hay token disponible y se requiere login
|
|
225
|
+
* @throws {BaserowReLoginRequiredError} Si el refresh token expiró
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```typescript
|
|
229
|
+
* const token = await authService.getValidToken()
|
|
230
|
+
* // Usar token en headers de request
|
|
231
|
+
* ```
|
|
232
|
+
*
|
|
233
|
+
* @since 1.0.0
|
|
234
|
+
*/
|
|
235
|
+
async getValidToken(): Promise<string> {
|
|
236
|
+
if (!this.accessToken) {
|
|
237
|
+
throw new BaserowError('No access token available, login required', 401, 'NO_ACCESS_TOKEN')
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (this.isTokenExpired()) {
|
|
241
|
+
await this.refreshAccessToken()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return this.accessToken!
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Verificar si el usuario está autenticado con token válido
|
|
249
|
+
*
|
|
250
|
+
* @returns `true` si hay token válido y no expirado, `false` en caso contrario
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* ```typescript
|
|
254
|
+
* if (!authService.isAuthenticated()) {
|
|
255
|
+
* await authService.login(credentials)
|
|
256
|
+
* }
|
|
257
|
+
* ```
|
|
258
|
+
*
|
|
259
|
+
* @since 1.0.0
|
|
260
|
+
*/
|
|
261
|
+
isAuthenticated(): boolean {
|
|
262
|
+
return !!this.accessToken && !this.isTokenExpired()
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Obtener el access token actual (sin validar expiración)
|
|
267
|
+
*
|
|
268
|
+
* Retorna el token almacenado en memoria sin verificar si está expirado.
|
|
269
|
+
* Para obtener un token válido garantizado, usar `getValidToken()`.
|
|
270
|
+
*
|
|
271
|
+
* @returns Access token actual o `undefined` si no hay token
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* ```typescript
|
|
275
|
+
* const token = authService.getCurrentToken()
|
|
276
|
+
* if (token) {
|
|
277
|
+
* console.log('Token disponible')
|
|
278
|
+
* }
|
|
279
|
+
* ```
|
|
280
|
+
*
|
|
281
|
+
* @since 1.0.0
|
|
282
|
+
*/
|
|
283
|
+
getCurrentToken(): string | undefined {
|
|
284
|
+
return this.accessToken
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Limpiar todos los tokens y credenciales almacenadas
|
|
289
|
+
*
|
|
290
|
+
* Limpia access token, refresh token, credenciales y detiene el keep-alive si está activo.
|
|
291
|
+
* También limpia el token del HttpClient.
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```typescript
|
|
295
|
+
* authService.clearTokens()
|
|
296
|
+
* console.log('Sesión limpiada')
|
|
297
|
+
* ```
|
|
298
|
+
*
|
|
299
|
+
* @since 1.0.0
|
|
300
|
+
*/
|
|
301
|
+
clearTokens(): void {
|
|
302
|
+
this.stopKeepAlive()
|
|
303
|
+
this.accessToken = undefined
|
|
304
|
+
this.refreshToken = undefined
|
|
305
|
+
this.tokenExpiry = undefined
|
|
306
|
+
this.credentials = undefined
|
|
307
|
+
this.http.clearAuthToken()
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Cerrar sesión y limpiar tokens
|
|
312
|
+
*
|
|
313
|
+
* Alias de `clearTokens()`. Limpia todos los tokens, credenciales y detiene keep-alive.
|
|
314
|
+
* No hace petición al servidor (Baserow no tiene endpoint de logout).
|
|
315
|
+
*
|
|
316
|
+
* @example
|
|
317
|
+
* ```typescript
|
|
318
|
+
* authService.logout()
|
|
319
|
+
* console.log('Sesión cerrada')
|
|
320
|
+
* ```
|
|
321
|
+
*
|
|
322
|
+
* @since 1.0.0
|
|
323
|
+
*/
|
|
324
|
+
logout(): void {
|
|
325
|
+
this.clearTokens()
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Iniciar keep-alive automático para evitar expiración de refresh token
|
|
330
|
+
*
|
|
331
|
+
* IMPORTANTE: El refresh token de Baserow expira 7 días después del login (fijo, NO se renueva con refresh).
|
|
332
|
+
* Keep-alive ejecuta RE-LOGIN completo periódicamente para obtener tokens frescos (access + refresh)
|
|
333
|
+
* y mantener la sesión activa indefinidamente en aplicaciones backend 24/7.
|
|
334
|
+
*
|
|
335
|
+
* ⚠️ SEGURIDAD: Las credenciales se almacenan en memoria del proceso.
|
|
336
|
+
* Solo usar en backends propios/confiables, NO en frontends o apps compartidas.
|
|
337
|
+
*
|
|
338
|
+
* @param intervalMinutes - Intervalo en minutos para re-login (default: 7200 = 5 días, margen 2 días)
|
|
339
|
+
*
|
|
340
|
+
* @example
|
|
341
|
+
* ```typescript
|
|
342
|
+
* // Re-login cada 5 días (recomendado para backends 24/7)
|
|
343
|
+
* authService.startKeepAlive()
|
|
344
|
+
*
|
|
345
|
+
* // Re-login cada 3 días (más conservador, margen 4 días)
|
|
346
|
+
* authService.startKeepAlive(4320)
|
|
347
|
+
* ```
|
|
348
|
+
*
|
|
349
|
+
* @since 1.1.0
|
|
350
|
+
*/
|
|
351
|
+
startKeepAlive(intervalMinutes: number = 7200): void {
|
|
352
|
+
// Prevenir múltiples timers simultáneos
|
|
353
|
+
if (this.keepAliveTimer) {
|
|
354
|
+
this.logger?.warn?.('⚠️ Keep-alive ya está activo, deteniendo timer anterior')
|
|
355
|
+
this.stopKeepAlive()
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (!this.credentials) {
|
|
359
|
+
this.logger?.error?.('❌ Keep-alive requiere credenciales almacenadas')
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const intervalMs = intervalMinutes * 60 * 1000
|
|
364
|
+
const days = (intervalMinutes / 1440).toFixed(1)
|
|
365
|
+
|
|
366
|
+
this.logger?.info?.(`🔄 Keep-alive iniciado: re-login cada ${days} días (${intervalMinutes} min)`)
|
|
367
|
+
|
|
368
|
+
this.keepAliveTimer = setInterval(async () => {
|
|
369
|
+
if (!this.credentials) {
|
|
370
|
+
this.logger?.error?.('❌ Keep-alive: credenciales no disponibles')
|
|
371
|
+
return
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
this.logger?.debug?.('🔄 Keep-alive: ejecutando re-login preventivo...')
|
|
376
|
+
await this.login(this.credentials)
|
|
377
|
+
this.logger?.info?.('✅ Keep-alive: Re-login exitoso, tokens renovados (válidos 7 días más)')
|
|
378
|
+
} catch (error) {
|
|
379
|
+
this.logger?.error?.('❌ Keep-alive: error en re-login preventivo:', error)
|
|
380
|
+
// No detenemos el timer, siguiente intento puede funcionar
|
|
381
|
+
}
|
|
382
|
+
}, intervalMs)
|
|
383
|
+
|
|
384
|
+
// Prevenir que el timer mantenga el proceso Node.js vivo indefinidamente
|
|
385
|
+
if (this.keepAliveTimer.unref) {
|
|
386
|
+
this.keepAliveTimer.unref()
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Detener keep-alive automático
|
|
392
|
+
*
|
|
393
|
+
* Limpia el timer de keep-alive. Se llama automáticamente en `logout()` y `clearTokens()`.
|
|
394
|
+
*
|
|
395
|
+
* @example
|
|
396
|
+
* ```typescript
|
|
397
|
+
* // Detener manualmente el keep-alive
|
|
398
|
+
* authService.stopKeepAlive()
|
|
399
|
+
* ```
|
|
400
|
+
*
|
|
401
|
+
* @since 1.1.0
|
|
402
|
+
*/
|
|
403
|
+
stopKeepAlive(): void {
|
|
404
|
+
if (this.keepAliveTimer) {
|
|
405
|
+
clearInterval(this.keepAliveTimer)
|
|
406
|
+
this.keepAliveTimer = undefined
|
|
407
|
+
this.logger?.info?.('🛑 Keep-alive detenido')
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Configurar interceptor para auto-refresh en 401 usando interceptors nativos de Axios
|
|
413
|
+
*
|
|
414
|
+
* Intercepta respuestas 401 (Unauthorized) y automáticamente intenta renovar el token
|
|
415
|
+
* usando refresh token. Si la renovación es exitosa, reintenta la petición original.
|
|
416
|
+
*
|
|
417
|
+
* NO intercepta endpoints de autenticación (`/user/token-*`) para evitar loops infinitos.
|
|
418
|
+
*
|
|
419
|
+
* @example
|
|
420
|
+
* ```typescript
|
|
421
|
+
* await authService.login(credentials)
|
|
422
|
+
* authService.setupAutoRefresh()
|
|
423
|
+
* // Ahora los 401 se manejan automáticamente
|
|
424
|
+
* ```
|
|
425
|
+
*
|
|
426
|
+
* @since 1.0.0
|
|
427
|
+
*/
|
|
428
|
+
setupAutoRefresh(): void {
|
|
429
|
+
this.http.addResponseInterceptor(
|
|
430
|
+
(response: AxiosResponse) => response,
|
|
431
|
+
async (error: any) => {
|
|
432
|
+
// El config puede venir del error original de Axios o del BaserowError transformado
|
|
433
|
+
const originalRequest = error.config
|
|
434
|
+
|
|
435
|
+
// NO hacer auto-refresh en endpoints de autenticación (previene loops infinitos)
|
|
436
|
+
const isAuthEndpoint = originalRequest?.url?.includes('/user/token-')
|
|
437
|
+
|
|
438
|
+
// Detectar 401 de cualquier fuente (error raw de Axios o BaserowError transformado)
|
|
439
|
+
const is401Error = error.response?.status === 401 || (error instanceof BaserowError && error.status === 401)
|
|
440
|
+
|
|
441
|
+
// Si recibimos 401 y tenemos refresh token, intentar renovar
|
|
442
|
+
// EXCEPTO en endpoints de autenticación (login, refresh)
|
|
443
|
+
if (is401Error && !isAuthEndpoint && this.refreshToken && originalRequest && !originalRequest._retry) {
|
|
444
|
+
originalRequest._retry = true
|
|
445
|
+
|
|
446
|
+
this.logger?.info?.('🔄 Auto-refresh: Detectado 401, intentando renovar token...')
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
const newToken = await this.refreshAccessToken()
|
|
450
|
+
|
|
451
|
+
this.logger?.info?.('✅ Auto-refresh: Token renovado, reintentando request original')
|
|
452
|
+
|
|
453
|
+
// Actualizar el header Authorization con el nuevo token
|
|
454
|
+
if (originalRequest.headers && newToken) {
|
|
455
|
+
originalRequest.headers.Authorization = `JWT ${newToken}`
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Reintentar la petición original con el nuevo token actualizado
|
|
459
|
+
return this.http.axios.request(originalRequest)
|
|
460
|
+
} catch {
|
|
461
|
+
// Si el refresh falla, limpiar tokens y propagar error original
|
|
462
|
+
this.logger?.error?.('❌ Auto-refresh: Falló la renovación del token')
|
|
463
|
+
this.clearTokens()
|
|
464
|
+
throw error
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
throw error
|
|
469
|
+
}
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
}
|