@growy/strapi-plugin-encrypted-field 2.1.9 → 2.3.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 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
@@ -34,63 +34,45 @@ const Input = (props) => {
34
34
  };
35
35
 
36
36
  const handleCopy = async () => {
37
- if (value) {
38
- try {
39
- await navigator.clipboard.writeText(value);
37
+ if (!value) return;
40
38
 
41
- // Registrar auditoría
42
- await logAudit('copy', name, value);
43
-
44
- toggleNotification({
45
- type: 'success',
46
- message: 'Copiado al portapapeles',
47
- });
48
- } catch (err) {
49
- toggleNotification({
50
- type: 'danger',
51
- message: 'Error al copiar',
52
- });
53
- }
54
- }
55
- };
56
-
57
- 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
39
  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
40
+ await navigator.clipboard.writeText(value);
41
+ toggleNotification({
42
+ type: 'success',
43
+ message: formatMessage({
44
+ id: 'encrypted-field.notification.copied',
45
+ defaultMessage: 'Copiado al portapapeles',
46
+ }),
47
+ });
48
+ } catch (err) {
49
+ toggleNotification({
50
+ type: 'danger',
51
+ message: formatMessage({
52
+ id: 'encrypted-field.notification.copyError',
53
+ defaultMessage: 'Error al copiar',
80
54
  }),
81
55
  });
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
56
  }
89
57
  };
90
58
 
59
+ const toggleVisibility = () => {
60
+ setIsVisible((prev) => !prev);
61
+ };
62
+
91
63
  const fieldName = name.includes('.') ? name.split('.').pop() : name;
92
64
  const label = intlLabel?.id ? formatMessage(intlLabel) : (intlLabel || fieldName);
93
65
 
66
+ const visibilityLabel = formatMessage({
67
+ id: isVisible ? 'encrypted-field.action.hide' : 'encrypted-field.action.show',
68
+ defaultMessage: isVisible ? 'Ocultar' : 'Mostrar',
69
+ });
70
+
71
+ const copyLabel = formatMessage({
72
+ id: 'encrypted-field.action.copy',
73
+ defaultMessage: 'Copiar',
74
+ });
75
+
94
76
  return (
95
77
  <Field.Root
96
78
  name={name}
@@ -117,11 +99,11 @@ const Input = (props) => {
117
99
  top: '50%',
118
100
  transform: 'translateY(-50%)',
119
101
  display: 'flex',
120
- gap: '4px'
102
+ gap: '4px',
121
103
  }}>
122
104
  <IconButton
123
105
  onClick={toggleVisibility}
124
- label={isVisible ? 'Ocultar' : 'Mostrar'}
106
+ label={visibilityLabel}
125
107
  disabled={disabled}
126
108
  variant="ghost"
127
109
  >
@@ -129,7 +111,7 @@ const Input = (props) => {
129
111
  </IconButton>
130
112
  <IconButton
131
113
  onClick={handleCopy}
132
- label="Copiar"
114
+ label={copyLabel}
133
115
  disabled={disabled || !value}
134
116
  variant="ghost"
135
117
  >
@@ -100,47 +100,18 @@ 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 }) {
126
- const importedTrads = await Promise.all(
127
- locales.map((locale) => {
128
- return import(`./translations/${locale}.json`)
129
- .then(({ default: data }) => {
130
- return {
131
- data,
132
- locale,
133
- };
134
- })
135
- .catch(() => {
136
- return {
137
- data: {},
138
- locale,
139
- };
140
- });
106
+ return Promise.all(
107
+ locales.map(async (locale) => {
108
+ try {
109
+ const { default: data } = await import(`./translations/${locale}.json`);
110
+ return { data, locale };
111
+ } catch {
112
+ return { data: {}, locale };
113
+ }
141
114
  })
142
115
  );
143
-
144
- return Promise.resolve(importedTrads);
145
116
  },
146
117
  };
@@ -13,5 +13,10 @@
13
13
  "encrypted-field.options.minLength.description": "Minimum number of characters required",
14
14
  "encrypted-field.options.advanced.regex": "Validation",
15
15
  "encrypted-field.options.regex.label": "RegEx pattern",
16
- "encrypted-field.options.regex.description": "Validation pattern before encryption"
17
- }
16
+ "encrypted-field.options.regex.description": "Validation pattern before encryption",
17
+ "encrypted-field.notification.copied": "Copied to clipboard",
18
+ "encrypted-field.notification.copyError": "Error copying to clipboard",
19
+ "encrypted-field.action.show": "Show",
20
+ "encrypted-field.action.hide": "Hide",
21
+ "encrypted-field.action.copy": "Copy"
22
+ }
@@ -13,5 +13,10 @@
13
13
  "encrypted-field.options.minLength.description": "Número mínimo de caracteres requeridos",
