eventlog: add params for from and to date

This commit is contained in:
Girish Ramakrishnan
2026-02-16 14:42:37 +01:00
parent aab20fd23e
commit 81659d4bf2
9 changed files with 145 additions and 26 deletions
+9 -4
View File
@@ -1466,14 +1466,19 @@ async function listBackups(app, page, perPage) {
return await backups.listByIdentifierAndStatePaged(app.id, backups.BACKUP_STATE_NORMAL, page, perPage);
}
async function listEventlog(app, page, perPage) {
async function listEventlog(app, filter, page, perPage) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof filter, 'object');
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
const actions = [];
const search = app.id;
return await eventlog.listPaged(actions, search, page, perPage);
const fullFilter = {
actions: [],
search: app.id,
from: filter.from,
to: filter.to
};
return await eventlog.listPaged(fullFilter, page, perPage);
}
async function drainStream(stream) {
-1
View File
@@ -290,7 +290,6 @@ async function del(domain, auditSource) {
];
const [error, results] = await safe(database.transaction(queries));
console.dir(error);
if (error && error.sqlCode === 'ER_ROW_IS_REFERENCED_2') {
if (error.message.includes('mailboxes_aliasDomain_constraint')) throw new BoxError(BoxError.CONFLICT, 'Domain is in use in a mailbox, list or an alias');
if (error.message.includes('mailboxes_domain_constraint')) throw new BoxError(BoxError.CONFLICT, 'Domain is in use in a mailbox, list or an alias');
+24 -14
View File
@@ -76,28 +76,38 @@ async function getActivationEvent() {
return postProcess(result[0]);
}
async function listPaged(actions, search, page, perPage) {
async function listPaged(filter, page, perPage) {
const { actions, search, from = null, to = null } = filter;
assert(Array.isArray(actions));
assert(typeof search === 'string' || search === null);
assert(from === null || from instanceof Date, 'from must be a Date or null');
assert(to === null || to instanceof Date, 'to must be a Date or null');
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
const data = [];
const conditions = [], data = [];
if (search) conditions.push('(sourceJson LIKE ' + mysql.escape('%' + search + '%') + ' OR dataJson LIKE ' + mysql.escape('%' + search + '%') + ')');
if (actions.length) {
const actionConds = actions.map((a) => ' (action LIKE ' + mysql.escape(`%${a}%`) + ') ').join(' OR ');
conditions.push('( ' + actionConds + ' )');
}
if (from) {
conditions.push('creationTime >= ?');
data.push(from);
}
if (to) {
conditions.push('creationTime <= ?');
data.push(to);
}
let query = `SELECT ${EVENTLOG_FIELDS} FROM eventlog`;
if (actions.length || search) query += ' WHERE';
if (search) query += ' (sourceJson LIKE ' + mysql.escape('%' + search + '%') + ' OR dataJson LIKE ' + mysql.escape('%' + search + '%') + ')';
if (actions.length && search) query += ' AND ( ';
actions.forEach(function (action, i) {
query += ' (action LIKE ' + mysql.escape(`%${action}%`) + ') ';
if (i < actions.length-1) query += ' OR ';
});
if (actions.length && search) query += ' ) ';
if (conditions.length) query += ' WHERE ' + conditions.join(' AND ');
query += ' ORDER BY creationTime DESC LIMIT ?,?';
data.push((page-1)*perPage);
data.push((page - 1) * perPage);
data.push(perPage);
const results = await database.query(query, data);
+16 -1
View File
@@ -1,4 +1,5 @@
import apps from '../apps.js';
import validator from '../validator.js';
import appstore from '../appstore.js';
import assert from 'node:assert';
import AuditSource from '../auditsource.js';
@@ -993,7 +994,21 @@ async function listEventlog(req, res, next) {
const perPage = typeof req.query.per_page === 'string'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
const [error, eventlogs] = await safe(apps.listEventlog(req.resources.app, page, perPage));
if (req.query.from) {
if (typeof req.query.from !== 'string') return next(new HttpError(400, 'from must be an ISO 8601 datetime string'));
if (!validator.isIsoDate(req.query.from)) return next(new HttpError(400, 'from must be a valid ISO 8601 datetime string'));
}
if (req.query.to) {
if (typeof req.query.to !== 'string') return next(new HttpError(400, 'to must be an ISO 8601 datetime string'));
if (!validator.isIsoDate(req.query.to)) return next(new HttpError(400, 'to must be a valid ISO 8601 datetime string'));
}
const filter = {
from: req.query.from ? new Date(req.query.from) : null,
to: req.query.to ? new Date(req.query.to) : null
};
const [error, eventlogs] = await safe(apps.listEventlog(req.resources.app, filter, page, perPage));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { eventlogs }));
+21 -1
View File
@@ -1,5 +1,6 @@
import BoxError from '../boxerror.js';
import eventlog from '../eventlog.js';
import validator from '../validator.js';
import { HttpError } from '@cloudron/connect-lastmile';
import { HttpSuccess } from '@cloudron/connect-lastmile';
import safe from 'safetydance';
@@ -24,10 +25,29 @@ async function list(req, res, next) {
if (req.query.action && typeof req.query.action !== 'string') return next(new HttpError(400, 'action must be a string'));
if (req.query.search && typeof req.query.search !== 'string') return next(new HttpError(400, 'search must be a string'));
if (req.query.from) {
if (typeof req.query.from !== 'string') return next(new HttpError(400, 'from must be an ISO 8601 datetime string'));
if (!validator.isIsoDate(req.query.from)) return next(new HttpError(400, 'from must be a valid ISO 8601 datetime string'));
}
if (req.query.to) {
if (typeof req.query.to !== 'string') return next(new HttpError(400, 'to must be an ISO 8601 datetime string'));
if (!validator.isIsoDate(req.query.to)) return next(new HttpError(400, 'to must be a valid ISO 8601 datetime string'));
}
const actions = typeof req.query.actions === 'string' ? req.query.actions.split(',').map(function (s) { return s.trim(); }) : [];
if (req.query.action) actions.push(req.query.action);
const [error, eventlogs] = await safe(eventlog.listPaged(actions, req.query.search || null, page, perPage));
if (req.query.to && !validator.isIsoDate(req.query.to)) return next(new HttpError(400, 'to must be a valid ISO 8601 datetime string'));
const filter = {
actions,
search: req.query.search || null,
from: req.query.from ? new Date(req.query.from) : null,
to: req.query.to ? new Date(req.query.to) : null
};
const [error, eventlogs] = await safe(eventlog.listPaged(filter, page, perPage));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { eventlogs }));
+26
View File
@@ -118,5 +118,31 @@ describe('Eventlog API', function () {
expect(response.status).to.equal(200);
expect(response.body.eventlogs.length).to.equal(0);
});
it('succeeds with from and to', async function () {
const from = new Date(Date.now() - 86400000).toISOString(); // 1 day ago
const to = new Date(Date.now() + 86400000).toISOString(); // 1 day from now
const response = await superagent.get(`${serverUrl}/api/v1/eventlog`)
.query({ access_token: owner.token, page: 1, per_page: 10, from, to });
expect(response.status).to.equal(200);
expect(response.body.eventlogs).to.be.an('array');
});
it('fails with invalid from', async function () {
const response = await superagent.get(`${serverUrl}/api/v1/eventlog`)
.query({ access_token: owner.token, page: 1, per_page: 10, from: 'not-a-date' })
.ok(() => true);
expect(response.status).to.equal(400);
});
it('fails with invalid to', async function () {
const response = await superagent.get(`${serverUrl}/api/v1/eventlog`)
.query({ access_token: owner.token, page: 1, per_page: 10, to: 'invalid' })
.ok(() => true);
expect(response.status).to.equal(400);
});
});
});
+24 -4
View File
@@ -47,7 +47,7 @@ describe('Eventlog', function () {
});
it('listPaged succeeds', async function () {
const results = await eventlog.listPaged([], null, 1, 1);
const results = await eventlog.listPaged({ actions: [], search: null }, 1, 1);
expect(results).to.be.an(Array);
expect(results.length).to.be(1);
@@ -58,7 +58,7 @@ describe('Eventlog', function () {
});
it('listPaged succeeds with source search', async function () {
const results = await eventlog.listPaged([], '1.2.3.4', 1, 1);
const results = await eventlog.listPaged({ actions: [], search: '1.2.3.4' }, 1, 1);
expect(results).to.be.an(Array);
expect(results.length).to.be(1);
expect(results[0].id).to.be(eventId);
@@ -68,7 +68,7 @@ describe('Eventlog', function () {
});
it('listPaged succeeds with data search', async function () {
const results = await eventlog.listPaged([], 'thatapp', 1, 1);
const results = await eventlog.listPaged({ actions: [], search: 'thatapp' }, 1, 1);
expect(results).to.be.an(Array);
expect(results.length).to.be(1);
expect(results[0].id).to.be(eventId);
@@ -77,6 +77,26 @@ describe('Eventlog', function () {
expect(results[0].data).to.be.eql({ appId: 'thatapp' });
});
it('listPaged succeeds with from/to time filter', async function () {
const event = await eventlog.get(eventId);
const creationTime = event.creationTime;
const from = new Date(creationTime.getTime() - 60000); // 1 min before
const to = new Date(creationTime.getTime() + 60000); // 1 min after
const results = await eventlog.listPaged({ actions: [], search: null, from, to }, 1, 10);
expect(results).to.be.an(Array);
expect(results.length).to.be.above(0);
expect(results.some((r) => r.id === eventId)).to.be(true);
});
it('listPaged returns empty with from after event', async function () {
const event = await eventlog.get(eventId);
const from = new Date(event.creationTime.getTime() + 86400000); // 1 day after
const results = await eventlog.listPaged({ actions: [], search: null, from }, 1, 10);
expect(results.some((r) => r.id === eventId)).to.be(false);
});
let loginEventId;
it('upsert with no existing entry succeeds', async function () {
const result = await eventlog.upsertLoginEvent('user.login', { ip: '1.2.3.4' }, { appId: 'thatapp' });
@@ -121,7 +141,7 @@ describe('Eventlog', function () {
const id = await eventlog.add(eventlog.ACTION_USER_LOGIN, { ip: '1.2.3.4' }, { appId: 'thatapp' });
await eventlog.cleanup({ creationTime: new Date(Date.now() - 1000) }); // 1 second ago
let results = await eventlog.listPaged([], null, 1, 100);
let results = await eventlog.listPaged({ actions: [], search: null }, 1, 100);
expect(results.length).to.be(1);
expect(results[0].id).to.be(id);
+16
View File
@@ -28,4 +28,20 @@ describe('Validator', function () {
for (const badEmail of badEmails) {
it(`isEmail returns false ${badEmail}`, () => expect(validator.isEmail(badEmail)).to.be(false));
}
describe('isIsoDate', function () {
it('returns true for valid ISO string', function () {
expect(validator.isIsoDate('2024-01-15T10:30:00.000Z')).to.be(true);
});
it('returns false for invalid string', function () {
expect(validator.isIsoDate('not-a-date')).to.be(false);
});
it('returns false for empty or non-string', function () {
expect(validator.isIsoDate('')).to.be(false);
expect(validator.isIsoDate(null)).to.be(false);
expect(validator.isIsoDate(undefined)).to.be(false);
});
});
});
+9 -1
View File
@@ -10,6 +10,14 @@ function isEmail(email) {
return emailRegex.test(email);
}
function isIsoDate(value) {
assert.strictEqual(typeof value, 'string');
const date = new Date(value);
return !Number.isNaN(date.getTime());
}
export default {
isEmail
isEmail,
isIsoDate
};