@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 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
- - ✅ **Compatible con Strapi 5** - Utiliza componentes actualizados del Design System v2
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 (compatible con Design System v2)
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
- const newVisibility = !isVisible;
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'
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growy/strapi-plugin-encrypted-field",
3
- "version": "2.1.9",
3
+ "version": "2.2.1",
4
4
  "description": "Campo personalizado de texto cifrado para Strapi",
5
5
  "strapi": {
6
6
  "name": "encrypted-field",
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
  },
@@ -1,8 +1,8 @@
1
1
  module.exports = ({ strapi }) => {
2
2
  const decryptMiddleware = require('./middlewares/decrypt');
3
-
3
+
4
4
  strapi.server.use(decryptMiddleware({}, { strapi }));
5
-
5
+
6
6
  strapi.customFields.register({
7
7
  name: 'encrypted-text',
8
8
  plugin: 'encrypted-field',
@@ -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
- ];