14
14
  "encrypted-field.options.advanced.regex": "Validación",
15
15
  "encrypted-field.options.regex.label": "RegEx pattern",
16
- "encrypted-field.options.regex.description": "Patrón de validación antes de cifrar"
17
- }
16
+ "encrypted-field.options.regex.description": "Patrón de validación antes de cifrar",
17
+ "encrypted-field.notification.copied": "Copiado al portapapeles",
18
+ "encrypted-field.notification.copyError": "Error al copiar al portapapeles",
19
+ "encrypted-field.action.show": "Mostrar",
20
+ "encrypted-field.action.hide": "Ocultar",
21
+ "encrypted-field.action.copy": "Copiar"
22
+ }
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.3.0",
4
4
  "description": "Campo personalizado de texto cifrado para Strapi",
5
5
  "strapi": {
6
6
  "name": "encrypted-field",
@@ -10,7 +10,9 @@
10
10
  },
11
11
  "dependencies": {},
12
12
  "peerDependencies": {
13
- "@strapi/strapi": "^5.0.0"
13
+ "@strapi/strapi": "^5.0.0",
14
+ "@strapi/design-system": "^2.0.0",
15
+ "react": "^18.0.0"
14
16
  },
15
17
  "main": "server/index.js",
16
18
  "exports": {
@@ -53,4 +55,4 @@
53
55
  "publishConfig": {
54
56
  "access": "public"
55
57
  }
56
- }
58
+ }
@@ -1,117 +1,79 @@
1
1
  const { encrypt, decrypt, validateValue, isEncryptedField } = require('./utils/crypto');
2
2
 
