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
3 changes: 3 additions & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/typeorm": "^11.0.0",
"bcrypt": "^6.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"passport-jwt": "^4.0.1",
Copy link

@coderabbitai coderabbitai bot Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify if passport core package is required.

You've added passport-jwt but there's no passport core package in dependencies. If you're using passport-jwt strategy, you typically need the passport core package as well.

Run the following script to check if passport is already present or if passport-jwt works standalone in your implementation:


🏁 Script executed:

#!/bin/bash
# Check for passport imports and usage in the codebase
rg -n --type=ts "from 'passport'" -A 2 -B 2
rg -n --type=ts "import.*passport[^-]" -A 2 -B 2

Length of output: 0


I need to gather more comprehensive information about the codebase to verify this dependency concern.


🏁 Script executed:

#!/bin/bash
# Check package.json for 'passport' and 'passport-jwt' entries
grep -E "(passport|passport-jwt)" apps/backend/package.json

Length of output: 66


🏁 Script executed:

#!/bin/bash
# Search more broadly for passport usage - include .js, require(), and variations
rg -n "passport" apps/backend/src -A 1 -B 1 --type=ts --type=js || echo "No passport mentions found in src"

Length of output: 33


🏁 Script executed:

#!/bin/bash
# Check if there are passport-jwt usage patterns (JwtStrategy, ExtractJwt, etc)
rg -n "JwtStrategy|ExtractJwt|passport\.use" apps/backend/src -A 1 -B 1 || echo "No JwtStrategy/ExtractJwt found"

Length of output: 31


🏁 Script executed:

#!/bin/bash
# Look for any strategy setup or auth configuration files that might use passport
fd -e ts -e js . apps/backend/src | head -20 | xargs grep -l "auth" 2>/dev/null || echo "No auth-related files easily identified"

Length of output: 235



🏁 Script executed:

#!/bin/bash
# Check auth.module.ts to see the actual auth strategy being used
cat apps/backend/src/auth/auth.module.ts

Length of output: 727


🏁 Script executed:

#!/bin/bash
# Check auth.service.ts to see JWT implementation
cat apps/backend/src/auth/auth.service.ts

Length of output: 2918


🏁 Script executed:

#!/bin/bash
# Check if @nestjs/jwt or other JWT strategies are in dependencies
grep -E "(@nestjs/jwt|jsonwebtoken|jwt)" apps/backend/package.json

Length of output: 96


🏁 Script executed:

#!/bin/bash
# Check app.module.ts to see module imports
cat apps/backend/src/app.module.ts

Length of output: 1164


Remove unused passport-jwt dependency and its type definitions.

The codebase uses @nestjs/jwt for JWT authentication (see auth.module.ts and auth.service.ts). The passport-jwt package is not imported or used anywhere in the implementation, making it an unused dependency. Remove "passport-jwt": "^4.0.1" and "@types/passport-jwt": "^4.0.1" from apps/backend/package.json.

🤖 Prompt for AI Agents
In apps/backend/package.json around line 33, remove the unused dependency
entries for "passport-jwt": "^4.0.1" and the corresponding
"@types/passport-jwt": "^4.0.1" from the dependencies/devDependencies section;
update the package.json by deleting those two lines, run npm/yarn install to
update lockfile, and ensure no imports/use of passport-jwt remain before
committing.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that passport-jwt isn't used yet.

While @nestjs/jwt handles the creation of tokens (which is done in the login function), passport-jwt is the standard library used with NestJS's AuthGuard to validate incoming tokens and protect routes.

I've included it now because the next step, after login, will be to implement route protection using AuthGuard('jwt'), which requires passport-jwt as its underlying strategy. Keeping it ensures the necessary dependency is ready for that implementation.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

"pg": "^8.16.3",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
Expand All @@ -43,6 +45,7 @@
"@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/node": "^22.10.7",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
Expand Down
83 changes: 83 additions & 0 deletions apps/backend/src/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ import { AuthController } from '@/auth/auth.controller';
import { AuthService } from '@/auth/auth.service';
import { RegisterUserDto } from '@/auth/dto/register-user.dto';
import { UserResponseDto } from '@/users/user-response.dto';
import { LoginUserDto } from '@/auth/dto/login-user.dto';
import { AccessTokenDto } from '@/auth/dto/access-token.dto';
import { ConflictException, UnauthorizedException } from '@nestjs/common';

const mockAuthService = {
register: jest.fn(),
login: jest.fn(),
};

