Skip to content

Commit

Permalink
feat: implemented user activity tracker middleware & tests, connected…
Browse files Browse the repository at this point in the history
… middleware to app
  • Loading branch information
chesterkmr committed Jun 28, 2023
1 parent 08428bb commit 916021e
Show file tree
Hide file tree
Showing 9 changed files with 336 additions and 189 deletions.
352 changes: 168 additions & 184 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions services/workflows-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"@types/supertest": "2.0.11",
"@typescript-eslint/eslint-plugin": "^5.54.1",
"@typescript-eslint/parser": "^5.54.1",
"dayjs": "^1.11.6",
"dotenv": "^16.0.3",
"eslint": "^8.35.0",
"eslint-config-prettier": "^8.7.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "lastActiveAt" TIMESTAMP(3);
1 change: 1 addition & 0 deletions services/workflows-service/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ model User {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lastActiveAt DateTime?
workflowRuntimeData WorkflowRuntimeData[]
}

Expand Down
3 changes: 2 additions & 1 deletion services/workflows-service/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { LogRequestInterceptor } from '@/common/interceptors/log-request.interce
import { AppLoggerModule } from '@/common/app-logger/app-logger.module';
import { ClsModule } from 'nestjs-cls';
import { FiltersModule } from '@/common/filters/filters.module';
import { UserActivityTrackerMiddleware } from '@/common/middlewares/user-activity-tracker.middleware';

@Module({
controllers: [],
Expand Down Expand Up @@ -76,6 +77,6 @@ import { FiltersModule } from '@/common/filters/filters.module';
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(RequestIdMiddleware).forRoutes('*');
consumer.apply(RequestIdMiddleware, UserActivityTrackerMiddleware).forRoutes('*');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { AppLoggerService } from '@/common/app-logger/app-logger.service';
import { UserService } from '@/user/user.service';
import { Injectable, NestMiddleware } from '@nestjs/common';
import { User } from '@prisma/client';
import { Request, Response } from 'express';

@Injectable()
export class UserActivityTrackerMiddleware implements NestMiddleware {
private FIVE_MINUTES_IN_MS = 1000 * 60 * 5;
UPDATE_INTERVAL = this.FIVE_MINUTES_IN_MS;

constructor(
private readonly logger: AppLoggerService,
private readonly userService: UserService,
) {}

async use(req: Request, res: Response, next: (error?: any) => void) {
if (req.session && req.user) {
if (this.isUpdateCanBePerformed((req.user as User).lastActiveAt)) {
await this.trackUserActivity(req.user as User);
}
}

next();
}

private isUpdateCanBePerformed(
lastUpdate: Date | null,
updateIntervalInMs: number = this.UPDATE_INTERVAL,
) {
if (!lastUpdate) return true;

const now = Date.now();
const pastDate = Number(new Date(lastUpdate));

return now - pastDate >= updateIntervalInMs;
}

private async trackUserActivity(user: User, activeDate = new Date()) {
this.logger.log(`Updating activity`, { userId: user.id });
await this.userService.updateById(user.id, { data: { lastActiveAt: activeDate } });
this.logger.log(`Updated activity`, { userId: user.id });
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { AppLoggerService } from '@/common/app-logger/app-logger.service';
import { UserActivityTrackerMiddleware } from '@/common/middlewares/user-activity-tracker.middleware';
import { PrismaModule } from '@/prisma/prisma.module';
import { commonTestingModules } from '@/test/helpers/nest-app-helper';
import { UserModule } from '@/user/user.module';
import { UserService } from '@/user/user.service';
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { User } from '@prisma/client';
import { Request, Response } from 'express';
import dayjs from 'dayjs';

describe('UserActivityTrackerMiddleware', () => {
const testUserPayload = {
firstName: 'Test',
lastName: 'User',
password: '',
email: 'example@mail.com',
roles: [],
} as unknown as User;
let moduleRef: TestingModule;
let app: INestApplication;
let testUser: User;
let middleware: UserActivityTrackerMiddleware;
let userService: UserService;
let callback: jest.Mock;

beforeEach(async () => {
moduleRef = await Test.createTestingModule({
imports: [UserModule, PrismaModule, ...commonTestingModules],
}).compile();
app = moduleRef.createNestApplication();
middleware = new UserActivityTrackerMiddleware(app.get(AppLoggerService), app.get(UserService));
userService = app.get(UserService);
callback = jest.fn(() => null);
});

describe('when request not includes session and user', () => {
it('will call callback', async () => {
await middleware.use({} as Request, {} as Response, callback);

expect(callback).toHaveBeenCalledTimes(1);
});
});

describe('when session and user in request', () => {
describe('when lastActiveAt unset', () => {
beforeEach(async () => {
testUser = await app.get(UserService).create({ data: testUserPayload as any });
});

afterEach(async () => {
await app.get(UserService).deleteById(testUser.id);
});

it('will be set on middleware call', async () => {
await middleware.use(
{ user: testUser, session: testUser } as any,
{} as Response,
callback,
);

const updatedUser = await app.get(UserService).getById(testUser.id);

expect(updatedUser.lastActiveAt).toBeTruthy();
expect(callback).toHaveBeenCalledTimes(1);
});
});

describe('when lastActiveAt is set', () => {
beforeEach(async () => {
testUser = await app.get(UserService).create({ data: testUserPayload as any });
app.use(middleware.use.bind(middleware));
});

afterEach(async () => {
await app.get(UserService).deleteById(testUser.id);
});

it('will not be changed when lastActiveAt not expired', async () => {
const nonExpiredDate = dayjs().subtract(middleware.UPDATE_INTERVAL - 10, 'ms');

testUser = await userService.updateById(testUser.id, {
data: { lastActiveAt: nonExpiredDate.toDate() },
});

await middleware.use(
{ user: testUser, session: testUser } as any,
{} as Response,
callback,
);

const user = await userService.getById(testUser.id);

expect(user.lastActiveAt).toEqual(nonExpiredDate.toDate());
expect(callback).toBeCalledTimes(1);
});

it('will be updated when lastActiveAt expired', async () => {
const expiredDate = dayjs().subtract(middleware.UPDATE_INTERVAL + 10, 'ms');

testUser.lastActiveAt = expiredDate.toDate();

await middleware.use(
{ user: testUser, session: testUser } as any,
{} as Response,
callback,
);

const updatedUser = await userService.getById(testUser.id);

expect(Number(updatedUser.lastActiveAt)).toBeGreaterThan(Number(expiredDate.toDate()));
expect(callback).toBeCalledTimes(1);
});
});
});
});
2 changes: 1 addition & 1 deletion services/workflows-service/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ async function main() {
httpOnly: true,
secure: false,
sameSite: 'strict',
maxAge: 1000 * 60 * 60 * 1, // 1 hour(s)
maxAge: 1000 * 60 * 60 * 1, // 1 hour(s),
}),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ import { ACGuard } from 'nest-access-control';
import { AclFilterResponseInterceptor } from '@/common/access-control/interceptors/acl-filter-response.interceptor';
import { AclValidateRequestInterceptor } from '@/common/access-control/interceptors/acl-validate-request.interceptor';
import { CallHandler, ExecutionContext, INestApplication, Provider, Type } from '@nestjs/common';
import { EndUserModule } from '@/end-user/end-user.module';
import { EndUserService } from '@/end-user/end-user.service';
import { InstanceLink } from '@nestjs/core/injector/instance-links-host';
import console from 'console';
import { AppLoggerModule } from '@/common/app-logger/app-logger.module';
import { ClsModule } from 'nestjs-cls';
Expand Down

0 comments on commit 916021e

Please sign in to comment.