@growy/strapi-plugin-encrypted-field 2.1.9 → 2.2.1
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 +2 -45
- package/admin/src/components/Input.jsx +5 -39
- package/admin/src/index.js +0 -20
- package/package.json +1 -1
- package/server/index.js +0 -6
- package/server/register.js +2 -2
- package/admin/src/pages/AuditLogs.jsx +0 -123
- package/server/content-types/audit-log/schema.json +0 -59
- package/server/controllers/audit-log.js +0 -67
- package/server/routes/audit-log.js +0 -20
package/README.md
CHANGED
|
@@ -16,11 +16,10 @@ Plugin oficial de **Growy AI** para Strapi que proporciona un campo personalizad
|
|
|
16
16
|
- ✅ **UI mejorada** con controles de visibilidad y copiar al portapapeles
|
|
17
17
|
- ✅ **Valores ocultos** por defecto con opción de mostrar/ocultar
|
|
18
18
|
- ✅ **Notificaciones** de confirmación al copiar valores
|
|
19
|
-
- ✅ **Sistema de auditoría** que registra quién accede a datos sensibles
|
|
20
19
|
- ✅ **Gestión de claves robusta** con validación y mensajes de error claros
|
|
21
20
|
- ✅ **Datos cifrados** en base de datos con IV único y Auth Tag
|
|
22
21
|
- ✅ **Reutilizable** en cualquier colección o componente
|
|
23
|
-
- ✅ **
|
|
22
|
+
- ✅ **Soporte completo** para componentes anidados y estructuras complejas
|
|
24
23
|
|
|
25
24
|
## Instalación
|
|
26
25
|
|
|
@@ -99,12 +98,10 @@ npm run develop
|
|
|
99
98
|
|
|
100
99
|
## Requisitos
|
|
101
100
|
|
|
102
|
-
- **Strapi**: v5.0.0 o superior
|
|
101
|
+
- **Strapi**: v5.0.0 o superior
|
|
103
102
|
- **Node.js**: 18.x - 22.x
|
|
104
103
|
- **npm**: 6.0.0 o superior
|
|
105
104
|
|
|
106
|
-
⚠️ **Nota sobre Strapi 5**: Esta versión utiliza los componentes actualizados del Design System v2. Si tienes problemas con componentes de layout, asegúrate de que tu proyecto esté actualizado a Strapi 5.
|
|
107
|
-
|
|
108
105
|
## Validación de datos
|
|
109
106
|
|
|
110
107
|
El plugin soporta validación antes del cifrado:
|
|
@@ -123,46 +120,6 @@ El plugin soporta validación antes del cifrado:
|
|
|
123
120
|
|
|
124
121
|
Si el valor no cumple con el patrón, se lanzará un error antes de cifrar.
|
|
125
122
|
|
|
126
|
-
## Auditoría de seguridad
|
|
127
|
-
|
|
128
|
-
El plugin incluye un sistema completo de auditoría que registra todas las acciones sensibles realizadas sobre los campos cifrados:
|
|
129
|
-
|
|
130
|
-
### Funcionalidades de auditoría
|
|
131
|
-
|
|
132
|
-
- **Registro automático** de accesos a datos sensibles
|
|
133
|
-
- **Acciones rastreadas**:
|
|
134
|
-
- ✅ **Vista** de valores cifrados (al mostrar contenido oculto)
|
|
135
|
-
- ✅ **Copia** de valores al portapapeles
|
|
136
|
-
- ✅ **Descifrado** de datos (próximamente)
|
|
137
|
-
- **Información registrada**:
|
|
138
|
-
- Usuario que realizó la acción
|
|
139
|
-
- Campo y colección afectados
|
|
140
|
-
- ID de entrada modificada
|
|
141
|
-
- Dirección IP y User-Agent del navegador
|
|
142
|
-
- Timestamp preciso de la acción
|
|
143
|
-
|
|
144
|
-
### Acceso a logs de auditoría
|
|
145
|
-
|
|
146
|
-
1. **En el panel de administración**, ve al menú lateral izquierdo
|
|
147
|
-
2. **Busca el ítem del plugin** "Encrypted Field" (con el ícono 🔒)
|
|
148
|
-
3. **Dentro encontrarás** la opción **"Logs de Auditoría"**
|
|
149
|
-
4. **Haz clic** para ver la tabla con todos los eventos registrados
|
|
150
|
-
5. **Usa la paginación** para navegar por eventos antiguos
|
|
151
|
-
|
|
152
|
-
**Ubicación exacta:** Menú izquierdo → Encrypted Field → Logs de Auditoría
|
|
153
|
-
|
|
154
|
-
⚠️ **Nota:** Si no ves la opción, asegúrate de tener permisos de administrador y reinstalar el plugin después de actualizar.
|
|
155
|
-
|
|
156
|
-
### Configuración de permisos
|
|
157
|
-
|
|
158
|
-
Para acceder a los logs de auditoría, los usuarios necesitan el permiso:
|
|
159
|
-
```json
|
|
160
|
-
{
|
|
161
|
-
"action": "plugin::encrypted-field.read",
|
|
162
|
-
"subject": null
|
|
163
|
-
}
|
|
164
|
-
```
|
|
165
|
-
|
|
166
123
|
## Uso
|
|
167
124
|
|
|
168
125
|
### 1. Agregar campo cifrado a una colección
|
|
@@ -37,10 +37,6 @@ const Input = (props) => {
|
|
|
37
37
|
if (value) {
|
|
38
38
|
try {
|
|
39
39
|
await navigator.clipboard.writeText(value);
|
|
40
|
-
|
|
41
|
-
// Registrar auditoría
|
|
42
|
-
await logAudit('copy', name, value);
|
|
43
|
-
|
|
44
40
|
toggleNotification({
|
|
45
41
|
type: 'success',
|
|
46
42
|
message: 'Copiado al portapapeles',
|
|
@@ -55,37 +51,7 @@ const Input = (props) => {
|
|
|
55
51
|
};
|
|
56
52
|
|
|
57
53
|
const toggleVisibility = () => {
|
|
58
|
-
|
|
59
|
-
setIsVisible(newVisibility);
|
|
60
|
-
|
|
61
|
-
// Registrar auditoría cuando se muestra el valor
|
|
62
|
-
if (newVisibility && value) {
|
|
63
|
-
logAudit('view', name, value);
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const logAudit = async (action, fieldName, fieldValue) => {
|
|
68
|
-
try {
|
|
69
|
-
const response = await fetch('/encrypted-field/audit-logs/log', {
|
|
70
|
-
method: 'POST',
|
|
71
|
-
headers: {
|
|
72
|
-
'Content-Type': 'application/json',
|
|
73
|
-
},
|
|
74
|
-
body: JSON.stringify({
|
|
75
|
-
action,
|
|
76
|
-
field: fieldName,
|
|
77
|
-
collection: 'unknown', // Se puede mejorar obteniendo el contexto real
|
|
78
|
-
entry_id: 'unknown', // Se puede mejorar obteniendo el ID real
|
|
79
|
-
value: fieldValue.substring(0, 50) + (fieldValue.length > 50 ? '...' : ''), // Solo los primeros 50 caracteres para el log
|
|
80
|
-
}),
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
if (!response.ok) {
|
|
84
|
-
console.warn('Failed to log audit event');
|
|
85
|
-
}
|
|
86
|
-
} catch (error) {
|
|
87
|
-
console.warn('Error logging audit event:', error);
|
|
88
|
-
}
|
|
54
|
+
setIsVisible(!isVisible);
|
|
89
55
|
};
|
|
90
56
|
|
|
91
57
|
const fieldName = name.includes('.') ? name.split('.').pop() : name;
|
|
@@ -111,10 +77,10 @@ const Input = (props) => {
|
|
|
111
77
|
disabled={disabled}
|
|
112
78
|
style={{ paddingRight: '80px' }}
|
|
113
79
|
/>
|
|
114
|
-
<div style={{
|
|
115
|
-
position: 'absolute',
|
|
116
|
-
right: '8px',
|
|
117
|
-
top: '50%',
|
|
80
|
+
<div style={{
|
|
81
|
+
position: 'absolute',
|
|
82
|
+
right: '8px',
|
|
83
|
+
top: '50%',
|
|
118
84
|
transform: 'translateY(-50%)',
|
|
119
85
|
display: 'flex',
|
|
120
86
|
gap: '4px'
|
package/admin/src/index.js
CHANGED
|
@@ -100,26 +100,6 @@ export default {
|
|
|
100
100
|
],
|
|
101
101
|
},
|
|
102
102
|
});
|
|
103
|
-
|
|
104
|
-
// Agregar página de auditoría al menú - Strapi 5 approach
|
|
105
|
-
app.addMenuLink({
|
|
106
|
-
to: '/plugins/encrypted-field/audit-logs',
|
|
107
|
-
icon: 'chartLine',
|
|
108
|
-
intlLabel: {
|
|
109
|
-
id: 'encrypted-field.menu.audit-logs',
|
|
110
|
-
defaultMessage: 'Logs de Auditoría',
|
|
111
|
-
},
|
|
112
|
-
Component: async () => {
|
|
113
|
-
const component = await import('./pages/AuditLogs');
|
|
114
|
-
return component.default;
|
|
115
|
-
},
|
|
116
|
-
permissions: [
|
|
117
|
-
{
|
|
118
|
-
action: 'plugin::encrypted-field.read',
|
|
119
|
-
subject: null,
|
|
120
|
-
},
|
|
121
|
-
],
|
|
122
|
-
});
|
|
123
103
|
},
|
|
124
104
|
|
|
125
105
|
async registerTrads({ locales }) {
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
const register = require('./register');
|
|
2
2
|
const bootstrap = require('./bootstrap');
|
|
3
3
|
const decrypt = require('./middlewares/decrypt');
|
|
4
|
-
const auditLogController = require('./controllers/audit-log');
|
|
5
|
-
const auditLogRoutes = require('./routes/audit-log');
|
|
6
4
|
|
|
7
5
|
module.exports = {
|
|
8
6
|
register,
|
|
9
7
|
bootstrap,
|
|
10
|
-
controllers: {
|
|
11
|
-
'audit-log': auditLogController,
|
|
12
|
-
},
|
|
13
|
-
routes: auditLogRoutes,
|
|
14
8
|
middlewares: {
|
|
15
9
|
decrypt,
|
|
16
10
|
},
|
package/server/register.js
CHANGED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
2
|
-
import { Box, Typography, Table, Pagination, Badge } from '@strapi/design-system';
|
|
3
|
-
import { useNotification } from '@strapi/strapi/admin';
|
|
4
|
-
|
|
5
|
-
const AuditLogs = () => {
|
|
6
|
-
const [logs, setLogs] = useState([]);
|
|
7
|
-
const [loading, setLoading] = useState(true);
|
|
8
|
-
const [pagination, setPagination] = useState({ page: 1, pageSize: 25, total: 0 });
|
|
9
|
-
const { toggleNotification } = useNotification();
|
|
10
|
-
|
|
11
|
-
const fetchLogs = async (page = 1) => {
|
|
12
|
-
try {
|
|
13
|
-
setLoading(true);
|
|
14
|
-
const response = await fetch(`/encrypted-field/audit-logs?page=${page}&pageSize=${pagination.pageSize}`);
|
|
15
|
-
|
|
16
|
-
if (response.ok) {
|
|
17
|
-
const data = await response.json();
|
|
18
|
-
setLogs(data.data || []);
|
|
19
|
-
setPagination(data.meta.pagination);
|
|
20
|
-
} else {
|
|
21
|
-
toggleNotification({
|
|
22
|
-
type: 'danger',
|
|
23
|
-
message: 'Error al cargar logs de auditoría',
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
} catch (error) {
|
|
27
|
-
console.error('Error fetching audit logs:', error);
|
|
28
|
-
toggleNotification({
|
|
29
|
-
type: 'danger',
|
|
30
|
-
message: 'Error al cargar logs de auditoría',
|
|
31
|
-
});
|
|
32
|
-
} finally {
|
|
33
|
-
setLoading(false);
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
fetchLogs(pagination.page);
|
|
39
|
-
}, [pagination.page]);
|
|
40
|
-
|
|
41
|
-
const handlePageChange = (page) => {
|
|
42
|
-
setPagination(prev => ({ ...prev, page }));
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const getBadgeColor = (action) => {
|
|
46
|
-
switch (action) {
|
|
47
|
-
case 'view':
|
|
48
|
-
return 'secondary';
|
|
49
|
-
case 'copy':
|
|
50
|
-
return 'warning';
|
|
51
|
-
case 'decrypt':
|
|
52
|
-
return 'danger';
|
|
53
|
-
default:
|
|
54
|
-
return 'neutral';
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const formatTimestamp = (timestamp) => {
|
|
59
|
-
return new Date(timestamp).toLocaleString('es-ES', {
|
|
60
|
-
year: 'numeric',
|
|
61
|
-
month: 'short',
|
|
62
|
-
day: 'numeric',
|
|
63
|
-
hour: '2-digit',
|
|
64
|
-
minute: '2-digit',
|
|
65
|
-
second: '2-digit',
|
|
66
|
-
});
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const tableHeaders = [
|
|
70
|
-
{ name: 'timestamp', label: 'Fecha/Hora' },
|
|
71
|
-
{ name: 'user', label: 'Usuario' },
|
|
72
|
-
{ name: 'action', label: 'Acción' },
|
|
73
|
-
{ name: 'field', label: 'Campo' },
|
|
74
|
-
{ name: 'collection', label: 'Colección' },
|
|
75
|
-
{ name: 'entry_id', label: 'ID de entrada' },
|
|
76
|
-
];
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
<Box padding={8}>
|
|
80
|
-
<Box paddingBottom={4}>
|
|
81
|
-
<Typography variant="alpha">Logs de Auditoría - Campos Cifrados</Typography>
|
|
82
|
-
<Typography variant="epsilon" textColor="neutral600">
|
|
83
|
-
Registro de acciones realizadas sobre campos cifrados
|
|
84
|
-
</Typography>
|
|
85
|
-
</Box>
|
|
86
|
-
|
|
87
|
-
{loading ? (
|
|
88
|
-
<Box>Cargando...</Box>
|
|
89
|
-
) : (
|
|
90
|
-
<>
|
|
91
|
-
<Table
|
|
92
|
-
headers={tableHeaders}
|
|
93
|
-
rows={logs.map(log => ({
|
|
94
|
-
timestamp: formatTimestamp(log.timestamp),
|
|
95
|
-
user: log.user,
|
|
96
|
-
action: (
|
|
97
|
-
<Badge color={getBadgeColor(log.action)}>
|
|
98
|
-
{log.action === 'view' && 'Vista'}
|
|
99
|
-
{log.action === 'copy' && 'Copiado'}
|
|
100
|
-
{log.action === 'decrypt' && 'Descifrado'}
|
|
101
|
-
</Badge>
|
|
102
|
-
),
|
|
103
|
-
field: log.field,
|
|
104
|
-
collection: log.collection,
|
|
105
|
-
entry_id: log.entry_id,
|
|
106
|
-
}))}
|
|
107
|
-
/>
|
|
108
|
-
{pagination.total > pagination.pageSize && (
|
|
109
|
-
<Box paddingTop={4}>
|
|
110
|
-
<Pagination
|
|
111
|
-
activePage={pagination.page}
|
|
112
|
-
pageCount={pagination.pageCount}
|
|
113
|
-
onPageChange={handlePageChange}
|
|
114
|
-
/>
|
|
115
|
-
</Box>
|
|
116
|
-
)}
|
|
117
|
-
</>
|
|
118
|
-
)}
|
|
119
|
-
</Box>
|
|
120
|
-
);
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
export default AuditLogs;
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"kind": "collectionType",
|
|
3
|
-
"collectionName": "audit_logs",
|
|
4
|
-
"info": {
|
|
5
|
-
"singularName": "audit-log",
|
|
6
|
-
"pluralName": "audit-logs",
|
|
7
|
-
"displayName": "Audit Log",
|
|
8
|
-
"description": "Registros de auditoría para el plugin de campos cifrados"
|
|
9
|
-
},
|
|
10
|
-
"options": {
|
|
11
|
-
"draftAndPublish": false,
|
|
12
|
-
"timestamps": true
|
|
13
|
-
},
|
|
14
|
-
"pluginOptions": {
|
|
15
|
-
"content-manager": {
|
|
16
|
-
"visible": true
|
|
17
|
-
},
|
|
18
|
-
"content-type-builder": {
|
|
19
|
-
"visible": false
|
|
20
|
-
}
|
|
21
|
-
},
|
|
22
|
-
"attributes": {
|
|
23
|
-
"action": {
|
|
24
|
-
"type": "enumeration",
|
|
25
|
-
"enum": [
|
|
26
|
-
"decrypt",
|
|
27
|
-
"copy",
|
|
28
|
-
"view"
|
|
29
|
-
],
|
|
30
|
-
"required": true
|
|
31
|
-
},
|
|
32
|
-
"user": {
|
|
33
|
-
"type": "string",
|
|
34
|
-
"required": true
|
|
35
|
-
},
|
|
36
|
-
"field": {
|
|
37
|
-
"type": "string",
|
|
38
|
-
"required": true
|
|
39
|
-
},
|
|
40
|
-
"collection": {
|
|
41
|
-
"type": "string",
|
|
42
|
-
"required": true
|
|
43
|
-
},
|
|
44
|
-
"entry_id": {
|
|
45
|
-
"type": "string",
|
|
46
|
-
"required": true
|
|
47
|
-
},
|
|
48
|
-
"ip_address": {
|
|
49
|
-
"type": "string"
|
|
50
|
-
},
|
|
51
|
-
"user_agent": {
|
|
52
|
-
"type": "text"
|
|
53
|
-
},
|
|
54
|
-
"timestamp": {
|
|
55
|
-
"type": "datetime",
|
|
56
|
-
"default": "now"
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
const { createCoreController } = require('@strapi/strapi').factories;
|
|
2
|
-
|
|
3
|
-
module.exports = createCoreController('plugin::encrypted-field.audit-log', ({ strapi }) => ({
|
|
4
|
-
async log(ctx) {
|
|
5
|
-
try {
|
|
6
|
-
const { action, field, collection, entry_id, ip_address, user_agent } = ctx.request.body;
|
|
7
|
-
|
|
8
|
-
// Obtener información del usuario actual
|
|
9
|
-
const user = ctx.state.user;
|
|
10
|
-
const userInfo = user ? `${user.firstname || ''} ${user.lastname || ''}`.trim() || user.username || user.email : 'Usuario desconocido';
|
|
11
|
-
|
|
12
|
-
// Crear el registro de auditoría
|
|
13
|
-
const auditLog = await strapi.entityService.create('plugin::encrypted-field.audit-log', {
|
|
14
|
-
data: {
|
|
15
|
-
action,
|
|
16
|
-
user: userInfo,
|
|
17
|
-
field,
|
|
18
|
-
collection,
|
|
19
|
-
entry_id,
|
|
20
|
-
ip_address: ip_address || ctx.request.ip,
|
|
21
|
-
user_agent: user_agent || ctx.request.header['user-agent'],
|
|
22
|
-
timestamp: new Date()
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
ctx.send({
|
|
27
|
-
success: true,
|
|
28
|
-
data: auditLog
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
} catch (error) {
|
|
32
|
-
strapi.log.error('Error creating audit log:', error);
|
|
33
|
-
ctx.badRequest('Error al registrar auditoría', { error: error.message });
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
|
|
37
|
-
async getLogs(ctx) {
|
|
38
|
-
try {
|
|
39
|
-
const { page = 1, pageSize = 25, sort = 'timestamp:desc' } = ctx.query;
|
|
40
|
-
|
|
41
|
-
const logs = await strapi.entityService.findMany('plugin::encrypted-field.audit-log', {
|
|
42
|
-
start: (page - 1) * pageSize,
|
|
43
|
-
limit: pageSize,
|
|
44
|
-
sort: sort,
|
|
45
|
-
populate: '*'
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const total = await strapi.entityService.count('plugin::encrypted-field.audit-log');
|
|
49
|
-
|
|
50
|
-
ctx.send({
|
|
51
|
-
data: logs,
|
|
52
|
-
meta: {
|
|
53
|
-
pagination: {
|
|
54
|
-
page: parseInt(page),
|
|
55
|
-
pageSize: parseInt(pageSize),
|
|
56
|
-
pageCount: Math.ceil(total / pageSize),
|
|
57
|
-
total
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
} catch (error) {
|
|
63
|
-
strapi.log.error('Error fetching audit logs:', error);
|
|
64
|
-
ctx.badRequest('Error al obtener logs de auditoría', { error: error.message });
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}));
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
module.exports = [
|
|
2
|
-
{
|
|
3
|
-
method: 'POST',
|
|
4
|
-
path: '/audit-logs/log',
|
|
5
|
-
handler: 'audit-log.log',
|
|
6
|
-
config: {
|
|
7
|
-
policies: [],
|
|
8
|
-
auth: false,
|
|
9
|
-
}
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
method: 'GET',
|
|
13
|
-
path: '/audit-logs',
|
|
14
|
-
handler: 'audit-log.getLogs',
|
|
15
|
-
config: {
|
|
16
|
-
policies: [],
|
|
17
|
-
auth: false,
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
];
|