Skip to content

Commit 930802b

Browse files
committed
feat(api): add user permissions caching and JWT token storage configuration
1 parent 7a44c43 commit 930802b

File tree

5 files changed

+97
-6
lines changed

5 files changed

+97
-6
lines changed

docs/api/Configurations.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Api Service Configurations
2+
3+
## Environment Variables
4+
5+
#### `VIDYA_AUTH_SAVE_PERMISSIONS_IN_JWT_TOKEN`.
6+
Saves user permissions in the JWT token. This will allow to keep the permissions in the token itself and avoid querying the database for each request.
7+
8+
#### `VIDYA_AUTH_USER_PERMISSIONS_CACHE_TTL`.
9+
Time to live for the user permissions cache in seconds. Set to 0 to disable caching for development purposes for example.
10+
11+
- If `VIDYA_AUTH_SAVE_PERMISSIONS_IN_JWT_TOKEN` is set to `false`, then user permissions will be fetched from the database for each request and stored in cache for the specified time.
12+
- If `VIDYA_AUTH_SAVE_PERMISSIONS_IN_JWT_TOKEN` is set to `true`, this will be ignored and user permissions will be stored in the JWT token itself.

modules/libs/protocol/auth.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as domain from '@vidya/domain';
22
import { OtpType } from "./otp";
33

4+
export const UserPermissionsStorageKey = (userId: string) => `users:permissions:${userId}`;
5+
46
/* -------------------------------------------------------------------------- */
57
/* One Time Password */
68
/* -------------------------------------------------------------------------- */

modules/services/api/src/auth/services/auth-users.service.ts

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
import { Mapper } from '@automapper/core';
22
import { InjectMapper } from '@automapper/nestjs';
3-
import { Injectable } from '@nestjs/common';
3+
import { Inject, Injectable, Logger } from '@nestjs/common';
4+
import { ConfigType } from '@nestjs/config';
45
import { InjectRepository } from '@nestjs/typeorm';
56
import * as dto from '@vidya/api/auth/dto';
7+
import { AuthConfig, RedisConfig } from '@vidya/api/configs';
68
import { Role, User } from '@vidya/entities';
79
import * as entities from '@vidya/entities';
8-
import { UserPermission } from '@vidya/protocol';
10+
import { UserPermission, UserPermissionsStorageKey } from '@vidya/protocol';
11+
import Redis from 'ioredis';
912
import { Repository } from 'typeorm';
1013

1114
export type LoginField = 'email' | 'phone';
1215

1316
@Injectable()
1417
export class AuthUsersService {
18+
private readonly redis: Redis;
19+
private readonly logger = new Logger(AuthUsersService.name);
20+
1521
/**
1622
* Creates an instance of AuthUsersService.
1723
* @param users Users repository
@@ -22,7 +28,16 @@ export class AuthUsersService {
2228
@InjectRepository(User) private readonly users: Repository<User>,
2329
@InjectRepository(Role) private readonly roles: Repository<Role>,
2430
@InjectMapper() private readonly mapper: Mapper,
25-
) {}
31+
@Inject(RedisConfig.KEY)
32+
private readonly redisConfig: ConfigType<typeof RedisConfig>,
33+
@Inject(AuthConfig.KEY)
34+
private readonly authConfig: ConfigType<typeof AuthConfig>,
35+
) {
36+
this.redis = new Redis({
37+
host: redisConfig.host,
38+
port: redisConfig.port,
39+
});
40+
}
2641

2742
/**
2843
* Finds a user by id.
@@ -74,7 +89,37 @@ export class AuthUsersService {
7489
* @returns User permissions
7590
*/
7691
async getUserPermissions(userId: string): Promise<UserPermission[]> {
77-
const userRoles = await this.getRolesOfUser(userId);
78-
return this.mapper.mapArray(userRoles, entities.Role, dto.UserPermission);
92+
// Get permissions from cache if available
93+
const permissions = await this.redis.get(UserPermissionsStorageKey(userId));
94+
95+
if (permissions) {
96+
// Permissions are cached. Parse and return them.
97+
return JSON.parse(permissions);
98+
} else {
99+
this.logger.verbose(`Fetching '${userId}' permissions from DB`);
100+
101+
// Permissions are not cached. Fetch them from the database.
102+
const userRoles = await this.getRolesOfUser(userId);
103+
const permissions = this.mapper.mapArray(
104+
userRoles,
105+
entities.Role,
106+
dto.UserPermission,
107+
);
108+
109+
// Cache the permissions if cache TTL is set to a positive value
110+
// otherwise, users permissions will be fetched from the database
111+
// on every request (which is good for development only)
112+
if (this.authConfig.userPermissionsCacheTtl > 0) {
113+
await this.redis.set(
114+
UserPermissionsStorageKey(userId),
115+
JSON.stringify(permissions),
116+
'EX',
117+
this.authConfig.userPermissionsCacheTtl,
118+
);
119+
}
120+
121+
// Return the permissions
122+
return permissions;
123+
}
79124
}
80125
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,29 @@
11
import { registerAs } from '@nestjs/config';
22

33
export default registerAs('auth', () => ({
4+
/**
5+
* Saves user permissions in the JWT token. This will allow
6+
* to keep the permissions in the token itself and avoid
7+
* querying the database for each request.
8+
*/
49
savePermissionsInJwtToken:
510
process.env.VIDYA_AUTH_SAVE_PERMISSIONS_IN_JWT_TOKEN === 'true',
11+
12+
/**
13+
* Time to live for the user permissions cache in seconds.
14+
* Set to 0 to disable caching for development purposes
15+
* for example.
16+
*
17+
* If `VIDYA_AUTH_SAVE_PERMISSIONS_IN_JWT_TOKEN` is set to `false`,
18+
* then user permissions will be fetched from the database for each
19+
* request and stored in cache for the specified time.
20+
*
21+
* If `VIDYA_AUTH_SAVE_PERMISSIONS_IN_JWT_TOKEN` is set to `true`,
22+
* this will be ignored and user permissions will be stored in the
23+
* JWT token itself.
24+
*/
25+
userPermissionsCacheTtl: parseInt(
26+
process.env.VIDYA_AUTH_USER_PERMISSIONS_CACHE_TTL ?? '0',
27+
10,
28+
),
629
}));

modules/services/api/src/edu/shared/testing-app.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { INestApplication, ValidationPipe } from '@nestjs/common';
22
import { Test } from '@nestjs/testing';
33
import { AppModule } from '@vidya/api/app.module';
4-
import { OtpService, RevokedTokensService } from '@vidya/api/auth/services';
4+
import {
5+
AuthUsersService,
6+
OtpService,
7+
RevokedTokensService,
8+
} from '@vidya/api/auth/services';
59
import { inMemoryDataSource } from '@vidya/api/utils';
610
import { useContainer } from 'class-validator';
711
import { DataSource } from 'typeorm';
@@ -12,10 +16,15 @@ export const createTestingApp = async (): Promise<INestApplication> => {
1216
})
1317
.overrideProvider(DataSource)
1418
.useValue(await inMemoryDataSource())
19+
// Mock the services with Redis client
20+
// to avoid connecting to the actual Redis server
21+
// and keeping connection open during the tests
1522
.overrideProvider(OtpService)
1623
.useValue({ validate: jest.fn() })
1724
.overrideProvider(RevokedTokensService)
1825
.useValue({ isRevoked: jest.fn() })
26+
.overrideProvider(AuthUsersService)
27+
.useValue({})
1928
.compile();
2029

2130
const app = module.createNestApplication();

0 commit comments

Comments
 (0)