3
+ function processEncryption(event, strapi) {
4
+ if (!event.model?.uid) return;
5
+
6
+ const { data } = event.params;
7
+ const currentModel = strapi.getModel(event.model.uid);
8
+
9
+ if (!currentModel?.attributes) return;
10
+
11
+ for (const [key, attribute] of Object.entries(currentModel.attributes)) {
12
+ if (!isEncryptedField(attribute)) continue;
13
+ if (!Object.prototype.hasOwnProperty.call(data, key)) continue;
14
+
15
+ const value = data[key];
16
+ if (value === null || value === undefined || value === '') continue;
17
+
18
+ const validation = validateValue(value, attribute);
19
+ if (!validation.valid) {
20
+ throw new Error(`Validación fallida para el campo "${key}": ${validation.error}`);
21
+ }
22
+
23
+ data[key] = encrypt(value, strapi);
24
+ }
25
+ }
26
+
27
+ function processDecryption(result, event, strapi) {
28
+ if (!result) return;
29
+ if (!event.model?.uid) return;
30
+
31
+ const currentModel = strapi.getModel(event.model.uid);
32
+ if (!currentModel?.attributes) return;
33
+
34
+ for (const [key, attribute] of Object.entries(currentModel.attributes)) {
35
+ if (!isEncryptedField(attribute)) continue;
36
+ if (!Object.prototype.hasOwnProperty.call(result, key)) continue;
37
+
38
+ const value = result[key];
39
+ if (typeof value === 'string' && value) {
40
+ result[key] = decrypt(value, strapi);
41
+ }
42
+ }
43
+ }
44
+
3
45
  module.exports = ({ strapi }) => {
4
-
5
-
6
- const contentTypes = Object.values(strapi.contentTypes);
7
- const components = Object.values(strapi.components);
8
- const allModels = [...contentTypes, ...components];
9
-
46
+ const allModels = [
47
+ ...Object.values(strapi.contentTypes),
48
+ ...Object.values(strapi.components),
49
+ ];
50
+
10
51
  allModels.forEach((model) => {
11
52
  const attributes = model.attributes || {};
12
-
13
- const encryptedFields = Object.entries(attributes)
14
- .filter(([key, attr]) => isEncryptedField(attr))
15
- .map(([key]) => key);
16
-
17
- if (encryptedFields.length === 0) return;
53
+ const hasEncryptedFields = Object.values(attributes).some(isEncryptedField);
18
54
 
19
- const uid = model.uid;
55
+ if (!hasEncryptedFields) return;
20
56
 
21
-
22
57
  strapi.db.lifecycles.subscribe({
23
- models: [uid],
24
-
58
+ models: [model.uid],
59
+
25
60
  async beforeCreate(event) {
26
- const { data } = event.params;
27
-
28
- if (!event.model?.uid) return;
29
-
30
- const currentModel = strapi.getModel(event.model.uid);
31
-
32
- if (!currentModel?.attributes) return;
33
-
34
- for (const [key, attribute] of Object.entries(currentModel.attributes)) {
35
- if (!isEncryptedField(attribute)) continue;
36
-
37
- if (Object.prototype.hasOwnProperty.call(data, key)) {
38
- const value = data[key];
39
-
40
- if (value === null || value === undefined || value === '') continue;
41
-
42
- const validation = validateValue(value, attribute);
43
- if (!validation.valid) {
44
- throw new Error(`Validación fallida para el campo "${key}": ${validation.error}`);
45
- }
46
-
47
- data[key] = encrypt(value, strapi);
48
- }
49
- }
61
+ processEncryption(event, strapi);
50
62
  },
51
63
 
52
64
  async beforeUpdate(event) {
53
- if (!event.model?.uid) return;
54
-
55
- const { data } = event.params;
56
- const currentModel = strapi.getModel(event.model.uid);
57
-
58
- if (!currentModel?.attributes) return;
59
-
60
- for (const [key, attribute] of Object.entries(currentModel.attributes)) {
61
- if (!isEncryptedField(attribute)) continue;
62
- if (Object.prototype.hasOwnProperty.call(data, key)) {
63
- const value = data[key];
64
- if (value === null || value === undefined || value === '') continue;
65
-
66
- const validation = validateValue(value, attribute);
67
- if (!validation.valid) {
68
- throw new Error(`Validación fallida para el campo "${key}": ${validation.error}`);
69
- }
70
-
71
- data[key] = encrypt(value, strapi);
72
- }
73
- }
65
+ processEncryption(event, strapi);
74
66
  },
75
67
 
76
68
  async afterFindOne(event) {
77
- const { result } = event;
78
- if (!result) return;
79
- if (!event.model?.uid) return;
80
-
81
- const currentModel = strapi.getModel(event.model.uid);
82
-
83
- if (!currentModel?.attributes) return;
84
-
85
- for (const [key, attribute] of Object.entries(currentModel.attributes)) {
86
- if (!isEncryptedField(attribute)) continue;
87
- if (Object.prototype.hasOwnProperty.call(result, key)) {
88
- const value = result[key];
89
- if (typeof value === 'string' && value) {
90
- result[key] = decrypt(value, strapi);
91
- }
92
- }
93
- }
69
+ processDecryption(event.result, event, strapi);
94
70
  },
95
71
 
96
72
  async afterFindMany(event) {
97
73
  const { result } = event;
98
74
  if (!result || !Array.isArray(result)) return;
99
- if (!event.model?.uid) return;
100
-
101
- const currentModel = strapi.getModel(event.model.uid);
102
-
103
- if (!currentModel?.attributes) return;
104
-
105
75
  for (const item of result) {
106
- for (const [key, attribute] of Object.entries(currentModel.attributes)) {
107
- if (!isEncryptedField(attribute)) continue;
108
- if (Object.prototype.hasOwnProperty.call(item, key)) {
109
- const value = item[key];
110
- if (typeof value === 'string' && value) {
111
- item[key] = decrypt(value, strapi);
112
- }
113
- }
114
- }
76
+ processDecryption(item, event, strapi);
115
77
  }
116
78
  },
117
79
  });
package/server/index.js CHANGED
@@ -1,16 +1,14 @@
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,
8
+ config: {
9
+ default: {},
10
+ validator: () => { },
12
11
  },
13
- routes: auditLogRoutes,
14
12
  middlewares: {
15
13
  decrypt,
16
14
  },
@@ -1,38 +1,49 @@
1
1
  const { decrypt, isEncryptedField } = require('../utils/crypto');
2
2
 
3
+ function resolveModelUidFromPath(ctx, strapi) {
4
+ const path = ctx.request?.path || '';
5
+ const match = path.match(/^\/api\/([^/]+)/);
6
+ if (!match) return null;
7
+
8
+ const slug = match[1];
9
+ const contentTypes = Object.values(strapi.contentTypes);
10
+
11
+ const ct = contentTypes.find((ct) => {
12
+ const info = ct.info || {};
13
+ return info.pluralName === slug || info.singularName === slug;
14
+ });
15
+
16
+ return ct?.uid || null;
17
+ }
18
+
3
19
  module.exports = (config, { strapi }) => {
4
20
  return async (ctx, next) => {
5
21
  await next();
6
22
 
7
23
  if (!ctx.body) return;
8
24
 
9
-
25
+ const rootModelUid = resolveModelUidFromPath(ctx, strapi);
26
+
10
27
  const decryptRecursive = (obj, modelUid = null) => {
11
28
  if (!obj || typeof obj !== 'object') return;
12
29
 
13
-
14
30
  if (Array.isArray(obj)) {
15
- obj.forEach(item => decryptRecursive(item, modelUid));
31
+ obj.forEach((item) => decryptRecursive(item, modelUid));
16
32
  return;
17
33
  }
18
34
 
19
-
20
35
  let currentModelUid = modelUid;
21
36
  if (obj.__component) {
22
37
  currentModelUid = obj.__component;
23
38
  }
24
39
 
25
-
26
40
  let model = null;
27
41
  if (currentModelUid) {
28
42
  try {
29
43
  model = strapi.getModel(currentModelUid) || strapi.components[currentModelUid];
30
- } catch (e) {
31
-
32
- }
44
+ } catch (e) { }
33
45
  }
34
46
 
35
-
36
47
  if (model?.attributes) {
37
48
  for (const [key, attribute] of Object.entries(model.attributes)) {
38
49
  if (isEncryptedField(attribute) && obj[key] && typeof obj[key] === 'string') {
@@ -45,7 +56,6 @@ module.exports = (config, { strapi }) => {
45
56
  }
46
57
  }
47
58
 
48
-
49
59
  for (const value of Object.values(obj)) {
50
60
  if (value && typeof value === 'object') {
51
61
  decryptRecursive(value, currentModelUid);
@@ -53,12 +63,10 @@ module.exports = (config, { strapi }) => {
53
63
  }
54
64
  };
55
65
 
56
-
57
66
  if (ctx.body.data) {
58
- decryptRecursive(ctx.body.data);
67
+ decryptRecursive(ctx.body.data, rootModelUid);
59
68
  } else {
60
-
61
- decryptRecursive(ctx.body);
69
+ decryptRecursive(ctx.body, rootModelUid);
62
70
  }
63
71
  };
64
72
  };
@@ -7,5 +7,9 @@ module.exports = ({ strapi }) => {
7
7
  name: 'encrypted-text',
8
8
  plugin: 'encrypted-field',
9
9
  type: 'string',
10
+ inputSize: {
11
+ default: 6,
12
+ isResizable: true,
13
+ },
10
14
  });
11
15
  };
@@ -5,9 +5,12 @@ const IV_LENGTH = 12;
5
5
  const AUTH_TAG_LENGTH = 16;
6
6
  const KEY_LENGTH = 32;
7
7
 
8
+ let _cachedKey = null;
9
+ let _cachedKeySource = null;
10
+
8
11
  function getEncryptionKey(strapi) {
9
12
  const key = process.env.ENCRYPTION_KEY || strapi?.config?.get('plugin.encrypted-field.encryptionKey');
10
-
13
+
11
14
  if (!key) {
12
15
  const errorMsg = '⚠️ ENCRYPTION_KEY no configurada. Debe establecer una clave de 64 caracteres hexadecimales en las variables de entorno o configuración de Strapi.';
13
16
  if (strapi?.log?.error) {
@@ -15,33 +18,39 @@ function getEncryptionKey(strapi) {
15
18
  }
16
19
  throw new Error(errorMsg);
17
20
  }
18
-
21
+
22
+ if (_cachedKey && _cachedKeySource === key) {
23
+ return _cachedKey;
24
+ }
25
+
19
26
  if (typeof key !== 'string' || key.length !== 64) {
20
27
  throw new Error(`ENCRYPTION_KEY debe tener exactamente 64 caracteres hexadecimales (32 bytes). Actual: ${key?.length || 0}`);
21
28
  }
22
-
29
+
23
30
  if (!/^[0-9a-fA-F]{64}$/.test(key)) {
24
31
  throw new Error('ENCRYPTION_KEY debe contener solo caracteres hexadecimales (0-9, a-f, A-F)');
25
32
  }
26
-
27
- return Buffer.from(key, 'hex');
33
+
34
+ _cachedKey = Buffer.from(key, 'hex');
35
+ _cachedKeySource = key;
36
+ return _cachedKey;
28
37
  }
29
38
 
30
39
  function encrypt(text, strapi) {
31
40
  if (typeof text !== 'string') return text;
32
-
41
+
33
42
  if (text === '') return text;
34
43
 
35
44
  try {
36
45
  const ENCRYPTION_KEY = getEncryptionKey(strapi);
37
46
  const iv = crypto.randomBytes(IV_LENGTH);
38
47
  const cipher = crypto.createCipheriv(ALGORITHM, ENCRYPTION_KEY, iv);
39
-
48
+
40
49
  let encrypted = cipher.update(text, 'utf8', 'hex');
41
50
  encrypted += cipher.final('hex');
42
-
51
+
43
52
  const authTag = cipher.getAuthTag();
44
-
53
+
45
54
  return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
46
55
  } catch (error) {
47
56
  if (strapi?.log?.error) {
@@ -53,29 +62,29 @@ function encrypt(text, strapi) {
53
62
 
54
63
  function decrypt(encryptedText, strapi) {
55
64
  if (!encryptedText || typeof encryptedText !== 'string') return encryptedText;
56
-
65
+
57
66
  const parts = encryptedText.split(':');
58
67
  if (parts.length !== 3) {
59
68
  return encryptedText;
60
69
  }
61
-
70
+
62
71
  try {
63
72
  const [ivHex, authTagHex, encrypted] = parts;
64
-
73
+
65
74
  if (ivHex.length !== IV_LENGTH * 2 || authTagHex.length !== AUTH_TAG_LENGTH * 2) {
66
75
  return encryptedText;
67
76
  }
68
-
77
+
69
78
  const ENCRYPTION_KEY = getEncryptionKey(strapi);
70
79
  const iv = Buffer.from(ivHex, 'hex');
71
80
  const authTag = Buffer.from(authTagHex, 'hex');
72
-
81
+
73
82
  const decipher = crypto.createDecipheriv(ALGORITHM, ENCRYPTION_KEY, iv);
74
83
  decipher.setAuthTag(authTag);
75
-
84
+
76
85
  let decrypted = decipher.update(encrypted, 'hex', 'utf8');
77
86
  decrypted += decipher.final('utf8');
78
-
87
+
79
88
  return decrypted;
80
89
  } catch (error) {
81
90
  if (strapi?.log?.debug) {
@@ -91,9 +100,9 @@ function validateValue(value, attribute) {
91
100
  }
92
101
 
93
102
  if (typeof value !== 'string') {
94
- return {
95
- valid: false,
96
- error: 'El valor debe ser una cadena de texto'
103
+ return {
104
+ valid: false,
105
+ error: 'El valor debe ser una cadena de texto'
97
106
  };
98
107
  }
99
108
 
@@ -101,30 +110,30 @@ function validateValue(value, attribute) {
101
110
  try {
102
111
  const regex = new RegExp(attribute.regex);
103
112
  if (!regex.test(value)) {
104
- return {
105
- valid: false,
106
- error: `El valor no cumple con el patrón de validación: ${attribute.regex}`
113
+ return {
114
+ valid: false,
115
+ error: `El valor no cumple con el patrón de validación: ${attribute.regex}`
107
116
  };
108
117
  }
109
118
  } catch (error) {
110
- return {
111
- valid: false,
112
- error: `Patrón regex inválido: ${error.message}`
119
+ return {
120
+ valid: false,
121
+ error: `Patrón regex inválido: ${error.message}`
113
122
  };
114
123
  }
115
124
  }
116
125
 
117
126
  if (attribute.maxLength && value.length > attribute.maxLength) {
118
- return {
119
- valid: false,
120
- error: `El valor excede la longitud máxima de ${attribute.maxLength} caracteres`
127
+ return {
128
+ valid: false,
129
+ error: `El valor excede la longitud máxima de ${attribute.maxLength} caracteres`
121
130
  };
122
131
  }
123
132
 
124
133
  if (attribute.minLength && value.length < attribute.minLength) {
125
- return {
126
- valid: false,
127
- error: `El valor debe tener al menos ${attribute.minLength} caracteres`
134
+ return {
135
+ valid: false,
136
+ error: `El valor debe tener al menos ${attribute.minLength} caracteres`
128
137
  };
129
138
  }
130
139
 
@@ -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
- ];