@nocios/crudify-ui 1.3.4 → 2.0.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/README.md +37 -39
- package/README_DEPTH.md +400 -216
- package/dist/index.d.mts +26 -26
- package/dist/index.d.ts +26 -26
- package/dist/index.js +1982 -1982
- package/dist/index.mjs +1837 -1837
- package/package.json +1 -1
package/README_DEPTH.md
CHANGED
|
@@ -43,8 +43,8 @@ npm install @mui/material @mui/icons-material react react-dom
|
|
|
43
43
|
El `SessionProvider` es el punto de entrada principal para manejar la autenticación y sesiones en tu aplicación:
|
|
44
44
|
|
|
45
45
|
```tsx
|
|
46
|
-
import React from
|
|
47
|
-
import { SessionProvider } from
|
|
46
|
+
import React from "react";
|
|
47
|
+
import { SessionProvider } from "@nocios/crudify-ui";
|
|
48
48
|
|
|
49
49
|
function App() {
|
|
50
50
|
return (
|
|
@@ -52,13 +52,13 @@ function App() {
|
|
|
52
52
|
options={{
|
|
53
53
|
// Configuración automática basada en variables de entorno
|
|
54
54
|
// o configuración manual si es necesario
|
|
55
|
-
storageType:
|
|
55
|
+
storageType: "localStorage", // 'localStorage' | 'sessionStorage'
|
|
56
56
|
onSessionExpired: () => {
|
|
57
|
-
console.log(
|
|
57
|
+
console.log("Sesión expirada");
|
|
58
58
|
},
|
|
59
59
|
onSessionRestored: (tokens) => {
|
|
60
|
-
console.log(
|
|
61
|
-
}
|
|
60
|
+
console.log("Sesión restaurada:", tokens);
|
|
61
|
+
},
|
|
62
62
|
}}
|
|
63
63
|
>
|
|
64
64
|
<YourApp />
|
|
@@ -84,24 +84,17 @@ REACT_APP_CRUDIFY_ENV=dev # dev | stg | api | prod
|
|
|
84
84
|
Hook principal para manejo completo de autenticación:
|
|
85
85
|
|
|
86
86
|
```tsx
|
|
87
|
-
import { useAuth } from
|
|
87
|
+
import { useAuth } from "@nocios/crudify-ui";
|
|
88
88
|
|
|
89
89
|
function LoginPage() {
|
|
90
|
-
const {
|
|
91
|
-
login,
|
|
92
|
-
logout,
|
|
93
|
-
isAuthenticated,
|
|
94
|
-
isLoading,
|
|
95
|
-
error,
|
|
96
|
-
clearError
|
|
97
|
-
} = useAuth();
|
|
90
|
+
const { login, logout, isAuthenticated, isLoading, error, clearError } = useAuth();
|
|
98
91
|
|
|
99
92
|
const handleLogin = async () => {
|
|
100
93
|
try {
|
|
101
|
-
const result = await login(
|
|
102
|
-
console.log(
|
|
94
|
+
const result = await login("user@example.com", "password");
|
|
95
|
+
console.log("Login exitoso:", result);
|
|
103
96
|
} catch (error) {
|
|
104
|
-
console.error(
|
|
97
|
+
console.error("Error de login:", error);
|
|
105
98
|
}
|
|
106
99
|
};
|
|
107
100
|
|
|
@@ -115,7 +108,7 @@ function LoginPage() {
|
|
|
115
108
|
<button onClick={handleLogout}>Cerrar Sesión</button>
|
|
116
109
|
) : (
|
|
117
110
|
<button onClick={handleLogin} disabled={isLoading}>
|
|
118
|
-
{isLoading ?
|
|
111
|
+
{isLoading ? "Iniciando..." : "Iniciar Sesión"}
|
|
119
112
|
</button>
|
|
120
113
|
)}
|
|
121
114
|
{error && <div>Error: {error}</div>}
|
|
@@ -125,6 +118,7 @@ function LoginPage() {
|
|
|
125
118
|
```
|
|
126
119
|
|
|
127
120
|
**Características del useAuth:**
|
|
121
|
+
|
|
128
122
|
- ✅ Login/logout automático con refresh tokens
|
|
129
123
|
- ✅ Estado de autenticación en tiempo real
|
|
130
124
|
- ✅ Manejo de errores integrado
|
|
@@ -136,19 +130,13 @@ function LoginPage() {
|
|
|
136
130
|
Hook para obtener y manejar los datos completos del usuario:
|
|
137
131
|
|
|
138
132
|
```tsx
|
|
139
|
-
import { useUserData } from
|
|
133
|
+
import { useUserData } from "@nocios/crudify-ui";
|
|
140
134
|
|
|
141
135
|
function UserProfile() {
|
|
142
|
-
const {
|
|
143
|
-
userData,
|
|
144
|
-
sessionData,
|
|
145
|
-
isLoading,
|
|
146
|
-
error,
|
|
147
|
-
refetch
|
|
148
|
-
} = useUserData({
|
|
136
|
+
const { userData, sessionData, isLoading, error, refetch } = useUserData({
|
|
149
137
|
autoFetch: true, // Fetch automático al montar
|
|
150
138
|
retryOnError: true, // Retry automático en errores
|
|
151
|
-
cacheTime: 300000 // Cache por 5 minutos
|
|
139
|
+
cacheTime: 300000, // Cache por 5 minutos
|
|
152
140
|
});
|
|
153
141
|
|
|
154
142
|
if (isLoading) return <div>Cargando usuario...</div>;
|
|
@@ -164,18 +152,19 @@ function UserProfile() {
|
|
|
164
152
|
|
|
165
153
|
{/* Datos de la base de datos (userData) */}
|
|
166
154
|
<p>Nombre completo: {userData?.fullName}</p>
|
|
167
|
-
<p>
|
|
155
|
+
<p>
|
|
156
|
+
Avatar: <img src={userData?.avatar} alt="Avatar" />
|
|
157
|
+
</p>
|
|
168
158
|
<p>Último login: {userData?.lastLogin}</p>
|
|
169
159
|
|
|
170
|
-
<button onClick={() => refetch()}>
|
|
171
|
-
Actualizar Datos
|
|
172
|
-
</button>
|
|
160
|
+
<button onClick={() => refetch()}>Actualizar Datos</button>
|
|
173
161
|
</div>
|
|
174
162
|
);
|
|
175
163
|
}
|
|
176
164
|
```
|
|
177
165
|
|
|
178
166
|
**Características del useUserData:**
|
|
167
|
+
|
|
179
168
|
- ✅ Combina datos del JWT (sessionData) y de la BD (userData)
|
|
180
169
|
- ✅ Auto-fetch con deduplicación inteligente
|
|
181
170
|
- ✅ Retry automático en errores de red
|
|
@@ -187,54 +176,54 @@ function UserProfile() {
|
|
|
187
176
|
Hook para realizar operaciones CRUD type-safe:
|
|
188
177
|
|
|
189
178
|
```tsx
|
|
190
|
-
import { useData } from
|
|
179
|
+
import { useData } from "@nocios/crudify-ui";
|
|
191
180
|
|
|
192
181
|
function ProductManager() {
|
|
193
182
|
const { create, read, update, remove, isInitialized } = useData();
|
|
194
183
|
|
|
195
184
|
const createProduct = async () => {
|
|
196
185
|
try {
|
|
197
|
-
const result = await create(
|
|
198
|
-
name:
|
|
186
|
+
const result = await create("products", {
|
|
187
|
+
name: "Nuevo Producto",
|
|
199
188
|
price: 99.99,
|
|
200
|
-
category:
|
|
189
|
+
category: "electronics",
|
|
201
190
|
});
|
|
202
|
-
console.log(
|
|
191
|
+
console.log("Producto creado:", result);
|
|
203
192
|
} catch (error) {
|
|
204
|
-
console.error(
|
|
193
|
+
console.error("Error creando producto:", error);
|
|
205
194
|
}
|
|
206
195
|
};
|
|
207
196
|
|
|
208
197
|
const getProducts = async () => {
|
|
209
198
|
try {
|
|
210
|
-
const products = await read(
|
|
211
|
-
filters: { category:
|
|
199
|
+
const products = await read("products", {
|
|
200
|
+
filters: { category: "electronics" },
|
|
212
201
|
limit: 10,
|
|
213
|
-
offset: 0
|
|
202
|
+
offset: 0,
|
|
214
203
|
});
|
|
215
|
-
console.log(
|
|
204
|
+
console.log("Productos:", products);
|
|
216
205
|
} catch (error) {
|
|
217
|
-
console.error(
|
|
206
|
+
console.error("Error obteniendo productos:", error);
|
|
218
207
|
}
|
|
219
208
|
};
|
|
220
209
|
|
|
221
210
|
const updateProduct = async (productId: string) => {
|
|
222
211
|
try {
|
|
223
|
-
const result = await update(
|
|
224
|
-
price: 89.99
|
|
212
|
+
const result = await update("products", productId, {
|
|
213
|
+
price: 89.99,
|
|
225
214
|
});
|
|
226
|
-
console.log(
|
|
215
|
+
console.log("Producto actualizado:", result);
|
|
227
216
|
} catch (error) {
|
|
228
|
-
console.error(
|
|
217
|
+
console.error("Error actualizando producto:", error);
|
|
229
218
|
}
|
|
230
219
|
};
|
|
231
220
|
|
|
232
221
|
const deleteProduct = async (productId: string) => {
|
|
233
222
|
try {
|
|
234
|
-
await remove(
|
|
235
|
-
console.log(
|
|
223
|
+
await remove("products", productId);
|
|
224
|
+
console.log("Producto eliminado");
|
|
236
225
|
} catch (error) {
|
|
237
|
-
console.error(
|
|
226
|
+
console.error("Error eliminando producto:", error);
|
|
238
227
|
}
|
|
239
228
|
};
|
|
240
229
|
|
|
@@ -244,14 +233,15 @@ function ProductManager() {
|
|
|
244
233
|
<div>
|
|
245
234
|
<button onClick={createProduct}>Crear Producto</button>
|
|
246
235
|
<button onClick={getProducts}>Obtener Productos</button>
|
|
247
|
-
<button onClick={() => updateProduct(
|
|
248
|
-
<button onClick={() => deleteProduct(
|
|
236
|
+
<button onClick={() => updateProduct("123")}>Actualizar</button>
|
|
237
|
+
<button onClick={() => deleteProduct("123")}>Eliminar</button>
|
|
249
238
|
</div>
|
|
250
239
|
);
|
|
251
240
|
}
|
|
252
241
|
```
|
|
253
242
|
|
|
254
243
|
**Características del useData:**
|
|
244
|
+
|
|
255
245
|
- ✅ Operaciones CRUD type-safe
|
|
256
246
|
- ✅ Verificación automática de inicialización
|
|
257
247
|
- ✅ Soporte para transacciones
|
|
@@ -262,38 +252,24 @@ function ProductManager() {
|
|
|
262
252
|
Hook de bajo nivel para control directo de sesiones:
|
|
263
253
|
|
|
264
254
|
```tsx
|
|
265
|
-
import { useSession } from
|
|
255
|
+
import { useSession } from "@nocios/crudify-ui";
|
|
266
256
|
|
|
267
257
|
function SessionManager() {
|
|
268
|
-
const {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
tokens,
|
|
272
|
-
error,
|
|
273
|
-
login,
|
|
274
|
-
logout,
|
|
275
|
-
refreshTokens,
|
|
276
|
-
isExpiringSoon,
|
|
277
|
-
expiresIn
|
|
278
|
-
} = useSession({
|
|
279
|
-
onSessionExpired: () => console.log('Sesión expirada'),
|
|
280
|
-
onSessionRestored: (tokens) => console.log('Sesión restaurada', tokens),
|
|
281
|
-
onTokensRefreshed: (tokens) => console.log('Tokens renovados', tokens)
|
|
258
|
+
const { isAuthenticated, isLoading, tokens, error, login, logout, refreshTokens, isExpiringSoon, expiresIn } = useSession({
|
|
259
|
+
onSessionExpired: () => console.log("Sesión expirada"),
|
|
260
|
+
onSessionRestored: (tokens) => console.log("Sesión restaurada", tokens),
|
|
261
|
+
onTokensRefreshed: (tokens) => console.log("Tokens renovados", tokens),
|
|
282
262
|
});
|
|
283
263
|
|
|
284
264
|
return (
|
|
285
265
|
<div>
|
|
286
266
|
<h3>Estado de Sesión</h3>
|
|
287
|
-
<p>Autenticado: {isAuthenticated ?
|
|
288
|
-
<p>Cargando: {isLoading ?
|
|
267
|
+
<p>Autenticado: {isAuthenticated ? "Sí" : "No"}</p>
|
|
268
|
+
<p>Cargando: {isLoading ? "Sí" : "No"}</p>
|
|
289
269
|
<p>Expira en: {Math.floor(expiresIn / 60)} minutos</p>
|
|
290
|
-
<p>Expira pronto: {isExpiringSoon ?
|
|
270
|
+
<p>Expira pronto: {isExpiringSoon ? "Sí" : "No"}</p>
|
|
291
271
|
|
|
292
|
-
{isExpiringSoon && (
|
|
293
|
-
<button onClick={() => refreshTokens()}>
|
|
294
|
-
Renovar Tokens
|
|
295
|
-
</button>
|
|
296
|
-
)}
|
|
272
|
+
{isExpiringSoon && <button onClick={() => refreshTokens()}>Renovar Tokens</button>}
|
|
297
273
|
|
|
298
274
|
{tokens && (
|
|
299
275
|
<div>
|
|
@@ -311,15 +287,10 @@ function SessionManager() {
|
|
|
311
287
|
Hook para acceder al contexto global de sesión:
|
|
312
288
|
|
|
313
289
|
```tsx
|
|
314
|
-
import { useSessionContext } from
|
|
290
|
+
import { useSessionContext } from "@nocios/crudify-ui";
|
|
315
291
|
|
|
316
292
|
function NavigationBar() {
|
|
317
|
-
const {
|
|
318
|
-
isAuthenticated,
|
|
319
|
-
sessionData,
|
|
320
|
-
logout,
|
|
321
|
-
getTokenInfo
|
|
322
|
-
} = useSessionContext();
|
|
293
|
+
const { isAuthenticated, sessionData, logout, getTokenInfo } = useSessionContext();
|
|
323
294
|
|
|
324
295
|
if (!isAuthenticated) return null;
|
|
325
296
|
|
|
@@ -327,9 +298,7 @@ function NavigationBar() {
|
|
|
327
298
|
<nav>
|
|
328
299
|
<span>Bienvenido, {sessionData?.email}</span>
|
|
329
300
|
<button onClick={() => logout()}>Cerrar Sesión</button>
|
|
330
|
-
<button onClick={() => console.log(getTokenInfo())}>
|
|
331
|
-
Ver Token Info
|
|
332
|
-
</button>
|
|
301
|
+
<button onClick={() => console.log(getTokenInfo())}>Ver Token Info</button>
|
|
333
302
|
</nav>
|
|
334
303
|
);
|
|
335
304
|
}
|
|
@@ -342,8 +311,8 @@ function NavigationBar() {
|
|
|
342
311
|
Componente completo de autenticación con múltiples pantallas:
|
|
343
312
|
|
|
344
313
|
```tsx
|
|
345
|
-
import { CrudifyLogin } from
|
|
346
|
-
import type { CrudifyLoginConfig } from
|
|
314
|
+
import { CrudifyLogin } from "@nocios/crudify-ui";
|
|
315
|
+
import type { CrudifyLoginConfig } from "@nocios/crudify-ui";
|
|
347
316
|
|
|
348
317
|
function AuthPage() {
|
|
349
318
|
const config: CrudifyLoginConfig = {
|
|
@@ -351,17 +320,17 @@ function AuthPage() {
|
|
|
351
320
|
logo: "/logo.png",
|
|
352
321
|
colors: {
|
|
353
322
|
primaryColor: "#1066BA",
|
|
354
|
-
bgColor: "#f5f5f5"
|
|
355
|
-
}
|
|
323
|
+
bgColor: "#f5f5f5",
|
|
324
|
+
},
|
|
356
325
|
};
|
|
357
326
|
|
|
358
327
|
const handleLoginSuccess = (userData, redirectUrl) => {
|
|
359
|
-
console.log(
|
|
328
|
+
console.log("Login exitoso:", userData);
|
|
360
329
|
// Redireccionar o actualizar estado
|
|
361
330
|
};
|
|
362
331
|
|
|
363
332
|
const handleError = (error: string) => {
|
|
364
|
-
console.error(
|
|
333
|
+
console.error("Error de autenticación:", error);
|
|
365
334
|
};
|
|
366
335
|
|
|
367
336
|
return (
|
|
@@ -379,12 +348,14 @@ function AuthPage() {
|
|
|
379
348
|
```
|
|
380
349
|
|
|
381
350
|
**Pantallas incluidas en CrudifyLogin:**
|
|
351
|
+
|
|
382
352
|
- 🔐 **Login**: Formulario principal de autenticación
|
|
383
353
|
- 🔑 **Forgot Password**: Solicitar recuperación de contraseña
|
|
384
354
|
- 📧 **Check Code**: Verificar código de recuperación
|
|
385
355
|
- 🔄 **Reset Password**: Establecer nueva contraseña
|
|
386
356
|
|
|
387
357
|
**Configuración disponible:**
|
|
358
|
+
|
|
388
359
|
- ✅ Logo y nombre de aplicación personalizable
|
|
389
360
|
- ✅ Colores y temas personalizables
|
|
390
361
|
- ✅ Traducciones completas (ES/EN)
|
|
@@ -396,7 +367,7 @@ function AuthPage() {
|
|
|
396
367
|
Componente para mostrar información del perfil del usuario:
|
|
397
368
|
|
|
398
369
|
```tsx
|
|
399
|
-
import { UserProfileDisplay } from
|
|
370
|
+
import { UserProfileDisplay } from "@nocios/crudify-ui";
|
|
400
371
|
|
|
401
372
|
function ProfilePage() {
|
|
402
373
|
return (
|
|
@@ -413,7 +384,7 @@ function ProfilePage() {
|
|
|
413
384
|
Componente para proteger rutas que requieren autenticación:
|
|
414
385
|
|
|
415
386
|
```tsx
|
|
416
|
-
import { ProtectedRoute } from
|
|
387
|
+
import { ProtectedRoute } from "@nocios/crudify-ui";
|
|
417
388
|
|
|
418
389
|
function App() {
|
|
419
390
|
return (
|
|
@@ -439,7 +410,7 @@ function App() {
|
|
|
439
410
|
Componente útil para desarrollo y debug:
|
|
440
411
|
|
|
441
412
|
```tsx
|
|
442
|
-
import { SessionDebugInfo } from
|
|
413
|
+
import { SessionDebugInfo } from "@nocios/crudify-ui";
|
|
443
414
|
|
|
444
415
|
function DevPage() {
|
|
445
416
|
return (
|
|
@@ -451,6 +422,223 @@ function DevPage() {
|
|
|
451
422
|
}
|
|
452
423
|
```
|
|
453
424
|
|
|
425
|
+
### Policies - Gestión de Políticas Públicas
|
|
426
|
+
|
|
427
|
+
Componente completo para configurar políticas de acceso a datos públicos:
|
|
428
|
+
|
|
429
|
+
```tsx
|
|
430
|
+
import { Policies, POLICY_ACTIONS, PREFERRED_POLICY_ORDER, PolicyAction } from "@nocios/crudify-ui";
|
|
431
|
+
import { useState } from "react";
|
|
432
|
+
|
|
433
|
+
function ModuleConfiguration() {
|
|
434
|
+
const [policies, setPolicies] = useState([
|
|
435
|
+
{
|
|
436
|
+
id: "1",
|
|
437
|
+
action: "read",
|
|
438
|
+
fields: {
|
|
439
|
+
allow: ["name", "email"],
|
|
440
|
+
owner_allow: ["phone"],
|
|
441
|
+
deny: ["password", "internal_notes"],
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
id: "2",
|
|
446
|
+
action: "create",
|
|
447
|
+
fields: {
|
|
448
|
+
allow: ["name", "email"],
|
|
449
|
+
owner_allow: [],
|
|
450
|
+
deny: ["status", "verified"],
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
]);
|
|
454
|
+
|
|
455
|
+
const availableFields = ["name", "email", "phone", "address", "status", "verified", "password", "internal_notes"];
|
|
456
|
+
|
|
457
|
+
const handlePolicyChange = (newPolicies) => {
|
|
458
|
+
setPolicies(newPolicies);
|
|
459
|
+
// Guardar en base de datos o estado global
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
return (
|
|
463
|
+
<div>
|
|
464
|
+
<h2>Configuración de Políticas Públicas</h2>
|
|
465
|
+
|
|
466
|
+
<Policies
|
|
467
|
+
policies={policies}
|
|
468
|
+
onChange={handlePolicyChange}
|
|
469
|
+
availableFields={availableFields}
|
|
470
|
+
errors={null} // O errores de validación del servidor
|
|
471
|
+
isSubmitting={false}
|
|
472
|
+
/>
|
|
473
|
+
|
|
474
|
+
{/* Mostrar acciones disponibles */}
|
|
475
|
+
<div style={{ marginTop: "20px" }}>
|
|
476
|
+
<h3>Acciones Disponibles:</h3>
|
|
477
|
+
<ul>
|
|
478
|
+
{POLICY_ACTIONS.map((action) => (
|
|
479
|
+
<li key={action}>{action}</li>
|
|
480
|
+
))}
|
|
481
|
+
</ul>
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
#### Propiedades del Componente Policies
|
|
489
|
+
|
|
490
|
+
| Prop | Tipo | Requerido | Descripción |
|
|
491
|
+
| ----------------- | ------------------------------ | --------- | --------------------------------------- |
|
|
492
|
+
| `policies` | `Policy[]` | ✅ | Array de políticas configuradas |
|
|
493
|
+
| `onChange` | `(policies: Policy[]) => void` | ✅ | Callback cuando cambian las políticas |
|
|
494
|
+
| `availableFields` | `string[]` | ✅ | Campos disponibles para configurar |
|
|
495
|
+
| `errors` | `string \| object` | ❌ | Errores de validación |
|
|
496
|
+
| `isSubmitting` | `boolean` | ❌ | Estado de envío (deshabilita controles) |
|
|
497
|
+
|
|
498
|
+
#### Estructura de una Policy
|
|
499
|
+
|
|
500
|
+
```tsx
|
|
501
|
+
type Policy = {
|
|
502
|
+
id: string; // ID único de la política
|
|
503
|
+
action: PolicyAction; // 'create' | 'read' | 'update' | 'delete'
|
|
504
|
+
|
|
505
|
+
// Para acciones create, read, update:
|
|
506
|
+
fields?: {
|
|
507
|
+
allow: string[]; // Campos permitidos para todos
|
|
508
|
+
owner_allow: string[]; // Campos adicionales para el propietario
|
|
509
|
+
deny: string[]; // Campos explícitamente denegados
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
// Para acción delete:
|
|
513
|
+
permission?: string; // '*' | 'deny' | 'owner'
|
|
514
|
+
};
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
#### Constantes Disponibles
|
|
518
|
+
|
|
519
|
+
```tsx
|
|
520
|
+
// Acciones de política disponibles
|
|
521
|
+
export const POLICY_ACTIONS = ["create", "read", "update", "delete"] as const;
|
|
522
|
+
export type PolicyAction = (typeof POLICY_ACTIONS)[number];
|
|
523
|
+
|
|
524
|
+
// Orden preferido para mostrar las políticas
|
|
525
|
+
export const PREFERRED_POLICY_ORDER: PolicyAction[] = ["create", "read", "update", "delete"];
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
#### Casos de Uso Comunes
|
|
529
|
+
|
|
530
|
+
**1. Configuración de API Pública**
|
|
531
|
+
|
|
532
|
+
```tsx
|
|
533
|
+
// Permitir lectura pública de ciertos campos
|
|
534
|
+
const publicReadPolicy = {
|
|
535
|
+
id: "public-read",
|
|
536
|
+
action: "read",
|
|
537
|
+
fields: {
|
|
538
|
+
allow: ["name", "description", "published_at"],
|
|
539
|
+
owner_allow: ["views", "stats"],
|
|
540
|
+
deny: ["internal_notes", "admin_flags"],
|
|
541
|
+
},
|
|
542
|
+
};
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
**2. Restricciones de Creación**
|
|
546
|
+
|
|
547
|
+
```tsx
|
|
548
|
+
// Permitir creación pero restringir ciertos campos
|
|
549
|
+
const createPolicy = {
|
|
550
|
+
id: "public-create",
|
|
551
|
+
action: "create",
|
|
552
|
+
fields: {
|
|
553
|
+
allow: ["title", "content", "category"],
|
|
554
|
+
owner_allow: [],
|
|
555
|
+
deny: ["status", "featured", "admin_verified"],
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
**3. Permisos de Eliminación**
|
|
561
|
+
|
|
562
|
+
```tsx
|
|
563
|
+
// Solo el propietario puede eliminar
|
|
564
|
+
const deletePolicy = {
|
|
565
|
+
id: "owner-delete",
|
|
566
|
+
action: "delete",
|
|
567
|
+
permission: "owner", // '*' = todos, 'deny' = nadie, 'owner' = solo propietario
|
|
568
|
+
};
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
#### Manejo de Errores
|
|
572
|
+
|
|
573
|
+
```tsx
|
|
574
|
+
function PolicyManager() {
|
|
575
|
+
const [errors, setErrors] = useState(null);
|
|
576
|
+
|
|
577
|
+
const handleSubmit = async (policies) => {
|
|
578
|
+
try {
|
|
579
|
+
await saveModulePolicies(moduleId, policies);
|
|
580
|
+
setErrors(null);
|
|
581
|
+
} catch (error) {
|
|
582
|
+
// Error de campo específico
|
|
583
|
+
if (error.fieldErrors) {
|
|
584
|
+
setErrors({
|
|
585
|
+
_error: "Hay errores en la configuración",
|
|
586
|
+
...error.fieldErrors,
|
|
587
|
+
});
|
|
588
|
+
} else {
|
|
589
|
+
// Error general
|
|
590
|
+
setErrors("Error al guardar las políticas");
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
return (
|
|
596
|
+
<Policies
|
|
597
|
+
policies={policies}
|
|
598
|
+
onChange={setPolicies}
|
|
599
|
+
availableFields={fields}
|
|
600
|
+
errors={errors} // Se mostrará como Alert en el componente
|
|
601
|
+
isSubmitting={isLoading}
|
|
602
|
+
/>
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
#### Integración con i18next
|
|
608
|
+
|
|
609
|
+
El componente usa react-i18next para internacionalización. Las claves de traducción incluidas:
|
|
610
|
+
|
|
611
|
+
```json
|
|
612
|
+
{
|
|
613
|
+
"modules": {
|
|
614
|
+
"form": {
|
|
615
|
+
"publicPolicies": {
|
|
616
|
+
"title": "Políticas Públicas",
|
|
617
|
+
"description": "Configura qué datos pueden acceder los usuarios públicos",
|
|
618
|
+
"noPolicies": "No hay políticas configuradas. Agrega una política para comenzar.",
|
|
619
|
+
"addPolicy": "Agregar Política",
|
|
620
|
+
"actions": {
|
|
621
|
+
"create": "Crear",
|
|
622
|
+
"read": "Leer",
|
|
623
|
+
"update": "Actualizar",
|
|
624
|
+
"delete": "Eliminar"
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
#### Características del Componente
|
|
633
|
+
|
|
634
|
+
- ✅ **Interfaz Intuitiva**: UI clara para gestionar permisos complejos
|
|
635
|
+
- ✅ **Validación en Tiempo Real**: Errores mostrados inmediatamente
|
|
636
|
+
- ✅ **Scroll Automático**: Nuevo ítem automáticamente visible
|
|
637
|
+
- ✅ **Prevención de Duplicados**: No permite acciones duplicadas
|
|
638
|
+
- ✅ **Orden Lógico**: Políticas ordenadas según PREFERRED_POLICY_ORDER
|
|
639
|
+
- ✅ **Responsive**: Adaptable a diferentes tamaños de pantalla
|
|
640
|
+
- ✅ **Internacionalización**: Soporte completo para múltiples idiomas
|
|
641
|
+
|
|
454
642
|
## 🛠️ Utilidades
|
|
455
643
|
|
|
456
644
|
### JWT Utils
|
|
@@ -458,23 +646,19 @@ function DevPage() {
|
|
|
458
646
|
Utilidades para trabajar con tokens JWT:
|
|
459
647
|
|
|
460
648
|
```tsx
|
|
461
|
-
import {
|
|
462
|
-
decodeJwtSafely,
|
|
463
|
-
getCurrentUserEmail,
|
|
464
|
-
isTokenExpired
|
|
465
|
-
} from '@nocios/crudify-ui';
|
|
649
|
+
import { decodeJwtSafely, getCurrentUserEmail, isTokenExpired } from "@nocios/crudify-ui";
|
|
466
650
|
|
|
467
651
|
// Decodificar JWT de forma segura
|
|
468
652
|
const payload = decodeJwtSafely(token);
|
|
469
|
-
console.log(
|
|
653
|
+
console.log("Usuario ID:", payload?.sub);
|
|
470
654
|
|
|
471
655
|
// Obtener email del usuario actual
|
|
472
656
|
const email = getCurrentUserEmail();
|
|
473
|
-
console.log(
|
|
657
|
+
console.log("Email:", email);
|
|
474
658
|
|
|
475
659
|
// Verificar si un token está expirado
|
|
476
660
|
const expired = isTokenExpired(token);
|
|
477
|
-
console.log(
|
|
661
|
+
console.log("Token expirado:", expired);
|
|
478
662
|
```
|
|
479
663
|
|
|
480
664
|
### Token Storage
|
|
@@ -482,35 +666,35 @@ console.log('Token expirado:', expired);
|
|
|
482
666
|
Sistema de almacenamiento seguro para tokens:
|
|
483
667
|
|
|
484
668
|
```tsx
|
|
485
|
-
import { TokenStorage } from
|
|
486
|
-
import type { TokenData, StorageType } from
|
|
669
|
+
import { TokenStorage } from "@nocios/crudify-ui";
|
|
670
|
+
import type { TokenData, StorageType } from "@nocios/crudify-ui";
|
|
487
671
|
|
|
488
672
|
// Crear instancia de storage
|
|
489
673
|
const storage = new TokenStorage({
|
|
490
|
-
type:
|
|
491
|
-
encryptionKey:
|
|
674
|
+
type: "localStorage", // 'localStorage' | 'sessionStorage'
|
|
675
|
+
encryptionKey: "mi-clave-secreta",
|
|
492
676
|
});
|
|
493
677
|
|
|
494
678
|
// Guardar tokens
|
|
495
679
|
const tokens: TokenData = {
|
|
496
|
-
accessToken:
|
|
497
|
-
refreshToken:
|
|
680
|
+
accessToken: "access_token_here",
|
|
681
|
+
refreshToken: "refresh_token_here",
|
|
498
682
|
expiresIn: 3600,
|
|
499
|
-
refreshExpiresIn: 86400
|
|
683
|
+
refreshExpiresIn: 86400,
|
|
500
684
|
};
|
|
501
685
|
|
|
502
686
|
await storage.setTokens(tokens);
|
|
503
687
|
|
|
504
688
|
// Obtener tokens
|
|
505
689
|
const storedTokens = await storage.getTokens();
|
|
506
|
-
console.log(
|
|
690
|
+
console.log("Tokens almacenados:", storedTokens);
|
|
507
691
|
|
|
508
692
|
// Limpiar tokens
|
|
509
693
|
await storage.clearTokens();
|
|
510
694
|
|
|
511
695
|
// Verificar validez
|
|
512
696
|
const isValid = await storage.isValid();
|
|
513
|
-
console.log(
|
|
697
|
+
console.log("Tokens válidos:", isValid);
|
|
514
698
|
```
|
|
515
699
|
|
|
516
700
|
### Error Handler
|
|
@@ -518,14 +702,8 @@ console.log('Tokens válidos:', isValid);
|
|
|
518
702
|
Sistema completo de manejo de errores:
|
|
519
703
|
|
|
520
704
|
```tsx
|
|
521
|
-
import {
|
|
522
|
-
|
|
523
|
-
parseApiError,
|
|
524
|
-
parseTransactionError,
|
|
525
|
-
getErrorMessage,
|
|
526
|
-
ERROR_CODES
|
|
527
|
-
} from '@nocios/crudify-ui';
|
|
528
|
-
import type { ParsedError, ErrorCode } from '@nocios/crudify-ui';
|
|
705
|
+
import { handleCrudifyError, parseApiError, parseTransactionError, getErrorMessage, ERROR_CODES } from "@nocios/crudify-ui";
|
|
706
|
+
import type { ParsedError, ErrorCode } from "@nocios/crudify-ui";
|
|
529
707
|
|
|
530
708
|
// Manejo universal de errores
|
|
531
709
|
try {
|
|
@@ -533,10 +711,10 @@ try {
|
|
|
533
711
|
} catch (error) {
|
|
534
712
|
const parsedError: ParsedError = handleCrudifyError(error);
|
|
535
713
|
|
|
536
|
-
console.log(
|
|
537
|
-
console.log(
|
|
538
|
-
console.log(
|
|
539
|
-
console.log(
|
|
714
|
+
console.log("Tipo:", parsedError.type);
|
|
715
|
+
console.log("Código:", parsedError.code);
|
|
716
|
+
console.log("Mensaje:", parsedError.message);
|
|
717
|
+
console.log("Severidad:", parsedError.severity);
|
|
540
718
|
|
|
541
719
|
// Mostrar mensaje apropiado al usuario
|
|
542
720
|
const userMessage = getErrorMessage(parsedError.code as ErrorCode);
|
|
@@ -550,7 +728,7 @@ const apiError = parseApiError(response);
|
|
|
550
728
|
const transactionError = parseTransactionError(response);
|
|
551
729
|
|
|
552
730
|
// Códigos de error disponibles
|
|
553
|
-
console.log(
|
|
731
|
+
console.log("Códigos disponibles:", ERROR_CODES);
|
|
554
732
|
```
|
|
555
733
|
|
|
556
734
|
## 🔄 API de Crudify Browser
|
|
@@ -558,15 +736,16 @@ console.log('Códigos disponibles:', ERROR_CODES);
|
|
|
558
736
|
Esta librería re-exporta completamente `@nocios/crudify-browser`, proporcionando una API unificada:
|
|
559
737
|
|
|
560
738
|
```tsx
|
|
561
|
-
import { crudify } from
|
|
739
|
+
import { crudify } from "@nocios/crudify-ui";
|
|
562
740
|
// Equivale a: import crudify from '@nocios/crudify-browser';
|
|
563
741
|
|
|
564
742
|
// Todas las funcionalidades de crudify-browser están disponibles
|
|
565
|
-
const result = await crudify.create(
|
|
566
|
-
const users = await crudify.read(
|
|
743
|
+
const result = await crudify.create("users", userData);
|
|
744
|
+
const users = await crudify.read("users");
|
|
567
745
|
```
|
|
568
746
|
|
|
569
747
|
Esto significa que puedes:
|
|
748
|
+
|
|
570
749
|
- ✅ Importar solo desde `@nocios/crudify-ui`
|
|
571
750
|
- ✅ Usar todas las funcionalidades de crudify-browser
|
|
572
751
|
- ✅ Tener una API unificada
|
|
@@ -598,13 +777,11 @@ import type {
|
|
|
598
777
|
// Tipos de utilidades
|
|
599
778
|
TokenData,
|
|
600
779
|
ParsedError,
|
|
601
|
-
ErrorCode
|
|
602
|
-
} from
|
|
780
|
+
ErrorCode,
|
|
781
|
+
} from "@nocios/crudify-ui";
|
|
603
782
|
|
|
604
783
|
// Ejemplo con tipos
|
|
605
|
-
const handleLogin = async (
|
|
606
|
-
credentials: LoginRequest
|
|
607
|
-
): Promise<CrudifyApiResponse<LoginResponse>> => {
|
|
784
|
+
const handleLogin = async (credentials: LoginRequest): Promise<CrudifyApiResponse<LoginResponse>> => {
|
|
608
785
|
// implementación con tipos seguros
|
|
609
786
|
};
|
|
610
787
|
```
|
|
@@ -671,43 +848,41 @@ import { CrudifyLogin } from '@nocios/crudify-ui';
|
|
|
671
848
|
### SessionProvider con opciones completas
|
|
672
849
|
|
|
673
850
|
```tsx
|
|
674
|
-
import { SessionProvider } from
|
|
675
|
-
import type { UseSessionOptions } from
|
|
851
|
+
import { SessionProvider } from "@nocios/crudify-ui";
|
|
852
|
+
import type { UseSessionOptions } from "@nocios/crudify-ui";
|
|
676
853
|
|
|
677
854
|
const sessionOptions: UseSessionOptions = {
|
|
678
855
|
// Tipo de almacenamiento
|
|
679
|
-
storageType:
|
|
856
|
+
storageType: "localStorage", // 'localStorage' | 'sessionStorage'
|
|
680
857
|
|
|
681
858
|
// Callbacks del ciclo de vida
|
|
682
859
|
onSessionExpired: () => {
|
|
683
|
-
console.log(
|
|
684
|
-
window.location.href =
|
|
860
|
+
console.log("Sesión expirada - redirigir a login");
|
|
861
|
+
window.location.href = "/login";
|
|
685
862
|
},
|
|
686
863
|
|
|
687
864
|
onSessionRestored: (tokens) => {
|
|
688
|
-
console.log(
|
|
865
|
+
console.log("Sesión restaurada exitosamente", tokens);
|
|
689
866
|
},
|
|
690
867
|
|
|
691
868
|
onTokensRefreshed: (tokens) => {
|
|
692
|
-
console.log(
|
|
869
|
+
console.log("Tokens renovados", tokens);
|
|
693
870
|
},
|
|
694
871
|
|
|
695
872
|
onLogout: () => {
|
|
696
|
-
console.log(
|
|
873
|
+
console.log("Usuario cerró sesión");
|
|
697
874
|
},
|
|
698
875
|
|
|
699
876
|
onError: (error) => {
|
|
700
|
-
console.error(
|
|
701
|
-
}
|
|
877
|
+
console.error("Error de sesión:", error);
|
|
878
|
+
},
|
|
702
879
|
};
|
|
703
880
|
|
|
704
881
|
function App() {
|
|
705
882
|
return (
|
|
706
883
|
<SessionProvider options={sessionOptions}>
|
|
707
884
|
<Router>
|
|
708
|
-
<Routes>
|
|
709
|
-
{/* tus rutas */}
|
|
710
|
-
</Routes>
|
|
885
|
+
<Routes>{/* tus rutas */}</Routes>
|
|
711
886
|
</Router>
|
|
712
887
|
</SessionProvider>
|
|
713
888
|
);
|
|
@@ -719,13 +894,13 @@ function App() {
|
|
|
719
894
|
Si necesitas configuración manual en lugar de variables de entorno:
|
|
720
895
|
|
|
721
896
|
```tsx
|
|
722
|
-
import { crudify } from
|
|
897
|
+
import { crudify } from "@nocios/crudify-ui";
|
|
723
898
|
|
|
724
899
|
// Configurar manualmente antes de usar
|
|
725
900
|
crudify.configure({
|
|
726
|
-
publicApiKey:
|
|
727
|
-
environment:
|
|
728
|
-
baseUrl:
|
|
901
|
+
publicApiKey: "tu-api-key",
|
|
902
|
+
environment: "production", // 'dev' | 'stg' | 'api' | 'prod'
|
|
903
|
+
baseUrl: "https://api.tudominio.com", // opcional
|
|
729
904
|
});
|
|
730
905
|
```
|
|
731
906
|
|
|
@@ -734,14 +909,9 @@ crudify.configure({
|
|
|
734
909
|
### Aplicación básica con autenticación
|
|
735
910
|
|
|
736
911
|
```tsx
|
|
737
|
-
import React from
|
|
738
|
-
import { BrowserRouter, Routes, Route, Navigate } from
|
|
739
|
-
import {
|
|
740
|
-
SessionProvider,
|
|
741
|
-
ProtectedRoute,
|
|
742
|
-
CrudifyLogin,
|
|
743
|
-
useSessionContext
|
|
744
|
-
} from '@nocios/crudify-ui';
|
|
912
|
+
import React from "react";
|
|
913
|
+
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
|
|
914
|
+
import { SessionProvider, ProtectedRoute, CrudifyLogin, useSessionContext } from "@nocios/crudify-ui";
|
|
745
915
|
|
|
746
916
|
// Componente Dashboard
|
|
747
917
|
function Dashboard() {
|
|
@@ -759,14 +929,14 @@ function Dashboard() {
|
|
|
759
929
|
// Componente Login
|
|
760
930
|
function LoginPage() {
|
|
761
931
|
return (
|
|
762
|
-
<div style={{ maxWidth:
|
|
932
|
+
<div style={{ maxWidth: "400px", margin: "0 auto", padding: "2rem" }}>
|
|
763
933
|
<CrudifyLogin
|
|
764
934
|
config={{
|
|
765
935
|
appName: "Mi App",
|
|
766
|
-
colors: { primaryColor: "#1976d2" }
|
|
936
|
+
colors: { primaryColor: "#1976d2" },
|
|
767
937
|
}}
|
|
768
938
|
onLoginSuccess={(userData) => {
|
|
769
|
-
console.log(
|
|
939
|
+
console.log("Login exitoso:", userData);
|
|
770
940
|
// La navegación se maneja automáticamente
|
|
771
941
|
}}
|
|
772
942
|
/>
|
|
@@ -802,8 +972,8 @@ export default App;
|
|
|
802
972
|
### Aplicación con operaciones CRUD
|
|
803
973
|
|
|
804
974
|
```tsx
|
|
805
|
-
import React, { useState, useEffect } from
|
|
806
|
-
import { useData, useUserData } from
|
|
975
|
+
import React, { useState, useEffect } from "react";
|
|
976
|
+
import { useData, useUserData } from "@nocios/crudify-ui";
|
|
807
977
|
|
|
808
978
|
function ProductManager() {
|
|
809
979
|
const [products, setProducts] = useState([]);
|
|
@@ -820,13 +990,13 @@ function ProductManager() {
|
|
|
820
990
|
const loadProducts = async () => {
|
|
821
991
|
setLoading(true);
|
|
822
992
|
try {
|
|
823
|
-
const result = await read(
|
|
993
|
+
const result = await read("products", {
|
|
824
994
|
filters: { userId: userData?.id },
|
|
825
|
-
limit: 20
|
|
995
|
+
limit: 20,
|
|
826
996
|
});
|
|
827
997
|
setProducts(result.data || []);
|
|
828
998
|
} catch (error) {
|
|
829
|
-
console.error(
|
|
999
|
+
console.error("Error cargando productos:", error);
|
|
830
1000
|
} finally {
|
|
831
1001
|
setLoading(false);
|
|
832
1002
|
}
|
|
@@ -834,31 +1004,31 @@ function ProductManager() {
|
|
|
834
1004
|
|
|
835
1005
|
const createProduct = async (productData) => {
|
|
836
1006
|
try {
|
|
837
|
-
await create(
|
|
1007
|
+
await create("products", {
|
|
838
1008
|
...productData,
|
|
839
|
-
userId: userData?.id
|
|
1009
|
+
userId: userData?.id,
|
|
840
1010
|
});
|
|
841
1011
|
await loadProducts(); // Recargar lista
|
|
842
1012
|
} catch (error) {
|
|
843
|
-
console.error(
|
|
1013
|
+
console.error("Error creando producto:", error);
|
|
844
1014
|
}
|
|
845
1015
|
};
|
|
846
1016
|
|
|
847
1017
|
const updateProduct = async (productId, updates) => {
|
|
848
1018
|
try {
|
|
849
|
-
await update(
|
|
1019
|
+
await update("products", productId, updates);
|
|
850
1020
|
await loadProducts(); // Recargar lista
|
|
851
1021
|
} catch (error) {
|
|
852
|
-
console.error(
|
|
1022
|
+
console.error("Error actualizando producto:", error);
|
|
853
1023
|
}
|
|
854
1024
|
};
|
|
855
1025
|
|
|
856
1026
|
const deleteProduct = async (productId) => {
|
|
857
1027
|
try {
|
|
858
|
-
await remove(
|
|
1028
|
+
await remove("products", productId);
|
|
859
1029
|
await loadProducts(); // Recargar lista
|
|
860
1030
|
} catch (error) {
|
|
861
|
-
console.error(
|
|
1031
|
+
console.error("Error eliminando producto:", error);
|
|
862
1032
|
}
|
|
863
1033
|
};
|
|
864
1034
|
|
|
@@ -868,25 +1038,33 @@ function ProductManager() {
|
|
|
868
1038
|
<div>
|
|
869
1039
|
<h2>Mis Productos</h2>
|
|
870
1040
|
|
|
871
|
-
<button
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1041
|
+
<button
|
|
1042
|
+
onClick={() =>
|
|
1043
|
+
createProduct({
|
|
1044
|
+
name: "Nuevo Producto",
|
|
1045
|
+
price: 99.99,
|
|
1046
|
+
})
|
|
1047
|
+
}
|
|
1048
|
+
>
|
|
875
1049
|
Crear Producto
|
|
876
1050
|
</button>
|
|
877
1051
|
|
|
878
1052
|
<ul>
|
|
879
|
-
{products.map(product => (
|
|
1053
|
+
{products.map((product) => (
|
|
880
1054
|
<li key={product.id}>
|
|
881
|
-
<span>
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1055
|
+
<span>
|
|
1056
|
+
{product.name} - ${product.price}
|
|
1057
|
+
</span>
|
|
1058
|
+
<button
|
|
1059
|
+
onClick={() =>
|
|
1060
|
+
updateProduct(product.id, {
|
|
1061
|
+
price: product.price * 1.1,
|
|
1062
|
+
})
|
|
1063
|
+
}
|
|
1064
|
+
>
|
|
885
1065
|
Aumentar Precio 10%
|
|
886
1066
|
</button>
|
|
887
|
-
<button onClick={() => deleteProduct(product.id)}>
|
|
888
|
-
Eliminar
|
|
889
|
-
</button>
|
|
1067
|
+
<button onClick={() => deleteProduct(product.id)}>Eliminar</button>
|
|
890
1068
|
</li>
|
|
891
1069
|
))}
|
|
892
1070
|
</ul>
|
|
@@ -911,10 +1089,12 @@ function ProductManager() {
|
|
|
911
1089
|
// 1. Usar SessionProvider en el nivel más alto
|
|
912
1090
|
function App() {
|
|
913
1091
|
return (
|
|
914
|
-
<SessionProvider
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1092
|
+
<SessionProvider
|
|
1093
|
+
options={{
|
|
1094
|
+
storageType: "localStorage", // Más persistente
|
|
1095
|
+
onSessionExpired: handleSessionExpired,
|
|
1096
|
+
}}
|
|
1097
|
+
>
|
|
918
1098
|
<YourApp />
|
|
919
1099
|
</SessionProvider>
|
|
920
1100
|
);
|
|
@@ -923,13 +1103,13 @@ function App() {
|
|
|
923
1103
|
// 2. Proteger rutas sensibles
|
|
924
1104
|
<ProtectedRoute fallback={<LoginRedirect />}>
|
|
925
1105
|
<SensitiveComponent />
|
|
926
|
-
</ProtectedRoute
|
|
1106
|
+
</ProtectedRoute>;
|
|
927
1107
|
|
|
928
1108
|
// 3. Manejar errores de autenticación
|
|
929
1109
|
const { error, clearError } = useAuth();
|
|
930
1110
|
useEffect(() => {
|
|
931
1111
|
if (error) {
|
|
932
|
-
console.error(
|
|
1112
|
+
console.error("Error de auth:", error);
|
|
933
1113
|
// Mostrar notificación al usuario
|
|
934
1114
|
clearError();
|
|
935
1115
|
}
|
|
@@ -941,8 +1121,8 @@ useEffect(() => {
|
|
|
941
1121
|
// Opcional: limpiar datos sensibles
|
|
942
1122
|
};
|
|
943
1123
|
|
|
944
|
-
window.addEventListener(
|
|
945
|
-
return () => window.removeEventListener(
|
|
1124
|
+
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
1125
|
+
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
946
1126
|
}, []);
|
|
947
1127
|
```
|
|
948
1128
|
|
|
@@ -951,6 +1131,7 @@ useEffect(() => {
|
|
|
951
1131
|
### Problemas Comunes
|
|
952
1132
|
|
|
953
1133
|
**1. Token expirado constantemente**
|
|
1134
|
+
|
|
954
1135
|
```tsx
|
|
955
1136
|
// Verificar configuración de refresh tokens
|
|
956
1137
|
const { isExpiringSoon, refreshTokens } = useSession();
|
|
@@ -963,6 +1144,7 @@ useEffect(() => {
|
|
|
963
1144
|
```
|
|
964
1145
|
|
|
965
1146
|
**2. Sesión no se restaura al recargar**
|
|
1147
|
+
|
|
966
1148
|
```tsx
|
|
967
1149
|
// Asegurar que SessionProvider esté en el nivel correcto
|
|
968
1150
|
// y verificar storageType
|
|
@@ -970,11 +1152,12 @@ useEffect(() => {
|
|
|
970
1152
|
```
|
|
971
1153
|
|
|
972
1154
|
**3. Errores de CORS**
|
|
1155
|
+
|
|
973
1156
|
```tsx
|
|
974
1157
|
// Verificar configuración de environment
|
|
975
1158
|
crudify.configure({
|
|
976
|
-
environment:
|
|
977
|
-
publicApiKey: process.env.REACT_APP_CRUDIFY_PUBLIC_API_KEY
|
|
1159
|
+
environment: "dev", // Asegurar environment correcto
|
|
1160
|
+
publicApiKey: process.env.REACT_APP_CRUDIFY_PUBLIC_API_KEY,
|
|
978
1161
|
});
|
|
979
1162
|
```
|
|
980
1163
|
|
|
@@ -982,11 +1165,11 @@ crudify.configure({
|
|
|
982
1165
|
|
|
983
1166
|
```tsx
|
|
984
1167
|
// Usar SessionDebugInfo para desarrollo
|
|
985
|
-
import { SessionDebugInfo } from
|
|
1168
|
+
import { SessionDebugInfo } from "@nocios/crudify-ui";
|
|
986
1169
|
|
|
987
1170
|
function DevPanel() {
|
|
988
|
-
return process.env.NODE_ENV ===
|
|
989
|
-
<div style={{ position:
|
|
1171
|
+
return process.env.NODE_ENV === "development" ? (
|
|
1172
|
+
<div style={{ position: "fixed", bottom: 0, right: 0, background: "white", padding: "1rem" }}>
|
|
990
1173
|
<SessionDebugInfo />
|
|
991
1174
|
</div>
|
|
992
1175
|
) : null;
|
|
@@ -997,34 +1180,35 @@ function DevPanel() {
|
|
|
997
1180
|
|
|
998
1181
|
### Hooks API Reference
|
|
999
1182
|
|
|
1000
|
-
| Hook
|
|
1001
|
-
|
|
1002
|
-
| `useAuth`
|
|
1003
|
-
| `useUserData`
|
|
1004
|
-
| `useData`
|
|
1005
|
-
| `useSession`
|
|
1006
|
-
| `useSessionContext` | Contexto global
|
|
1183
|
+
| Hook | Propósito | Returns |
|
|
1184
|
+
| ------------------- | ----------------------- | -------------------------------------------------------- |
|
|
1185
|
+
| `useAuth` | Autenticación principal | `{ login, logout, isAuthenticated, isLoading, error }` |
|
|
1186
|
+
| `useUserData` | Datos del usuario | `{ userData, sessionData, isLoading, error, refetch }` |
|
|
1187
|
+
| `useData` | Operaciones CRUD | `{ create, read, update, remove, isInitialized }` |
|
|
1188
|
+
| `useSession` | Control de sesión | `{ tokens, isAuthenticated, refreshTokens, expiresIn }` |
|
|
1189
|
+
| `useSessionContext` | Contexto global | `{ sessionData, isAuthenticated, logout, getTokenInfo }` |
|
|
1007
1190
|
|
|
1008
1191
|
### Componentes API Reference
|
|
1009
1192
|
|
|
1010
|
-
| Componente
|
|
1011
|
-
|
|
1012
|
-
| `SessionProvider`
|
|
1013
|
-
| `CrudifyLogin`
|
|
1014
|
-
| `ProtectedRoute`
|
|
1015
|
-
| `UserProfileDisplay` | `showAvatar, showDetails`
|
|
1193
|
+
| Componente | Props Principales | Propósito |
|
|
1194
|
+
| -------------------- | ---------------------------------- | ---------------------------- |
|
|
1195
|
+
| `SessionProvider` | `options: UseSessionOptions` | Provider global de sesión |
|
|
1196
|
+
| `CrudifyLogin` | `config, onLoginSuccess, language` | Componente de login completo |
|
|
1197
|
+
| `ProtectedRoute` | `fallback, children` | Protección de rutas |
|
|
1198
|
+
| `UserProfileDisplay` | `showAvatar, showDetails` | Display de perfil |
|
|
1016
1199
|
|
|
1017
1200
|
### Utilidades API Reference
|
|
1018
1201
|
|
|
1019
|
-
| Utilidad
|
|
1020
|
-
|
|
1021
|
-
| `JWT Utils`
|
|
1022
|
-
| `Token Storage` | `setTokens, getTokens, clearTokens, isValid`
|
|
1023
|
-
| `Error Handler` | `handleCrudifyError, parseApiError, getErrorMessage`
|
|
1202
|
+
| Utilidad | Funciones | Propósito |
|
|
1203
|
+
| --------------- | ------------------------------------------------------ | --------------------- |
|
|
1204
|
+
| `JWT Utils` | `decodeJwtSafely, getCurrentUserEmail, isTokenExpired` | Manejo de JWT |
|
|
1205
|
+
| `Token Storage` | `setTokens, getTokens, clearTokens, isValid` | Almacenamiento seguro |
|
|
1206
|
+
| `Error Handler` | `handleCrudifyError, parseApiError, getErrorMessage` | Manejo de errores |
|
|
1024
1207
|
|
|
1025
1208
|
## 🔄 Changelog
|
|
1026
1209
|
|
|
1027
1210
|
### v1.3.1
|
|
1211
|
+
|
|
1028
1212
|
- ✅ Sistema completo de Refresh Token Pattern
|
|
1029
1213
|
- ✅ Hooks modernos (useAuth, useUserData, useData)
|
|
1030
1214
|
- ✅ SessionProvider con contexto global
|
|
@@ -1043,4 +1227,4 @@ Si encuentras bugs o tienes sugerencias, por favor crea un issue en el repositor
|
|
|
1043
1227
|
|
|
1044
1228
|
---
|
|
1045
1229
|
|
|
1046
|
-
**¿Necesitas ayuda?** Consulta la documentación completa o contacta al equipo de desarrollo.
|
|
1230
|
+
**¿Necesitas ayuda?** Consulta la documentación completa o contacta al equipo de desarrollo.
|