webspresso 0.0.97 → 0.0.99
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/.agents/ADMIN.md +163 -5
- package/.agents/AGENTS.md +1 -1
- package/.agents/ARCHITECTURE.md +10 -0
- package/.agents/AUTH.md +169 -0
- package/.agents/CLI.md +195 -1
- package/.agents/ORM.md +88 -4
- package/.agents/PLUGINS.md +221 -9
- package/.agents/QUEUE.md +127 -0
- package/.agents/ROUTING.md +127 -7
- package/AGENTS.md +1 -0
- package/bin/commands/add-esbuild.js +80 -0
- package/bin/commands/add-tailwind.js +76 -31
- package/bin/commands/analytics-prune.js +57 -0
- package/bin/commands/api.js +207 -15
- package/bin/commands/build.js +99 -0
- package/bin/commands/dev.js +64 -35
- package/bin/commands/doctor.js +26 -0
- package/bin/commands/email-prune.js +47 -0
- package/bin/commands/make-job.js +86 -0
- package/bin/commands/make-model.js +250 -0
- package/bin/commands/new.js +45 -28
- package/bin/commands/openapi.js +102 -0
- package/bin/commands/page.js +6 -3
- package/bin/commands/queue-prune.js +59 -0
- package/bin/commands/routes.js +52 -0
- package/bin/commands/service.js +6 -3
- package/bin/commands/typegen.js +106 -0
- package/bin/commands/upload-prune.js +79 -0
- package/bin/webspresso.js +22 -0
- package/core/applySchema.js +13 -6
- package/core/auth/hash.js +21 -12
- package/core/auth/index.js +60 -0
- package/core/auth/manager.js +134 -0
- package/core/auth/middleware.js +88 -0
- package/core/auth/oauth.js +517 -0
- package/core/auth/qrcode.js +499 -0
- package/core/auth/totp.js +504 -0
- package/core/compression/middleware.js +23 -17
- package/core/openapi/build-from-api-routes.js +160 -29
- package/core/orm/cache/fingerprint.js +15 -1
- package/core/orm/cache/memory-provider.js +27 -2
- package/core/orm/eager-loader.js +144 -10
- package/core/orm/index.js +20 -7
- package/core/orm/model.js +53 -7
- package/core/orm/query-builder.js +90 -4
- package/core/orm/repository.js +44 -3
- package/core/orm/search.js +213 -0
- package/core/queue/adapters/database.js +94 -0
- package/core/queue/adapters/memory.js +62 -0
- package/core/queue/adapters/redis.js +80 -0
- package/core/queue/cron.js +331 -0
- package/core/queue/index.js +8 -1
- package/core/queue/manager.js +205 -1
- package/core/queue/scheduler.js +359 -0
- package/core/sse/index.js +19 -0
- package/core/sse/stream.js +430 -0
- package/core/ssr/stream.js +11 -0
- package/index.d.ts +1188 -10
- package/index.js +88 -96
- package/package.json +1 -1
- package/plugins/admin-panel/api.js +279 -17
- package/plugins/admin-panel/app.js +114 -22
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
- package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +333 -36
- package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
- package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
- package/plugins/admin-panel/client/parts/08-record-list.js +248 -23
- package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
- package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
- package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
- package/plugins/admin-panel/core/admin-module.js +13 -7
- package/plugins/admin-panel/core/api-extensions.js +30 -2
- package/plugins/admin-panel/core/registry.js +96 -16
- package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
- package/plugins/admin-panel/field-renderers/index.js +2 -0
- package/plugins/admin-panel/index.js +96 -0
- package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
- package/plugins/admin-panel/modules/admin-users.js +16 -1
- package/plugins/admin-panel/modules/bulk-actions.js +13 -5
- package/plugins/admin-panel/modules/custom-pages.js +492 -3
- package/plugins/admin-panel/modules/dashboard.js +283 -0
- package/plugins/admin-panel/modules/menu.js +56 -18
- package/plugins/admin-panel/modules/user-management.js +24 -6
- package/plugins/api-key/crypto.js +101 -0
- package/plugins/api-key/index.js +288 -0
- package/plugins/api-key/middleware.js +461 -0
- package/plugins/api-key/migration-template.js +48 -0
- package/plugins/api-key/model.js +50 -0
- package/plugins/api-key/scopes.js +125 -0
- package/plugins/audit-log/admin-component.js +487 -59
- package/plugins/audit-log/api-handlers.js +148 -0
- package/plugins/audit-log/index.js +153 -3
- package/plugins/audit-log/purge.js +60 -10
- package/plugins/dashboard/app.js +152 -27
- package/plugins/dashboard/index.js +143 -39
- package/plugins/dashboard/styles.js +78 -0
- package/plugins/data-exchange/export-xlsx.js +9 -2
- package/plugins/data-exchange/import.js +9 -2
- package/plugins/data-exchange/index.js +6 -3
- package/plugins/email/index.js +132 -2
- package/plugins/email/purge.js +60 -10
- package/plugins/file-manager/admin-component.js +20 -4
- package/plugins/file-manager/index.js +21 -4
- package/plugins/index.js +10 -0
- package/plugins/oauth/index.js +338 -0
- package/plugins/orm-cache-admin/index.js +4 -2
- package/plugins/queue/index.js +15 -0
- package/plugins/schema-explorer/admin-component.js +714 -0
- package/plugins/schema-explorer/relations-graph.js +205 -0
- package/plugins/schema-explorer.js +248 -69
- package/plugins/site-analytics/admin-component.js +23 -0
- package/plugins/site-analytics/api-handlers.js +42 -0
- package/plugins/site-analytics/index.js +147 -2
- package/plugins/site-analytics/purge.js +168 -0
- package/plugins/swagger.js +261 -13
- package/plugins/upload/index.js +26 -0
- package/plugins/upload/orphan-pruner.js +292 -0
- package/src/api/api-loader.js +4 -1
- package/src/bundle/index.js +517 -0
- package/src/bundle/manifest.js +165 -0
- package/src/dev/banner.js +296 -0
- package/src/dev/colors.js +70 -0
- package/src/dev/index.js +62 -0
- package/src/dev/profiler.js +354 -0
- package/src/dev/typegen.js +497 -0
- package/src/dev-reload/index.js +611 -0
- package/src/discovery/file-route-parser.js +44 -1
- package/src/discovery/scan-routes.js +2 -2
- package/src/file-router.js +347 -162
- package/src/helpers.js +967 -98
- package/src/njk-frontmatter.js +12 -0
- package/src/pages/page-loader.js +61 -11
- package/src/routing/cascading.js +378 -0
- package/src/routing/middleware-resolver.js +92 -0
- package/src/server.js +635 -63
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* @module plugins/audit-log/api-handlers
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const { purgeAuditLogs } = require('./purge');
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* @param {Object} options
|
|
8
10
|
* @param {import('knex').Knex} options.knex
|
|
@@ -19,12 +21,34 @@ function createAuditLogHandlers(options) {
|
|
|
19
21
|
if (query.action) {
|
|
20
22
|
qb.where('action', String(query.action));
|
|
21
23
|
}
|
|
24
|
+
const recordId = query.recordId || query.resource_id || query.resourceId;
|
|
25
|
+
if (recordId !== undefined && recordId !== null && String(recordId).trim() !== '') {
|
|
26
|
+
qb.where('resource_id', String(recordId));
|
|
27
|
+
}
|
|
28
|
+
const actorId = query.actorId || query.actor_id;
|
|
29
|
+
if (actorId !== undefined && actorId !== null && String(actorId).trim() !== '') {
|
|
30
|
+
qb.where('actor_id', Number(actorId));
|
|
31
|
+
}
|
|
32
|
+
const actorEmail = query.actorEmail || query.actor_email;
|
|
33
|
+
if (actorEmail) {
|
|
34
|
+
qb.where('actor_email', 'like', `%${String(actorEmail).trim()}%`);
|
|
35
|
+
}
|
|
22
36
|
if (query.from) {
|
|
23
37
|
qb.where('created_at', '>=', String(query.from));
|
|
24
38
|
}
|
|
25
39
|
if (query.to) {
|
|
26
40
|
qb.where('created_at', '<=', String(query.to));
|
|
27
41
|
}
|
|
42
|
+
const searchTerm = query.search || query.q;
|
|
43
|
+
if (searchTerm && String(searchTerm).trim() !== '') {
|
|
44
|
+
const term = `%${String(searchTerm).trim()}%`;
|
|
45
|
+
qb.where((sub) => {
|
|
46
|
+
sub.where('actor_email', 'like', term)
|
|
47
|
+
.orWhere('path', 'like', term)
|
|
48
|
+
.orWhere('resource_id', 'like', term)
|
|
49
|
+
.orWhere('resource_model', 'like', term);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
28
52
|
}
|
|
29
53
|
|
|
30
54
|
async function listHandler(req, res) {
|
|
@@ -51,13 +75,119 @@ function createAuditLogHandlers(options) {
|
|
|
51
75
|
}
|
|
52
76
|
}
|
|
53
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Dedicated Record Timeline Handler
|
|
80
|
+
* GET /audit-logs/timeline/:model/:id or GET /audit-logs/timeline?model=Post&id=1
|
|
81
|
+
*/
|
|
82
|
+
async function timelineHandler(req, res) {
|
|
83
|
+
try {
|
|
84
|
+
const model = req.params?.model || req.query?.model;
|
|
85
|
+
const recordId = req.params?.id || req.query?.id || req.query?.recordId || req.query?.resource_id;
|
|
86
|
+
|
|
87
|
+
if (!model || recordId === undefined) {
|
|
88
|
+
return res.status(400).json({ error: 'model and id are required to fetch timeline' });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const rows = await knex(tableName)
|
|
92
|
+
.where({
|
|
93
|
+
resource_model: String(model),
|
|
94
|
+
resource_id: String(recordId),
|
|
95
|
+
})
|
|
96
|
+
.orderBy('created_at', 'asc');
|
|
97
|
+
|
|
98
|
+
const totalChanges = rows.length;
|
|
99
|
+
const createdEvent = rows.find((r) => r.action === 'create') || null;
|
|
100
|
+
const lastModifiedEvent = rows.length > 0 ? rows[rows.length - 1] : null;
|
|
101
|
+
|
|
102
|
+
const events = rows.map((r, index) => {
|
|
103
|
+
let parsedMetadata = r.metadata;
|
|
104
|
+
if (typeof parsedMetadata === 'string') {
|
|
105
|
+
try {
|
|
106
|
+
parsedMetadata = JSON.parse(parsedMetadata);
|
|
107
|
+
} catch {}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const changedFields = Array.isArray(parsedMetadata?.changedFields)
|
|
111
|
+
? parsedMetadata.changedFields
|
|
112
|
+
: [];
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
id: r.id,
|
|
116
|
+
action: r.action,
|
|
117
|
+
createdAt: r.created_at,
|
|
118
|
+
actor: {
|
|
119
|
+
id: r.actor_id,
|
|
120
|
+
email: r.actor_email,
|
|
121
|
+
},
|
|
122
|
+
resourceModel: r.resource_model,
|
|
123
|
+
resourceId: r.resource_id,
|
|
124
|
+
httpMethod: r.http_method,
|
|
125
|
+
path: r.path,
|
|
126
|
+
ip: r.ip,
|
|
127
|
+
userAgent: r.user_agent,
|
|
128
|
+
changedFields,
|
|
129
|
+
metadata: parsedMetadata,
|
|
130
|
+
index: index + 1,
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
res.json({
|
|
135
|
+
model: String(model),
|
|
136
|
+
recordId: String(recordId),
|
|
137
|
+
summary: {
|
|
138
|
+
totalChanges,
|
|
139
|
+
createdAt: createdEvent?.created_at || (events[0] ? events[0].createdAt : null),
|
|
140
|
+
createdBy: createdEvent?.actor_email || (events[0] ? events[0].actor?.email : null),
|
|
141
|
+
lastModifiedAt: lastModifiedEvent?.created_at || null,
|
|
142
|
+
lastModifiedBy: lastModifiedEvent?.actor_email || null,
|
|
143
|
+
},
|
|
144
|
+
timeline: events,
|
|
145
|
+
});
|
|
146
|
+
} catch (err) {
|
|
147
|
+
res.status(500).json({ error: err.message });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function purgeHandler(req, res) {
|
|
152
|
+
try {
|
|
153
|
+
const body = req.body || {};
|
|
154
|
+
const query = req.query || {};
|
|
155
|
+
const rawDays = body.days !== undefined
|
|
156
|
+
? body.days
|
|
157
|
+
: (body.retentionDays !== undefined ? body.retentionDays : (query.days !== undefined ? query.days : 90));
|
|
158
|
+
const days = parseInt(rawDays, 10);
|
|
159
|
+
if (Number.isNaN(days) || days < 1) {
|
|
160
|
+
return res.status(400).json({ error: 'days must be a positive integer' });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const olderThan = body.olderThan
|
|
164
|
+
? new Date(body.olderThan)
|
|
165
|
+
: new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
166
|
+
|
|
167
|
+
if (Number.isNaN(olderThan.getTime())) {
|
|
168
|
+
return res.status(400).json({ error: 'invalid olderThan date' });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const deleted = await purgeAuditLogs(knex, {
|
|
172
|
+
tableName,
|
|
173
|
+
olderThan,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
res.json({ ok: true, success: true, deleted, olderThan: olderThan.toISOString() });
|
|
177
|
+
} catch (err) {
|
|
178
|
+
res.status(500).json({ error: err.message });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
54
182
|
/**
|
|
55
183
|
* Programmatic list (for other plugins)
|
|
56
184
|
* @param {Object} [filters]
|
|
57
185
|
* @param {string} [filters.model]
|
|
58
186
|
* @param {string} [filters.action]
|
|
187
|
+
* @param {string} [filters.recordId]
|
|
59
188
|
* @param {string} [filters.from]
|
|
60
189
|
* @param {string} [filters.to]
|
|
190
|
+
* @param {string} [filters.search]
|
|
61
191
|
* @param {number} [filters.limit=100]
|
|
62
192
|
* @param {number} [filters.offset=0]
|
|
63
193
|
*/
|
|
@@ -72,9 +202,27 @@ function createAuditLogHandlers(options) {
|
|
|
72
202
|
.limit(limit);
|
|
73
203
|
}
|
|
74
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Programmatic query for single record timeline
|
|
207
|
+
* @param {string} model
|
|
208
|
+
* @param {string|number} recordId
|
|
209
|
+
*/
|
|
210
|
+
async function queryTimeline(model, recordId) {
|
|
211
|
+
if (!model || recordId === undefined) return [];
|
|
212
|
+
return knex(tableName)
|
|
213
|
+
.where({
|
|
214
|
+
resource_model: String(model),
|
|
215
|
+
resource_id: String(recordId),
|
|
216
|
+
})
|
|
217
|
+
.orderBy('created_at', 'asc');
|
|
218
|
+
}
|
|
219
|
+
|
|
75
220
|
return {
|
|
76
221
|
listHandler,
|
|
222
|
+
timelineHandler,
|
|
223
|
+
purgeHandler,
|
|
77
224
|
queryLogs,
|
|
225
|
+
queryTimeline,
|
|
78
226
|
};
|
|
79
227
|
}
|
|
80
228
|
|
|
@@ -16,40 +16,167 @@ const { purgeAuditLogs } = require('./purge');
|
|
|
16
16
|
* @param {string} [options.tableName='audit_logs']
|
|
17
17
|
* @param {boolean} [options.includeDefaultPage=true] - Register Mithril list page + menu
|
|
18
18
|
* @param {string} [options.apiPrefix='/audit-logs'] - GET list under admin API
|
|
19
|
+
* @param {Object|boolean} [options.retention] - Retention policy configuration (set false to disable)
|
|
20
|
+
* @param {number} [options.retention.days=90] - Days to retain audit logs
|
|
21
|
+
* @param {number} [options.retention.intervalMs=86400000] - Auto-cleanup timer frequency (24h)
|
|
22
|
+
* @param {boolean} [options.retention.runOnStart=true] - Run initial purge shortly after startup
|
|
23
|
+
* @param {number} [options.retentionDays=90] - Shorthand for retention.days
|
|
24
|
+
* @param {number} [options.cleanupIntervalMs=86400000] - Shorthand for retention.intervalMs
|
|
25
|
+
* @param {boolean} [options.autoCleanup=true] - Enable automatic background cleanup
|
|
26
|
+
* @param {boolean} [options.cleanupOnStart=true] - Run initial purge on start
|
|
27
|
+
* @param {number} [options.cleanupStartDelayMs] - Delay in ms before start purge
|
|
19
28
|
*/
|
|
20
29
|
function auditLogPlugin(options = {}) {
|
|
21
30
|
const {
|
|
22
31
|
db,
|
|
32
|
+
title,
|
|
33
|
+
adminTitle,
|
|
34
|
+
pageTitle: customPageTitle,
|
|
23
35
|
adminPath: adminPathOpt,
|
|
24
36
|
tableName = 'audit_logs',
|
|
25
37
|
includeDefaultPage = true,
|
|
26
38
|
apiPrefix = '/audit-logs',
|
|
39
|
+
registerRecordAction = true,
|
|
27
40
|
} = options;
|
|
28
41
|
|
|
42
|
+
const pageTitle = title || adminTitle || customPageTitle || 'Audit log';
|
|
43
|
+
|
|
29
44
|
if (!db) {
|
|
30
45
|
throw new Error('audit-log plugin requires a database instance. Pass `db` in options.');
|
|
31
46
|
}
|
|
32
47
|
|
|
33
48
|
const knex = db.knex || db;
|
|
34
49
|
|
|
50
|
+
// Retention configuration
|
|
51
|
+
const retentionOption = options.retention;
|
|
52
|
+
const isRetentionDisabled =
|
|
53
|
+
retentionOption === false ||
|
|
54
|
+
(typeof retentionOption === 'object' && retentionOption.enabled === false) ||
|
|
55
|
+
options.retentionDays === null ||
|
|
56
|
+
options.retentionDays === 0;
|
|
57
|
+
|
|
58
|
+
const autoCleanupEnabled =
|
|
59
|
+
!isRetentionDisabled &&
|
|
60
|
+
options.autoCleanup !== false &&
|
|
61
|
+
(typeof retentionOption !== 'object' || retentionOption.autoCleanup !== false);
|
|
62
|
+
|
|
63
|
+
const retentionConfig = {
|
|
64
|
+
enabled: !isRetentionDisabled,
|
|
65
|
+
autoCleanup: autoCleanupEnabled,
|
|
66
|
+
days: (typeof options.retentionDays === 'number' && options.retentionDays > 0)
|
|
67
|
+
? options.retentionDays
|
|
68
|
+
: (typeof retentionOption === 'object' && retentionOption?.days ? retentionOption.days : 90),
|
|
69
|
+
intervalMs: (typeof options.cleanupIntervalMs === 'number' && options.cleanupIntervalMs > 0)
|
|
70
|
+
? options.cleanupIntervalMs
|
|
71
|
+
: (typeof retentionOption === 'object' && retentionOption?.intervalMs ? retentionOption.intervalMs : 24 * 60 * 60 * 1000),
|
|
72
|
+
runOnStart: (typeof retentionOption === 'object' && retentionOption?.runOnStart !== undefined)
|
|
73
|
+
? !!retentionOption.runOnStart
|
|
74
|
+
: (options.cleanupOnStart !== undefined ? !!options.cleanupOnStart : true),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
let cleanupInterval = null;
|
|
78
|
+
let cleanupStartTimeout = null;
|
|
79
|
+
|
|
80
|
+
async function runCleanup() {
|
|
81
|
+
try {
|
|
82
|
+
return await purgeAuditLogs(knex, {
|
|
83
|
+
tableName,
|
|
84
|
+
days: retentionConfig.days,
|
|
85
|
+
});
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
88
|
+
console.warn('[audit-log] Failed to run retention cleanup:', err.message);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function startCleanup(customIntervalMs) {
|
|
94
|
+
stopCleanup();
|
|
95
|
+
if (!retentionConfig.enabled) return;
|
|
96
|
+
if (typeof customIntervalMs === 'number' && customIntervalMs > 0) {
|
|
97
|
+
retentionConfig.intervalMs = customIntervalMs;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (retentionConfig.runOnStart) {
|
|
101
|
+
const delayMs = typeof options.cleanupStartDelayMs === 'number'
|
|
102
|
+
? options.cleanupStartDelayMs
|
|
103
|
+
: (process.env.NODE_ENV === 'test' ? 50 : 5000);
|
|
104
|
+
cleanupStartTimeout = setTimeout(() => {
|
|
105
|
+
runCleanup();
|
|
106
|
+
}, delayMs);
|
|
107
|
+
if (cleanupStartTimeout && cleanupStartTimeout.unref) {
|
|
108
|
+
cleanupStartTimeout.unref();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
cleanupInterval = setInterval(() => {
|
|
113
|
+
runCleanup();
|
|
114
|
+
}, retentionConfig.intervalMs);
|
|
115
|
+
if (cleanupInterval && cleanupInterval.unref) {
|
|
116
|
+
cleanupInterval.unref();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function stopCleanup() {
|
|
121
|
+
if (cleanupStartTimeout) {
|
|
122
|
+
clearTimeout(cleanupStartTimeout);
|
|
123
|
+
cleanupStartTimeout = null;
|
|
124
|
+
}
|
|
125
|
+
if (cleanupInterval) {
|
|
126
|
+
clearInterval(cleanupInterval);
|
|
127
|
+
cleanupInterval = null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
35
131
|
const handlers = createAuditLogHandlers({ knex, tableName });
|
|
36
132
|
|
|
37
133
|
return {
|
|
38
134
|
name: 'audit-log',
|
|
39
135
|
version: '1.0.0',
|
|
40
|
-
description: 'Audit trail for admin panel model CRUD API',
|
|
136
|
+
description: 'Audit trail for admin panel model CRUD API with retention cleanup',
|
|
41
137
|
dependencies: { 'admin-panel': '*' },
|
|
42
138
|
|
|
43
139
|
api: {
|
|
140
|
+
purge(opts = {}) {
|
|
141
|
+
return purgeAuditLogs(knex, {
|
|
142
|
+
tableName,
|
|
143
|
+
days: opts.days !== undefined ? opts.days : (opts.retentionDays !== undefined ? opts.retentionDays : retentionConfig.days),
|
|
144
|
+
olderThan: opts.olderThan,
|
|
145
|
+
...opts,
|
|
146
|
+
});
|
|
147
|
+
},
|
|
44
148
|
purgeAuditLogs: (kx, opts = {}) =>
|
|
45
|
-
purgeAuditLogs(kx || knex, { tableName: opts.tableName || tableName, olderThan: opts.olderThan }),
|
|
149
|
+
purgeAuditLogs(kx || knex, { tableName: opts.tableName || tableName, olderThan: opts.olderThan, days: opts.days, retentionDays: opts.retentionDays }),
|
|
150
|
+
startCleanup,
|
|
151
|
+
stopCleanup,
|
|
152
|
+
getRetentionConfig() {
|
|
153
|
+
return { ...retentionConfig };
|
|
154
|
+
},
|
|
46
155
|
queryLogs: (filters) => handlers.queryLogs(filters),
|
|
156
|
+
queryTimeline: (model, recordId) => handlers.queryTimeline(model, recordId),
|
|
157
|
+
getRecordTimeline: (model, recordId) => handlers.queryTimeline(model, recordId),
|
|
47
158
|
getMigrationTemplate: (name) => generateAuditLogsMigration(name || tableName),
|
|
48
159
|
},
|
|
49
160
|
|
|
50
161
|
register(ctx) {
|
|
51
162
|
const adminPath = adminPathOpt || '/_admin';
|
|
52
163
|
ctx.app.use(createAuditMiddleware({ knex, adminPath, tableName }));
|
|
164
|
+
|
|
165
|
+
if (retentionConfig.enabled && autoCleanupEnabled) {
|
|
166
|
+
startCleanup();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return () => {
|
|
170
|
+
stopCleanup();
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
dispose() {
|
|
175
|
+
stopCleanup();
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
shutdown() {
|
|
179
|
+
stopCleanup();
|
|
53
180
|
},
|
|
54
181
|
|
|
55
182
|
onRoutesReady(ctx) {
|
|
@@ -61,11 +188,12 @@ function auditLogPlugin(options = {}) {
|
|
|
61
188
|
|
|
62
189
|
adminApi.registerModule({
|
|
63
190
|
id: 'audit-log',
|
|
191
|
+
title: pageTitle,
|
|
64
192
|
|
|
65
193
|
pages: includeDefaultPage
|
|
66
194
|
? [{
|
|
67
195
|
id: 'audit-log',
|
|
68
|
-
title:
|
|
196
|
+
title: pageTitle,
|
|
69
197
|
path: '/audit-log',
|
|
70
198
|
icon: 'database',
|
|
71
199
|
component: generateAuditLogComponent({ apiPrefix }),
|
|
@@ -86,9 +214,31 @@ function auditLogPlugin(options = {}) {
|
|
|
86
214
|
prefix: apiPrefix,
|
|
87
215
|
routes: [
|
|
88
216
|
{ method: 'get', path: '', handler: handlers.listHandler },
|
|
217
|
+
{ method: 'get', path: '/timeline', handler: handlers.timelineHandler },
|
|
218
|
+
{ method: 'get', path: '/timeline/:model/:id', handler: handlers.timelineHandler },
|
|
219
|
+
{ method: 'post', path: '/purge', handler: handlers.purgeHandler },
|
|
89
220
|
],
|
|
90
221
|
},
|
|
91
222
|
});
|
|
223
|
+
|
|
224
|
+
if (registerRecordAction && typeof adminApi.registerAction === 'function') {
|
|
225
|
+
adminApi.registerAction('audit-timeline', {
|
|
226
|
+
id: 'audit-timeline',
|
|
227
|
+
label: 'Zaman Tüneli',
|
|
228
|
+
icon: 'clock',
|
|
229
|
+
color: 'blue',
|
|
230
|
+
models: '*',
|
|
231
|
+
handler: async (record, model) => {
|
|
232
|
+
const primaryKey = model?.primaryKey || 'id';
|
|
233
|
+
const id = record ? record[primaryKey] : null;
|
|
234
|
+
const modelName = model?.name || (typeof model === 'string' ? model : '');
|
|
235
|
+
return {
|
|
236
|
+
redirect: `/audit-log?model=${encodeURIComponent(modelName)}&recordId=${encodeURIComponent(id)}`,
|
|
237
|
+
message: `Zaman tüneli: ${modelName} #${id}`,
|
|
238
|
+
};
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
}
|
|
92
242
|
},
|
|
93
243
|
};
|
|
94
244
|
}
|
|
@@ -3,24 +3,74 @@
|
|
|
3
3
|
* @module plugins/audit-log/purge
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* @param {import('knex').Knex} knex
|
|
8
|
-
* @param {Object} options
|
|
10
|
+
* @param {Object} [options={}]
|
|
9
11
|
* @param {string} [options.tableName='audit_logs']
|
|
10
|
-
* @param {Date|string} options.olderThan - Rows with created_at strictly before this are removed
|
|
11
|
-
* @
|
|
12
|
+
* @param {Date|string|number} [options.olderThan] - Rows with created_at strictly before this are removed
|
|
13
|
+
* @param {number} [options.days] - Number of days of audit logs to keep
|
|
14
|
+
* @param {number} [options.retentionDays=90] - Alias for days
|
|
15
|
+
* @returns {Promise<number>} Deleted row count
|
|
12
16
|
*/
|
|
13
|
-
async function purgeAuditLogs(knex, options) {
|
|
17
|
+
async function purgeAuditLogs(knex, options = {}) {
|
|
18
|
+
if (!knex) {
|
|
19
|
+
throw new Error('purgeAuditLogs requires a database (knex) instance');
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
const tableName = options.tableName || 'audit_logs';
|
|
15
|
-
const olderThan = options.olderThan instanceof Date
|
|
16
|
-
? options.olderThan
|
|
17
|
-
: new Date(options.olderThan);
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
let olderThan;
|
|
25
|
+
if (options.olderThan !== undefined) {
|
|
26
|
+
olderThan = options.olderThan instanceof Date
|
|
27
|
+
? options.olderThan
|
|
28
|
+
: new Date(options.olderThan);
|
|
29
|
+
|
|
30
|
+
if (Number.isNaN(olderThan.getTime())) {
|
|
31
|
+
throw new Error('purgeAuditLogs: invalid olderThan date');
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
const rawDays = options.days !== undefined ? options.days : (options.retentionDays !== undefined ? options.retentionDays : 90);
|
|
35
|
+
const days = typeof rawDays === 'number' ? rawDays : Number(rawDays);
|
|
36
|
+
if (Number.isNaN(days) || days <= 0) {
|
|
37
|
+
throw new Error('purgeAuditLogs: days must be a positive number');
|
|
38
|
+
}
|
|
39
|
+
olderThan = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let hasTable = true;
|
|
43
|
+
if (knex.schema && typeof knex.schema.hasTable === 'function') {
|
|
44
|
+
try {
|
|
45
|
+
hasTable = await knex.schema.hasTable(tableName);
|
|
46
|
+
} catch {
|
|
47
|
+
hasTable = false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!hasTable) {
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const client = knex?.client?.config?.client;
|
|
56
|
+
const isSqlite = client === 'sqlite3' || client === 'better-sqlite3';
|
|
57
|
+
|
|
58
|
+
let query = knex(tableName);
|
|
59
|
+
if (isSqlite) {
|
|
60
|
+
const iso = olderThan.toISOString();
|
|
61
|
+
const cutoffMs = olderThan.getTime();
|
|
62
|
+
query = query.where((builder) => {
|
|
63
|
+
builder.whereRaw(
|
|
64
|
+
"((typeof(created_at) = 'integer' and created_at < ?) or (typeof(created_at) = 'text' and created_at < ?))",
|
|
65
|
+
[cutoffMs, iso]
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
} else {
|
|
69
|
+
query = query.where('created_at', '<', olderThan);
|
|
21
70
|
}
|
|
22
71
|
|
|
23
|
-
|
|
72
|
+
const deleted = await query.delete();
|
|
73
|
+
return Number(deleted) || 0;
|
|
24
74
|
}
|
|
25
75
|
|
|
26
76
|
module.exports = {
|