Skip to content

Commit

Permalink
feat(authentication-service): filtering option in activity logs (#2049)
Browse files Browse the repository at this point in the history
* chore(authentication-service): rebase

gh-1517

* feat(authentication-service): rebase

gh-1517

* feat(authentication-service): add test cases for login activity controller

gh-1517

* feat(authentication-service): test cases for login activity controller

gh-1517
  • Loading branch information
yeshamavani authored Mar 22, 2024
1 parent d849562 commit aa60b14
Show file tree
Hide file tree
Showing 12 changed files with 367 additions and 11 deletions.
33 changes: 33 additions & 0 deletions services/authentication-service/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@
"type": "string",
"format": "date-time"
}
},
{
"name": "filter",
"in": "query",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
],
"operationId": "LoginActivityController.getActiveUsers"
Expand Down Expand Up @@ -2296,6 +2308,27 @@
"additionalProperties": false
},
"Date": {},
"ActiveUsersFilter": {
"title": "ActiveUsersFilter",
"type": "object",
"properties": {
"inclusion": {
"type": "boolean"
},
"userIdentity": {
"type": "string"
},
"userIdentifier": {
"type": "object"
}
},
"required": [
"inclusion",
"userIdentity",
"userIdentifier"
],
"additionalProperties": false
},
"loopback.Count": {
"type": "object",
"title": "loopback.Count",
Expand Down
27 changes: 27 additions & 0 deletions services/authentication-service/openapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ fetch('/active-users/{range}',
|range|path|string|true|none|
|startDate|query|string(date-time)|false|none|
|endDate|query|string(date-time)|false|none|
|filter|query|object|false|none|

> Example responses
Expand Down Expand Up @@ -4582,6 +4583,32 @@ null

*None*

<h2 id="tocS_ActiveUsersFilter">ActiveUsersFilter</h2>
<!-- backwards compatibility -->
<a id="schemaactiveusersfilter"></a>
<a id="schema_ActiveUsersFilter"></a>
<a id="tocSactiveusersfilter"></a>
<a id="tocsactiveusersfilter"></a>

```json
{
"inclusion": true,
"userIdentity": "string",
"userIdentifier": {}
}

```

ActiveUsersFilter

### Properties

|Name|Type|Required|Restrictions|Description|
|---|---|---|---|---|
|inclusion|boolean|true|none|none|
|userIdentity|string|true|none|none|
|userIdentifier|object|true|none|none|

<h2 id="tocS_loopback.Count">loopback.Count</h2>
<!-- backwards compatibility -->
<a id="schemaloopback.count"></a>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import {Client, expect} from '@loopback/testlab';
import {LoginType} from '../../enums';
import {LoginActivityRepository} from '../../repositories';
import {TestingApplication} from '../fixtures/application';
import {token} from '../fixtures/datasources/userCredsAndPermission';
import {setupApplication} from './test-helper';

describe('', () => {
let app: TestingApplication;
let client: Client;
let loginActivityRepo: LoginActivityRepository;
before('setupApplication', async () => {
({app, client} = await setupApplication());
});
after(async () => app.stop());
before(givenLoginActivityRepository);
before(setMockData);
after(deleteMockData);
afterEach(() => {
delete process.env.JWT_ISSUER;
delete process.env.JWT_SECRET;
delete process.env.ENCRYPTION_KEY;
});
const basePath = '/login-activity';
const range =
'startDate=2024-03-01T00:00:00.596Z&endDate=2024-04-30T23:59:59.596Z';
const rangeWithFilter =
'startDate=2024-03-01T00:00:00.596Z&endDate=2024-04-30T23:59:59.596Z&filter={"userIdentity":"actorId","inclusion":true,"userIdentifier":["user1"]}';

it('throws error when token is not passed', async () => {
const response = await client.get(`/login-activity/count`).send();
expect(response.error).to.have.property('status').to.be.equal(401);
});

it('returns the count with status code 200', async () => {
await client
.get(`${basePath}/count`)
.set('authorization', `Bearer ${token}`)
.send()
.expect(200);
});

it('returns all the data when no filter passed', async () => {
const response = await client
.get(`${basePath}`)
.set('authorization', `Bearer ${token}`)
.send()
.expect(200);
expect(response.body.length).to.be.greaterThan(0);
});

it('returns single entry matching the id passed in filter', async () => {
const response = await client
.get(`${basePath}/1`)
.set('authorization', `Bearer ${token}`)
.send()
.expect(200);
expect(response.body).to.have.property('id').to.be.equal(`1`);
});

it('returns all the daily active users when no filter is passed ', async () => {
await client
.get(`/active-users/daily?${range}`)
.set('authorization', `Bearer ${token}`)
.send()
.expect(200);
});

it('returns all the monthly active users when no filter is passed ', async () => {
await client
.get(`/active-users/monthly?${range}`)
.set('authorization', `Bearer ${token}`)
.send()
.expect(200);
});

it('returns only those daily active users that satisfy the passed filter', async () => {
await client
.get(`/active-users/daily?${rangeWithFilter}`)
.set('authorization', `Bearer ${token}`)
.send()
.expect(200);
});

it('returns only those monthly active users that satisfy the passed filter', async () => {
await client
.get(`/active-users/monthly?${rangeWithFilter}`)
.set('authorization', `Bearer ${token}`)
.send()
.expect(200);
});

async function givenLoginActivityRepository() {
loginActivityRepo = await app.getRepository(LoginActivityRepository);
}

async function deleteMockData() {
await loginActivityRepo.deleteAll();
}
async function setMockData() {
const tokenPayload = 'encrypted payload';
await loginActivityRepo.createAll([
{
id: '1',
actor: 'user1',
tenantId: 'tenant1',
loginTime: new Date('2024-03-22T13:28:51.596Z'),
tokenPayload,
loginType: LoginType.ACCESS,
deviceInfo: 'chrome',
ipAddress: 'ipaddress',
},
{
id: '2',
actor: 'user1',
tenantId: 'tenant1',
loginTime: new Date('2024-03-22T14:28:51.596Z'),
tokenPayload,
loginType: LoginType.RELOGIN,
deviceInfo: 'chrome',
ipAddress: 'ipaddress',
},
{
id: '3',
actor: 'user1',
tenantId: 'tenant1',
loginTime: new Date('2024-03-22T15:28:51.596Z'),
tokenPayload,
loginType: LoginType.LOGOUT,
deviceInfo: 'chrome',
ipAddress: 'ipaddress',
},
{
id: '4',
actor: 'user1',
tenantId: 'tenant1',
loginTime: new Date('2024-03-23T13:28:51.596Z'),
tokenPayload,
loginType: LoginType.ACCESS,
deviceInfo: 'chrome',
ipAddress: 'ipaddress',
},
{
id: '5',
actor: 'user1',
tenantId: 'tenant1',
loginTime: new Date('2024-04-22T13:28:51.596Z'),
tokenPayload,
loginType: LoginType.ACCESS,
deviceInfo: 'chrome',
ipAddress: 'ipaddress',
},
]);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as jwt from 'jsonwebtoken';
import {PermissionKey} from '../../../enums';

process.env.JWT_ISSUER = 'test';
process.env.JWT_SECRET = 'test';
//User Creds
const User = {
id: 1,
username: 'test_user',
password: 'test_password',
};
export const testUserPayload = {
...User,
role: 1,
authClientId: 2,
authClientIds: [2],
deleted: false,
userLocked: false,
permissions: [PermissionKey.ViewLoginActivity],
};
export const token = jwt.sign(testUserPayload, process.env.JWT_SECRET, {
expiresIn: 180000,
issuer: process.env.JWT_ISSUER,
});
13 changes: 11 additions & 2 deletions services/authentication-service/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,11 @@ import {UserValidationProvider} from './providers/user-validation.provider';
import {repositories} from './repositories/index';
import {repositories as sequelizeRepositories} from './repositories/sequelize';
import {MySequence} from './sequence';
import {LoginHelperService, OtpService} from './services';
import {
ActiveUserFilterBuilderService,
LoginHelperService,
OtpService,
} from './services';
import {IAuthServiceConfig, IMfaConfig, IOtpConfig} from './types';

export class AuthenticationServiceComponent implements Component {
Expand Down Expand Up @@ -179,11 +183,16 @@ export class AuthenticationServiceComponent implements Component {
.bind('services.LoginHelperService')
.toClass(LoginHelperService);
this.application.bind('services.otpService').toClass(OtpService);
this.application.bind(AuthServiceBindings.ActorIdKey).to('userId');

//set the userActivity to false by default
this.application
.bind(AuthServiceBindings.MarkUserActivity)
.to({markUserActivity: false});
this.models = models;
this.application
.bind('services.ActiveUserFilterBuilderService')
.toClass(ActiveUserFilterBuilderService);
this.application.bind(AuthServiceBindings.ActorIdKey).to('userId');

this.controllers = controllers;
}
Expand Down
12 changes: 6 additions & 6 deletions services/authentication-service/src/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,29 @@
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
import {ForgetPasswordController} from './forget-password.controller';
import {SignupRequestController} from './signup-request.controller';
import {LoginActivityController} from './login-activity.controller';
import {
AppleLoginController,
AzureLoginController,
CognitoLoginController,
FacebookLoginController,
GoogleLoginController,
InstagramLoginController,
KeycloakLoginController,
LoginController,
LogoutController,
OtpController,
AzureLoginController,
CognitoLoginController,
SamlLoginController,
} from '../modules/auth/controllers';
import {ForgetPasswordController} from './forget-password.controller';
import {LoginActivityController} from './login-activity.controller';
import {SignupRequestController} from './signup-request.controller';

export * from '../modules/auth/controllers/login.controller';
export * from '../modules/auth/controllers/logout.controller';
export * from '../modules/auth/controllers/otp.controller';
export * from './forget-password.controller';
export * from './signup-request.controller';
export * from './login-activity.controller';
export * from './signup-request.controller';

export const controllers = [
LoginController,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {inject} from '@loopback/core';
import {
Count,
CountSchema,
Expand All @@ -21,15 +22,18 @@ import {STRATEGY, authenticate} from 'loopback4-authentication';
import {authorize} from 'loopback4-authorization';
import moment from 'moment';
import {ActiveUsersRange, PermissionKey} from '../enums';
import {LoginActivity} from '../models';
import {ActiveUsersFilter, LoginActivity} from '../models';
import {LoginActivityRepository} from '../repositories';
import {ActiveUserFilterBuilderService} from '../services';
import {ActiveUsersGroupData} from '../types';

const baseUrl = '/login-activity';
export class LoginActivityController {
constructor(
@repository(LoginActivityRepository)
private readonly loginActivityRepo: LoginActivityRepository,
@inject('services.ActiveUserFilterBuilderService')
private readonly filterBuilder: ActiveUserFilterBuilderService,
) {}

@authenticate(STRATEGY.BEARER, {
Expand Down Expand Up @@ -134,10 +138,17 @@ export class LoginActivityController {
@param.query.dateTime('startDate')
startDate: Date,
@param.query.dateTime('endDate') endDate: Date,
@param.query.object('filter')
filter?: ActiveUsersFilter,
): Promise<ActiveUsersGroupData> {
let optionalWhere = {};
if (filter) {
optionalWhere = await this.filterBuilder.buildActiveUsersFilter(filter);
}
const activeUsersForTime = await this.loginActivityRepo.find({
where: {
loginTime: {between: [startDate, endDate]},
...optionalWhere,
},
});

Expand Down
5 changes: 5 additions & 0 deletions services/authentication-service/src/enums/login-type.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ export const enum ActiveUsersRange {
DAILY = 'daily',
MONTHLY = 'monthly',
}

export const enum UserIdentity {
ACTOR_ID = 'actorId',
USER_NAME = 'userName',
}
Loading

0 comments on commit aa60b14

Please sign in to comment.