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
2 changes: 2 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{
}
756 changes: 485 additions & 271 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"seed": "npx prisma db seed"
},
"dependencies": {
"@nest-lab/throttler-storage-redis": "^1.2.0",
"@nestjs/bull": "^11.0.4",
"@nestjs/bullmq": "^11.0.4",
"@nestjs/common": "^11.0.1",
Expand All @@ -36,6 +37,7 @@
"@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.2.6",
"@nestjs/terminus": "^11.1.1",
"@nestjs/throttler": "^6.5.0",
"@prisma/adapter-pg": "^7.4.0",
"@prisma/client": "^7.4.1",
"bcryptjs": "^3.0.3",
Expand All @@ -45,7 +47,7 @@
"class-validator": "^0.14.3",
"cloudinary": "^1.41.3",
"dotenv": "^16.6.1",
"ioredis": "^5.9.3",
"ioredis": "^5.10.1",
"nodemailer": "^8.0.1",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
Expand Down
3 changes: 3 additions & 0 deletions src/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AppService } from './app.service';
import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
import { Throttle } from '@nestjs/throttler';

@Controller()
export class AppController {
Expand All @@ -11,9 +12,11 @@ export class AppController {
return this.appService.getHello();
}

@Throttle({ default: { ttl: 60000, limit: 5 } })
@Get('protected')
@UseGuards(JwtAuthGuard)
getProtected(): string {
return 'Protected route accessed';
}
}

42 changes: 35 additions & 7 deletions src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { APP_INTERCEPTOR, APP_GUARD } from '@nestjs/core';
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaModule } from './prisma/prisma.module';
Expand All @@ -13,13 +15,33 @@ import { AuthModule } from './auth/auth.module';
import { HealthModule } from './health/health.module';
import { LoggingModule } from './logging/logging.module';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import { APP_INTERCEPTOR } from '@nestjs/core';

import { LoggingInterceptor } from './logging/logging.interceptor';
import { JobsModule } from './jobs/jobs.module';
import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis';


@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
ThrottlerModule.forRoot([
{
ttl: 60000,
limit: 100,
},
]),


// To be used only when limits needs to be persisted upon restart

// ThrottlerModule.forRoot({
// throttlers: [{ ttl: 60000, limit: 100 }],
// storage: new ThrottlerStorageRedisService({
// host: 'redis', // Docker service name
// port: 6379,
// }),
// }),

PrismaModule,
PetsModule,
AdoptionModule,
Expand All @@ -31,17 +53,23 @@ import { JobsModule } from './jobs/jobs.module';
HealthModule,
LoggingModule,
JobsModule,

],

controllers: [AppController],
providers: [
{
AppService,
HttpExceptionFilter,
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
AppService, HttpExceptionFilter],

],
})
export class AppModule {}


export class AppModule { }
16 changes: 16 additions & 0 deletions src/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,19 @@ describe('AuthController (e2e)', () => {
firstName: 'Test',
lastName: 'User',
});

const body = res.body as {
access_token: string;
user: { email: string; role: string };
};

expect(res.status).toBe(201);
expect(body).toHaveProperty('access_token');
expect(body.user).toMatchObject({
email: uniqueEmail,
role: 'USER',
});

}, 10000);

