Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,20 @@ describe('AuthService', () => {
expect(mockUserRepo.update).toHaveBeenCalledWith('user-1', { refreshToken: null });
});

it('throws UnauthorizedException when the user status is SUSPENDED', async () => {
mockJwtService.verify.mockReturnValue(validDecoded);
mockUserRepo.findOneBy.mockResolvedValue(makeUser({ status: UserStatus.SUSPENDED }));

await expect(service.refreshTokens('token')).rejects.toThrow(UnauthorizedException);
});

it('throws UnauthorizedException when the user status is INACTIVE', async () => {
mockJwtService.verify.mockReturnValue(validDecoded);
mockUserRepo.findOneBy.mockResolvedValue(makeUser({ status: UserStatus.INACTIVE }));

await expect(service.refreshTokens('token')).rejects.toThrow(UnauthorizedException);
});

it('issues new tokens when the refresh token is valid and not blacklisted', async () => {
mockJwtService.verify.mockReturnValue(validDecoded);
mockUserRepo.findOneBy.mockResolvedValue(makeUser());
Expand Down
6 changes: 5 additions & 1 deletion src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { v4 as uuidv4 } from 'uuid';
import * as bcrypt from 'bcrypt';
import { User } from '../users/entities/user.entity';
import { User, UserStatus } from '../users/entities/user.entity';
import { TokenBlacklistService } from './services/token-blacklist.service';

@Injectable()
Expand Down Expand Up @@ -51,6 +51,10 @@ export class AuthService {
throw new UnauthorizedException('Access Denied');
}

if (user.status !== UserStatus.ACTIVE) {
throw new UnauthorizedException('User is not active');
}

const refreshTokenMatches = await bcrypt.compare(refreshToken, user.refreshToken);
if (!refreshTokenMatches) {
throw new UnauthorizedException('Access Denied');
Expand Down
98 changes: 98 additions & 0 deletions src/auth/jwt.strategy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { UnauthorizedException } from '@nestjs/common';
import { JwtStrategy, JwtPayload } from './jwt.strategy';
import { User, UserStatus } from '../users/entities/user.entity';

describe('JwtStrategy', () => {
let strategy: JwtStrategy;

const mockUserRepo = {
findOneBy: jest.fn(),
createQueryBuilder: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [JwtStrategy, { provide: getRepositoryToken(User), useValue: mockUserRepo }],
}).compile();

strategy = module.get<JwtStrategy>(JwtStrategy);
});

afterEach(() => jest.clearAllMocks());

it('should be defined', () => {
expect(strategy).toBeDefined();
});

describe('validate', () => {
const payload: JwtPayload = {
sub: 'user-1',
email: 'test@example.com',
roles: [],
permissions: [],
};

const mockUser = {
id: 'user-1',
email: 'test@example.com',
status: UserStatus.ACTIVE,
};

const mockUserWithRolesAndPermissions = {
...mockUser,
roles: [
{
name: 'student',
permissions: [{ resource: 'course', action: 'read' }],
},
],
};

it('should successfully validate and return payload with roles and permissions if user is active', async () => {
mockUserRepo.findOneBy.mockResolvedValue(mockUser);

const mockQueryBuilder = {
leftJoinAndSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
getOne: jest.fn().mockResolvedValue(mockUserWithRolesAndPermissions),
};
mockUserRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder);

const result = await strategy.validate(payload);

expect(mockUserRepo.findOneBy).toHaveBeenCalledWith({ id: 'user-1' });
expect(result).toEqual({
sub: 'user-1',
email: 'test@example.com',
roles: ['student'],
permissions: ['course:read'],
});
});

it('should throw UnauthorizedException if the user is suspended', async () => {
mockUserRepo.findOneBy.mockResolvedValue({
...mockUser,
status: UserStatus.SUSPENDED,
});

await expect(strategy.validate(payload)).rejects.toThrow(UnauthorizedException);
});

it('should throw UnauthorizedException if the user is inactive', async () => {
mockUserRepo.findOneBy.mockResolvedValue({
...mockUser,
status: UserStatus.INACTIVE,
});

await expect(strategy.validate(payload)).rejects.toThrow(UnauthorizedException);
});

it('should throw an error if the user is not found', async () => {
mockUserRepo.findOneBy.mockResolvedValue(null);

await expect(strategy.validate(payload)).rejects.toThrow(Error);
});
});
});
8 changes: 6 additions & 2 deletions src/auth/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Injectable } from '@nestjs/common';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../users/entities/user.entity';
import { User, UserStatus } from '../users/entities/user.entity';

export interface JwtPayload {
sub: string;
Expand Down Expand Up @@ -39,6 +39,10 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
throw new Error('User not found');
}

if (user.status !== UserStatus.ACTIVE) {
throw new UnauthorizedException('User is not active');
}

// Fetch roles and permissions for the user
const userWithRolesAndPermissions = await this.userRepository
.createQueryBuilder('user')
Expand Down
Loading