Skip to content

Commit f3f9bb2

Browse files
authored
define audit log schema + add login action (#1669)
* add audit log schema * add audit util + login audit log * move audit-log imports to top * Add test to ensure login succeeds even if audit logging fails + minor util and schema changes * change audit file names * fix audit import names * fix audit import names (2) * remove unnecessary audit details * move login audit log tests to correct file and update helper functions * remove async from audit log at login endpoint * fix merge conflicts * remove audit util function + fix file names
1 parent 4ac057b commit f3f9bb2

4 files changed

Lines changed: 163 additions & 35 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const mongoose = require('mongoose');
2+
const Schema = mongoose.Schema;
3+
const AuditLogActions = require('../util/auditLogActions');
4+
5+
const AuditLogSchema = new Schema(
6+
{
7+
userId: {
8+
type: Schema.Types.ObjectId,
9+
ref: 'User',
10+
required: true,
11+
},
12+
action: {
13+
type: String,
14+
enum: Object.keys(AuditLogActions),
15+
required: true,
16+
},
17+
documentId: {
18+
type: Schema.Types.ObjectId,
19+
default: null,
20+
},
21+
details: {
22+
type: Schema.Types.Mixed,
23+
default: {},
24+
},
25+
},
26+
{ timestamps: { createdAt: true, updatedAt: false } }
27+
);
28+
29+
module.exports = mongoose.model('AuditLog', AuditLogSchema);

api/main_endpoints/routes/Auth.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ const PASSWORD_RESET_EXPIRATION = require('../../util/constants').PASSWORD_RESET
3030
const { sendVerificationEmail, sendPasswordReset } = require('../util/emailHelpers');
3131
const { userWithEmailExists, checkIfPageCountResets, findPasswordReset } = require('../util/userHelpers');
3232

33+
const AuditLogActions = require('../util/auditLogActions.js');
34+
const AuditLog = require('../models/AuditLog.js');
35+
3336
// Register a member
3437
router.post('/register', async (req, res) => {
3538
const registrationStatus = await registerUser(req.body);
@@ -177,6 +180,13 @@ router.post('/login', function(req, res) {
177180
const token = jwt.sign(
178181
userToBeSigned, config.secretKey, jwtOptions
179182
);
183+
// Create audit log on successful sign-in
184+
AuditLog.create({
185+
userId: user._id,
186+
action: AuditLogActions.LOG_IN,
187+
details: { email: user.email }
188+
}).catch(logger.error);
189+
180190
res.json({ token: 'JWT ' + token });
181191
})
182192
.catch((error) => {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const AuditLogActions = {
2+
LOG_IN: 'LOG_IN',
3+
UPDATE_USER: 'UPDATE_USER',
4+
PRINT_PAGE: 'PRINT_PAGE',
5+
SIGN_UP: 'SIGN_UP',
6+
VERIFY_EMAIL: 'VERIFY_EMAIL',
7+
EMAIL_SENT: 'EMAIL_SENT',
8+
CHANGE_PW: 'CHANGE_PW',
9+
RESET_PW: 'RESET_PW',
10+
};
11+
12+
module.exports = AuditLogActions;

test/api/Auth.js

Lines changed: 112 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,11 @@ const {
3434
const { checkIfPageCountResets } = require('../../api/main_endpoints/util/userHelpers.js');
3535
const { mockDayMonthAndYear, revertClock } = require('../util/mocks/Date.js');
3636
const { MEMBERSHIP_STATE } = require('../../api/util/constants');
37-
chai.should();
3837

38+
const AuditLogActions = require('../../api/main_endpoints/util/auditLogActions.js');
39+
const AuditLog = require('../../api/main_endpoints/models/AuditLog.js');
40+
41+
chai.should();
3942
chai.use(chaiHttp);
4043

4144
// Our parent block
@@ -141,39 +144,6 @@ describe('Auth', () => {
141144
expect(verificationArgs).to.eql([user.firstName + ' ' + user.lastName, user.email]);
142145
expect(result).to.have.status(OK);
143146
});
144-
});
145-
146-
describe('/POST login', () => {
147-
it('Should return statusCode 400 if an email and/or ' +
148-
'password is not provided', async () => {
149-
const user = {};
150-
const result = await test.sendPostRequest(
151-
'/api/Auth/login', user);
152-
expect(result).to.have.status(BAD_REQUEST);
153-
});
154-
155-
it('Should return statusCode 401 if an email/pass combo ' +
156-
'does not match a record in the DB', async () => {
157-
const user = {
158-
email: 'nota@b.c',
159-
password: 'Passwd'
160-
};
161-
const result = await test.sendPostRequest(
162-
'/api/Auth/login', user);
163-
expect(result).to.have.status(UNAUTHORIZED);
164-
});
165-
166-
it('Should return statusCode 401 if the email exists ' +
167-
'but password is incorrect', async () => {
168-
const user = {
169-
email: 'a@b.c',
170-
password: 'password'
171-
};
172-
const result = await test.sendPostRequest(
173-
'/api/Auth/login', user);
174-
expect(result).to.have.status(UNAUTHORIZED);
175-
});
176-
177147
it('Should allow login with correct credentials after registering a user', async () => {
178148
const user = {
179149
email: 'logintest@gmail.com',
@@ -228,6 +198,114 @@ describe('Auth', () => {
228198
});
229199
});
230200

201+
describe('/POST login', () => {
202+
it('Should return statusCode 400 if an email and/or ' +
203+
'password is not provided', async () => {
204+
const user = {};
205+
const result = await test.sendPostRequest(
206+
'/api/Auth/login', user);
207+
expect(result).to.have.status(BAD_REQUEST);
208+
});
209+
210+
it('Should return statusCode 401 if an email/pass combo ' +
211+
'does not match a record in the DB', async () => {
212+
const user = {
213+
email: 'nota@b.c',
214+
password: 'Passwd'
215+
};
216+
const result = await test.sendPostRequest(
217+
'/api/Auth/login', user);
218+
expect(result).to.have.status(UNAUTHORIZED);
219+
});
220+
221+
it('Should return statusCode 401 if the email exists ' +
222+
'but password is incorrect', async () => {
223+
const user = {
224+
email: 'a@b.c',
225+
password: 'password'
226+
};
227+
const result = await test.sendPostRequest(
228+
'/api/Auth/login', user);
229+
expect(result).to.have.status(UNAUTHORIZED);
230+
});
231+
232+
describe('with an existing user', () => {
233+
let user;
234+
235+
before(async () => {
236+
user = new User({
237+
_id: new mongoose.Types.ObjectId(),
238+
firstName: 'Test',
239+
lastName: 'User',
240+
email: 'logintest@gmail.com',
241+
password: 'Passw0rd',
242+
emailVerified: true,
243+
accessLevel: MEMBERSHIP_STATE.MEMBER,
244+
apiKey: null
245+
});
246+
await user.save();
247+
});
248+
249+
after(async () => {
250+
await User.deleteOne({ email: 'logintest@gmail.com' });
251+
});
252+
253+
beforeEach(async () => {
254+
await AuditLog.deleteMany({});
255+
});
256+
257+
afterEach(async () => {
258+
await AuditLog.deleteMany({});
259+
sinon.restore();
260+
});
261+
262+
it('Should create an audit log entry on successful login', async () => {
263+
const loginPayload = {
264+
email: 'logintest@gmail.com',
265+
password: 'Passw0rd',
266+
};
267+
268+
const res = await test.sendPostRequest('/api/Auth/login', loginPayload);
269+
expect(res).to.have.status(OK);
270+
expect(res.body).to.have.property('token');
271+
272+
const auditEntry = await AuditLog.findOne({
273+
action: AuditLogActions.LOG_IN,
274+
details: {email: loginPayload.email},
275+
});
276+
277+
expect(auditEntry).to.exist;
278+
expect(auditEntry).to.have.property('userId');
279+
expect(auditEntry.details).to.have.property('email', loginPayload.email);
280+
});
281+
282+
it('Should return 200 even if audit logging fails', async () => {
283+
const auditStub = sinon.stub(AuditLog, 'create').rejects(new Error('Simulated audit log failure'));
284+
285+
const loginPayload = {
286+
email: 'logintest@gmail.com',
287+
password: 'Passw0rd',
288+
};
289+
290+
try {
291+
292+
const res = await test.sendPostRequest('/api/Auth/login', loginPayload);
293+
expect(res).to.have.status(OK);
294+
expect(res.body).to.have.property('token');
295+
296+
const auditEntry = await AuditLog.findOne({
297+
action: AuditLogActions.LOG_IN,
298+
'details.email': loginPayload.email,
299+
});
300+
301+
expect(auditEntry).to.not.exist;
302+
} finally {
303+
auditStub.restore();
304+
}
305+
});
306+
});
307+
});
308+
231309
describe('/POST sendPasswordReset', () => {
232310
it('Should return statusCode 401 if the email is invalid', async () => {
233311
const data = {
@@ -427,5 +505,4 @@ describe('Auth', () => {
427505
expect(result).to.be.false;
428506
});
429507
});
430-
431508
});

0 commit comments

Comments
 (0)