it('should reject duplicate email registration', async () => {
Expand All @@ -86,6 +89,7 @@ describe('AuthController (e2e)', () => {
firstName: 'Dupe',
lastName: 'User',
});

const res: Response = await request(app.getHttpServer())
.post('/auth/register')
.send({
Expand All @@ -94,6 +98,7 @@ describe('AuthController (e2e)', () => {
firstName: 'Dupe',
lastName: 'User',
});

const body = res.body as { message: string };
expect(res.status).toBe(409);
expect(body.message).toMatch(/already registered/i);
Expand All @@ -108,6 +113,7 @@ describe('AuthController (e2e)', () => {
firstName: 'Bad',
lastName: 'Email',
});

const body = res.body as { message: string };
expect(res.status).toBe(400);
expect(body.message).toContain('email must be an email');
Expand All @@ -122,11 +128,13 @@ describe('AuthController (e2e)', () => {
firstName: 'Weak',
lastName: 'Pass',
});

const body = res.body as { message: string };
expect(res.status).toBe(400);
expect(body.message).toContain(
'password must be longer than or equal to 8 characters',
);

});

it('should login successfully and return JWT', async () => {
Expand All @@ -138,17 +146,20 @@ describe('AuthController (e2e)', () => {
firstName: 'Login',
lastName: 'User',
});

// Login
const res: Response = await request(app.getHttpServer())
.post('/auth/login')
.send({
email: uniqueEmail,
password: 'StrongPass123',
});

const body = res.body as {
access_token: string;
user: { email: string; role: string };
};

expect(res.status).toBe(201);
expect(body).toHaveProperty('access_token');
expect(body.user).toMatchObject({ email: uniqueEmail, role: 'USER' });
Expand All @@ -161,6 +172,7 @@ describe('AuthController (e2e)', () => {
email: 'nonexistent@example.com',
password: 'wrongpassword',
});

const body = res.body as { message: string };
expect(res.status).toBe(401);
expect(body.message).toMatch(/invalid credentials/i);
Expand All @@ -174,6 +186,7 @@ describe('AuthController (e2e)', () => {
password: 'StrongPass123',
// missing firstName and lastName
});

const body = res.body as { message: string[] };
expect(res.status).toBe(400);
expect(body.message).toContain('firstName should not be empty');
Expand All @@ -191,6 +204,7 @@ describe('AuthController (e2e)', () => {
lastName: 'Fields',
extraField: 'shouldBeIgnored',
});

const body = res.body as { message: string[] };
expect(res.status).toBe(400);
expect(
Expand All @@ -217,6 +231,7 @@ describe('AuthController (e2e)', () => {
password: 'StrongPass123',
extraField: 'shouldBeIgnored',
});

const body = res.body as { message: string[] };
expect(res.status).toBe(400);
expect(
Expand All @@ -225,4 +240,5 @@ describe('AuthController (e2e)', () => {
),
).toBe(true);
});

});
11 changes: 11 additions & 0 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ import { AuthService } from './auth.service';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';


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


@Throttle({ default: { ttl: 60000, limit: 3 } }) // 3 requests per minute
@Post('register')
@ApiOperation({ summary: 'User Registration' })
@ApiResponse({ status: 200, description: 'Registration successful' })
Expand All @@ -17,6 +21,7 @@ export class AuthController {
return this.authService.register(dto);
}

@Throttle({ default: { ttl: 60000, limit: 5 } }) // 5 requests per minute
@Post('login')
@ApiOperation({ summary: 'User login' })
@ApiResponse({ status: 200, description: 'Login successful' })
Expand All @@ -25,3 +30,9 @@ export class AuthController {
return this.authService.login(dto);
}
}






1 change: 1 addition & 0 deletions src/auth/guards/jwt-auth.guard.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';


@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
6 changes: 6 additions & 0 deletions src/auth/guards/roles.guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('RolesGuard', () => {
guard = new RolesGuard(reflector);
});


it('should allow access when no roles required', () => {
jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(undefined);

Expand All @@ -20,6 +21,7 @@ describe('RolesGuard', () => {
expect(guard.canActivate(context)).toBe(true);
});


it('should allow access when user has required role', () => {
jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue([Role.ADMIN]);

Expand All @@ -28,6 +30,7 @@ describe('RolesGuard', () => {
expect(guard.canActivate(context)).toBe(true);
});


it('should deny access when user lacks required role', () => {
jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue([Role.ADMIN]);

Expand All @@ -36,6 +39,7 @@ describe('RolesGuard', () => {
expect(guard.canActivate(context)).toBe(false);
});


it('should allow access when user has one of multiple required roles', () => {
jest
.spyOn(reflector, 'getAllAndOverride')
Expand All @@ -45,13 +49,15 @@ describe('RolesGuard', () => {

expect(guard.canActivate(context)).toBe(true);
});

});

function createMockContext(user: any): ExecutionContext {
return {
switchToHttp: () => ({
getRequest: () => ({ user }),
}),

getHandler: () => ({}),
getClass: () => ({}),
} as any;
Expand Down
1 change: 1 addition & 0 deletions src/auth/guards/roles.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Role } from '../enums/role.enum';
import { Request } from 'express';

interface AuthenticatedRequest extends Request {

user?: {
role?: string; // JWT carries string
};
Expand Down
5 changes: 5 additions & 0 deletions src/common/logger/logger.service.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@

import { Injectable, LoggerService } from '@nestjs/common';

@Injectable()
export class AppLogger implements LoggerService {
log(message: string) {
console.log(`[LOG] ${message}`);

}

error(message: string, trace?: string) {
console.error(`[ERROR] ${message}`, trace);

}

warn(message: string) {
console.warn(`[WARN] ${message}`);

}

debug(message: string) {
console.debug(`[DEBUG] ${message}`);

}

verbose(message: string) {
Expand Down
3 changes: 3 additions & 0 deletions src/health/health.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import {
} from '@nestjs/terminus';
import { PrismaService } from '../prisma/prisma.service';
import { PrismaClient } from '@prisma/client';
import { SkipThrottle } from '@nestjs/throttler';

@SkipThrottle()
@Controller('health')
export class HealthController {
constructor(
Expand Down Expand Up @@ -39,3 +41,4 @@ export class HealthController {
// ]);
// }
}

3 changes: 3 additions & 0 deletions src/health/health.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ import { HealthController } from './health.controller';
controllers: [HealthController],
})
export class HealthModule {}



Loading
Loading