eventlog: add params for from and to date
This commit is contained in:
+9
-4
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 }));
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user