Skip to content

Commit 321ad44

Browse files
authored
Merge pull request #1045 from Vox-d-glitch/issue-974-jwt-token-issuance
Add configurable JWT access token issuance
2 parents f13b6c6 + 21e7d65 commit 321ad44

1,517 files changed

Lines changed: 11491 additions & 138 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/src/auth/auth.module.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,25 @@
11
import { Module } from '@nestjs/common';
22
import { JwtModule } from '@nestjs/jwt';
33
import { PassportModule } from '@nestjs/passport';
4-
import { ConfigModule, ConfigService } from '@nestjs/config';
54
import { AuthController } from './auth.controller.js';
65
import { AuthService } from './auth.service.js';
76
import { JwtStrategy } from './strategies/jwt.strategy.js';
87
import { WalletStrategy } from './strategies/wallet.strategy.js';
98
import { JwtAuthGuard } from './guards/jwt-auth.guard.js';
109
import { RolesGuard } from './guards/roles.guard.js';
10+
import { jwtModuleConfig } from '../config/jwt.config.js';
1111
import { UsersModule } from '../users/users.module.js';
1212

13+
/**
14+
* #971: Self-contained Auth module.
15+
*
16+
* Exports guards and strategies for use in other modules.
17+
* Handles wallet authentication, JWT lifecycle, session management, and RBAC.
18+
*/
1319
@Module({
1420
imports: [
1521
PassportModule.register({ defaultStrategy: 'jwt' }),
16-
JwtModule.registerAsync({
17-
imports: [ConfigModule],
18-
useFactory: (configService: ConfigService) => ({
19-
secret: configService.get('JWT_SECRET', 'dev-secret'),
20-
signOptions: {
21-
expiresIn: configService.get('JWT_ACCESS_TTL', '900'),
22-
},
23-
}),
24-
inject: [ConfigService],
25-
}),
22+
JwtModule.registerAsync(jwtModuleConfig),
2623
UsersModule,
2724
],
2825
controllers: [AuthController],

backend/src/auth/auth.service.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { JwtAccessTokenPayload } from './interfaces/jwt-payload.interface.js';
66
import { WalletStrategy } from './strategies/wallet.strategy.js';
77
import { UsersService } from '../users/users.service.js';
88
import { AuthRole } from '../common/enums/auth-role.enum.js';
9+
import { ROLE_PERMISSIONS } from '../common/constants/role-permissions.constant.js';
910

1011
/**
1112
* #971-978: Auth service handling wallet login, JWT lifecycle, and session management.
@@ -125,6 +126,7 @@ export class AuthService {
125126
const roles = user.roles?.length
126127
? user.roles.map((role) => role.name)
127128
: [AuthRole.MENTEE];
129+
const permissions = this.resolvePermissions(roles);
128130

129131
const accessPayload: JwtAccessTokenPayload = {
130132
sub: user.id,
@@ -133,6 +135,7 @@ export class AuthService {
133135
iat: Math.floor(Date.now() / 1000),
134136
exp: Math.floor(Date.now() / 1000) + accessTtl,
135137
roles,
138+
permissions,
136139
status: user.status,
137140
};
138141

@@ -222,11 +225,17 @@ export class AuthService {
222225
// In production: persist to database via UsersService
223226
}
224227

225-
private resolveRoles(walletAddress: string): Promise<string[]> {
226-
// In production: query from database, keyed by walletAddress
227-
// Default role for all authenticated users
228-
void walletAddress;
229-
return Promise.resolve(['MENTEE']);
228+
/**
229+
* #974: Resolve the union of permissions granted by a set of roles.
230+
*/
231+
private resolvePermissions(roles: AuthRole[]): string[] {
232+
const permissions = new Set<string>();
233+
for (const role of roles) {
234+
for (const permission of ROLE_PERMISSIONS[role] ?? []) {
235+
permissions.add(permission);
236+
}
237+
}
238+
return Array.from(permissions);
230239
}
231240

232241
private cleanExpiredNonces(): void {

backend/src/auth/guards/jwt-auth.guard.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
import { ConfigService } from '@nestjs/config';
88
import { JwtService } from '@nestjs/jwt';
99
import { Request } from 'express';
10-
import { JwtAccessTokenPayload } from '../interfaces/jwt-payload.interface';
10+
import { JwtAccessTokenPayload } from '../interfaces/jwt-payload.interface.js';
1111
import { UserStatus } from '../../users/enums/user-status.enum.js';
1212

1313
/**

backend/src/auth/interfaces/jwt-payload.interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ export interface JwtAccessTokenPayload {
77
iat: number;
88
exp: number;
99
roles?: string[];
10+
permissions?: string[];
1011
status: UserStatus;
1112
}

backend/src/auth/strategies/jwt.strategy.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
2929
iat: payload.iat,
3030
exp: payload.exp,
3131
roles: payload.roles || [],
32+
permissions: payload.permissions || [],
3233
status: payload.status,
3334
};
3435
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { AuthRole } from '../enums/auth-role.enum.js';
2+
import { Permission } from '../enums/permission.enum.js';
3+
4+
export const ROLE_PERMISSIONS: Record<AuthRole, Permission[]> = {
5+
[AuthRole.USER]: [Permission.PROFILE_READ],
6+
[AuthRole.MENTEE]: [Permission.PROFILE_READ, Permission.MENTEE_PROFILE_WRITE],
7+
[AuthRole.MENTOR]: [Permission.PROFILE_READ, Permission.MENTOR_PROFILE_WRITE],
8+
[AuthRole.ADMIN]: [
9+
Permission.PROFILE_READ,
10+
Permission.PROFILE_WRITE,
11+
Permission.MENTOR_PROFILE_WRITE,
12+
Permission.MENTEE_PROFILE_WRITE,
13+
Permission.USER_MANAGE,
14+
],
15+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export enum Permission {
2+
PROFILE_READ = 'profile:read',
3+
PROFILE_WRITE = 'profile:write',
4+
MENTOR_PROFILE_WRITE = 'mentor_profile:write',
5+
MENTEE_PROFILE_WRITE = 'mentee_profile:write',
6+
USER_MANAGE = 'user:manage',
7+
}

backend/src/config/jwt.config.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { ConfigModule, ConfigService } from '@nestjs/config';
2+
import { JwtModuleOptions } from '@nestjs/jwt';
3+
import { Algorithm } from 'jsonwebtoken';
4+
5+
export const jwtModuleConfig = {
6+
imports: [ConfigModule],
7+
inject: [ConfigService],
8+
useFactory: (config: ConfigService): JwtModuleOptions => {
9+
const algorithm = config.get<Algorithm>('JWT_ALGORITHM', 'HS256');
10+
11+
if (algorithm === 'RS256') {
12+
return {
13+
privateKey: config.get<string>('JWT_PRIVATE_KEY'),
14+
publicKey: config.get<string>('JWT_PUBLIC_KEY'),
15+
signOptions: { algorithm },
16+
verifyOptions: { algorithms: [algorithm] },
17+
};
18+
}
19+
20+
return {
21+
secret: config.get<string>('JWT_SECRET', 'dev-secret'),
22+
signOptions: { algorithm },
23+
verifyOptions: { algorithms: [algorithm] },
24+
};
25+
},
26+
};

backend/src/users/users.controller.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ export class UsersController {
129129
dto.username,
130130
);
131131
return UserResponseDto.fromEntity(user);
132+
}
133+
132134
@Get('admin/completeness')
133135
@UseGuards(RolesGuard)
134136
@Roles(AuthRole.ADMIN)

backend/src/users/users.module.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Module } from '@nestjs/common';
22
import { TypeOrmModule } from '@nestjs/typeorm';
33
import { JwtModule } from '@nestjs/jwt';
4-
import { ConfigModule, ConfigService } from '@nestjs/config';
4+
import { ConfigModule } from '@nestjs/config';
55
import { UsersController } from './users.controller.js';
66
import { ProfilesController } from './profiles.controller.js';
77
import { UsersService } from './users.service.js';
@@ -14,6 +14,7 @@ import { MenteeProfile } from './entities/mentee-profile.entity.js';
1414
import { PortfolioLink } from './entities/portfolio-link.entity.js';
1515
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard.js';
1616
import { RolesGuard } from '../auth/guards/roles.guard.js';
17+
import { jwtModuleConfig } from '../config/jwt.config.js';
1718
import { StorageModule } from '../storage/storage.module.js';
1819
import { AvailabilityModule } from '../availability/availability.module.js';
1920
import { ProfileCompletenessService } from './profile-completeness.service.js';
@@ -27,22 +28,12 @@ import { ProfileCompletenessService } from './profile-completeness.service.js';
2728
MenteeProfile,
2829
PortfolioLink,
2930
]),
30-
JwtModule.registerAsync({
31-
imports: [ConfigModule],
32-
inject: [ConfigService],
33-
useFactory: (config: ConfigService) => ({
34-
secret: config.get<string>('JWT_SECRET'),
35-
signOptions: { expiresIn: '1h' },
36-
}),
37-
}),
31+
JwtModule.registerAsync(jwtModuleConfig),
3832
ConfigModule,
3933
StorageModule,
4034
AvailabilityModule,
4135
],
4236
controllers: [UsersController, ProfilesController, AvatarController],
43-
providers: [UsersService, JwtAuthGuard, RolesGuard],
44-
exports: [UsersService],
45-
controllers: [UsersController],
4637
providers: [
4738
UsersService,
4839
JwtAuthGuard,

0 commit comments

Comments
 (0)