describe('AuthController', () => {
Expand Down Expand Up @@ -58,5 +62,84 @@ describe('AuthController', () => {
expect(authService.register).toHaveBeenCalledWith(mockRegisterUserDto);
expect(actualUserResponseDto).toEqual(mockUserResponseDto);
});

it('should propagate ConflictException from the Auth Service', async () => {
// Arrange
const mockRegisterUserDto: RegisterUserDto = {
name: 'Test User',
email: 'test.user@test.com',
password: 'StrongPassword@123',
};

(authService.register as jest.Mock).mockRejectedValue(
new ConflictException('Unable to complete registration at this time.'),
);

// Act & Assert
await expect(
controller.register(mockRegisterUserDto),
).rejects.toMatchObject({
name: 'ConflictException',
message: 'Unable to complete registration at this time.',
});
});

it('should propagate any general error from the Auth Service', async () => {
// Arrange
const mockRegisterUserDto: RegisterUserDto = {
name: 'Test User',
email: 'test.user@test.com',
password: 'StrongPassword@123',
};

(authService.register as jest.Mock).mockRejectedValue(
new Error('Something went wrong.'),
);

// Act & Assert
await expect(controller.register(mockRegisterUserDto)).rejects.toThrow(
'Something went wrong.',
);
});
});

describe('login', () => {
it('should call the login method of the Auth Service', async () => {
// Arrange
const mockLoginUserDto: LoginUserDto = {
email: 'test.user@test.com',
password: 'PlainTextPassword',
};
const expectedResponse: AccessTokenDto = {
access_token: 'SomeReallyLongAccessTokenText',
};

(authService.login as jest.Mock).mockResolvedValue(expectedResponse);

// Act
const actualResponse = await controller.login(mockLoginUserDto);

// Assert
expect(authService.login).toHaveBeenCalledWith(mockLoginUserDto);
expect(actualResponse).toEqual(expectedResponse);
});

it('should propagate UnauthorizedException from the Auth Service', async () => {
// Arrange
const mockLoginUserDto: LoginUserDto = {
email: 'test.user@test.com',
password: 'PlainTextPassword',
};

(authService.login as jest.Mock).mockRejectedValue(
new UnauthorizedException('Invalid credentials.'),
);

// Act & Assert
await expect(controller.login(mockLoginUserDto)).rejects.toMatchObject({
name: 'UnauthorizedException',
message: 'Invalid credentials.',
});
});
});
});
11 changes: 10 additions & 1 deletion apps/backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@ import { Body, Controller, Post } from '@nestjs/common';
import { RegisterUserDto } from '@/auth/dto/register-user.dto';
import { AuthService } from '@/auth/auth.service';
import { UserResponseDto } from '@/users/user-response.dto';
import { LoginUserDto } from '@/auth/dto/login-user.dto';
import { AccessTokenDto } from '@/auth/dto/access-token.dto';

@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}

@Post('register')
register(@Body() registerUserDto: RegisterUserDto): Promise<UserResponseDto> {
async register(
@Body() registerUserDto: RegisterUserDto,
): Promise<UserResponseDto> {
return this.authService.register(registerUserDto);
}

@Post('login')
async login(@Body() loginUserDto: LoginUserDto): Promise<AccessTokenDto> {
return this.authService.login(loginUserDto);
}
}
24 changes: 19 additions & 5 deletions apps/backend/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from '@/users/user.entity';
import { AuthController } from '@/auth/auth.controller';
import { AuthService } from '@/auth/auth.service';
import { UsersModule } from '@/users/users.module';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
imports: [TypeOrmModule.forFeature([User])],
imports: [
UsersModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
global: true,
secret: configService.getOrThrow<string>('JWT_SECRET'),
signOptions: {
expiresIn: '60m',
},
}),
inject: [ConfigService],
}),
],
controllers: [AuthController],
providers: [AuthService],
})
Expand Down
118 changes: 117 additions & 1 deletion apps/backend/src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ import { DataSource } from 'typeorm';
import { RegisterUserDto } from '@/auth/dto/register-user.dto';
import { User } from '@/users/user.entity';
import * as bcrypt from 'bcrypt';
import { ConflictException } from '@nestjs/common';
import { ConflictException, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { UsersService } from '@/users/users.service';
import { JwtService } from '@nestjs/jwt';
import { LoginUserDto } from '@/auth/dto/login-user.dto';
import { AccessTokenDto } from '@/auth/dto/access-token.dto';

jest.mock('bcrypt', () => ({
hash: jest.fn(),
compare: jest.fn(),
}));

const mockQueryRunner = {
Expand All @@ -30,10 +35,20 @@ const mockConfigService = {
get: jest.fn(),
};

const mockUsersService = {
findOneByEmail: jest.fn(),
};

const mockJwtService = {
signAsync: jest.fn(),
};

describe('AuthService', () => {
let service: AuthService;
let dataSource: DataSource;
let configService: ConfigService;
let usersService: UsersService;
let jwtService: JwtService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
Expand All @@ -47,12 +62,22 @@ describe('AuthService', () => {
provide: ConfigService,
useValue: mockConfigService,
},
{
provide: UsersService,
useValue: mockUsersService,
},
{
provide: JwtService,
useValue: mockJwtService,
},
],
}).compile();

service = module.get<AuthService>(AuthService);
dataSource = module.get<DataSource>(DataSource);
configService = module.get<ConfigService>(ConfigService);
usersService = module.get<UsersService>(UsersService);
jwtService = module.get<JwtService>(JwtService);

jest.clearAllMocks();
});
Expand Down Expand Up @@ -257,4 +282,95 @@ describe('AuthService', () => {
);
});
});

describe('login', () => {
it('should return the access token when credentials are valid', async () => {
// Arrange
const mockLoginUserDto: LoginUserDto = {
email: 'test.user@test.com',
password: 'PlainTextPassword',
};

const testUser = new User();
testUser.id = 1;
testUser.email = 'test.user@test.com';
testUser.password = 'SomeRandomHashedPassword';

const expectedPayload = {
sub: testUser.id,
email: testUser.email,
};
const expectedAccessToken = 'SomeReallyLongAccessTokenText';
const expectedResponse: AccessTokenDto = {
access_token: expectedAccessToken,
};

(usersService.findOneByEmail as jest.Mock).mockResolvedValue(testUser);
(bcrypt.compare as jest.Mock).mockResolvedValue(true);
(jwtService.signAsync as jest.Mock).mockReturnValue(expectedAccessToken);

// Act
const response = await service.login(mockLoginUserDto);

// Assert
expect(usersService.findOneByEmail).toHaveBeenCalledWith(
mockLoginUserDto.email,
);
expect(bcrypt.compare).toHaveBeenCalledWith(
mockLoginUserDto.password,
testUser.password,
);
expect(jwtService.signAsync).toHaveBeenCalledWith(expectedPayload);
expect(response).toEqual(expectedResponse);
});

it('should throw Unauthorized exception when user does not exists', async () => {
// Arrange
const mockLoginUserDto: LoginUserDto = {
email: 'nouser@test.com',
password: 'PlainTextPassword',
};

(usersService.findOneByEmail as jest.Mock).mockResolvedValue(null);

// Act & Assert
await expect(service.login(mockLoginUserDto)).rejects.toBeInstanceOf(
UnauthorizedException,
);
expect(usersService.findOneByEmail).toHaveBeenCalledWith(
mockLoginUserDto.email,
);
expect(bcrypt.compare).not.toHaveBeenCalled();
expect(jwtService.signAsync).not.toHaveBeenCalled();
});

it('should throw Unauthorized exception when passwords do not match', async () => {
// Arrange
const mockLoginUserDto: LoginUserDto = {
email: 'test.user@test.com',
password: 'PlainTextPassword',
};

const testUser = new User();
testUser.id = 1;
testUser.email = 'test.user@test.com';
testUser.password = 'SomeRandomHashedPassword';

(usersService.findOneByEmail as jest.Mock).mockResolvedValue(testUser);
(bcrypt.compare as jest.Mock).mockResolvedValue(false);

// Act & Assert
await expect(service.login(mockLoginUserDto)).rejects.toBeInstanceOf(
UnauthorizedException,
);
expect(usersService.findOneByEmail).toHaveBeenCalledWith(
mockLoginUserDto.email,
);
expect(bcrypt.compare).toHaveBeenCalledWith(
mockLoginUserDto.password,
testUser.password,
);
expect(jwtService.signAsync).not.toHaveBeenCalled();
});
});
});
45 changes: 44 additions & 1 deletion apps/backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
import { ConflictException, Injectable } from '@nestjs/common';
import {
ConflictException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { RegisterUserDto } from '@/auth/dto/register-user.dto';
import * as bcrypt from 'bcrypt';
import { DataSource } from 'typeorm';
import { User } from '@/users/user.entity';
import { UserResponseDto } from '@/users/user-response.dto';
import { ConfigService } from '@nestjs/config';
import { UsersService } from '@/users/users.service';
import { JwtService } from '@nestjs/jwt';
import { LoginUserDto } from '@/auth/dto/login-user.dto';
import { AccessTokenDto } from '@/auth/dto/access-token.dto';

@Injectable()
export class AuthService {
constructor(
private dataSource: DataSource,
private configService: ConfigService,
private usersService: UsersService,
private jwtService: JwtService,
) {}
async register(registerUserDto: RegisterUserDto): Promise<UserResponseDto> {
const { name, email, password } = registerUserDto;
Expand Down Expand Up @@ -62,4 +72,37 @@ export class AuthService {
await queryRunner.release();
}
}

async login(loginUserDto: LoginUserDto): Promise<AccessTokenDto> {
const lowerCaseEmail = loginUserDto.email.toLowerCase().trim();
const user = await this.usersService.findOneByEmail(lowerCaseEmail);

if (!user) {
throw new UnauthorizedException({
message: 'Invalid credentials.',
});
}

const isPasswordCorrect = await bcrypt.compare(
loginUserDto.password,
user.password,
);

if (!isPasswordCorrect) {
throw new UnauthorizedException({
message: 'Invalid credentials.',
});
}

const payload = {
sub: user.id,
email: user.email,
};

const accessToken = await this.jwtService.signAsync(payload);

return {
access_token: accessToken,
};
}
}
Loading