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
41 changes: 39 additions & 2 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { AuthGuard, CustomRequest } from './auth.guard';
import { PasswordDTO } from './dto/password.dto';
import { ForgotPasswordDTO } from './dto/forgot-password.dto';
import { UserService } from 'src/user/user.service';
import { generateOTP } from 'src/utils/string';
import { generateOTP, formatString } from 'src/utils/string';
import { EmailService } from 'src/email/email.service';
import { OtpDTO } from 'src/user/dto/otp.dto';

Expand Down Expand Up @@ -54,7 +54,7 @@ export class AuthController {
otp = generateOTP(6);
await this.userService.saveOTP(user, otp);
}
this.emailService.sendEmail({
await this.emailService.sendEmail({
recipients: [{ email: user.email, name: user.firstName }],
subject: 'Reset your password',
html: `<p>Your OTP is <b>${otp}</b></p>`,
Expand Down Expand Up @@ -93,4 +93,41 @@ export class AuthController {
}
return HttpStatus.NO_CONTENT;
}

@UseGuards(AuthGuard)
@ApiBearerAuth()
@Post('otp/send')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Send OTP for email verification to authenticated user',
})
@ApiResponse({
status: HttpStatus.NO_CONTENT,
description: 'OTP sent successfully',
})
async sendOtp(@Req() req: CustomRequest): Promise<number> {
const user = req.user;

const otp = generateOTP(6);

await this.userService.saveOTP(user, otp);

let emailContent: string;
try {
const emailTemplate =
await this.emailService.findTemplateByName('otp_verification');
emailContent = formatString(emailTemplate.html, otp);
} catch {
emailContent = `<p>Your OTP is <b>${otp}</b></p>`;
}

await this.emailService.sendEmail({
recipients: [{ email: user.email, name: user.firstName }],
subject: 'Email Verification',
html: emailContent,
text: `Your OTP is ${otp}`,
});

return HttpStatus.NO_CONTENT;
}
}
11 changes: 7 additions & 4 deletions src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { Module } from '@nestjs/common';
import { Module, forwardRef } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UserModule } from 'src/user/user.module';
import { JwtModule } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { UserService } from 'src/user/user.service';
import { EmailModule } from 'src/email/email.module';
import { AuthGuard } from './auth.guard';

@Module({
imports: [
UserModule,
forwardRef(() => UserModule),
ConfigModule,
JwtModule.registerAsync({
useFactory: (configService: ConfigService) => ({
global: true,
Expand All @@ -22,7 +24,8 @@ import { EmailModule } from 'src/email/email.module';
}),
EmailModule,
],
providers: [AuthService, UserService],
providers: [AuthService, UserService, AuthGuard],
controllers: [AuthController],
exports: [AuthService, AuthGuard, JwtModule],
})
export class AuthModule {}
3 changes: 3 additions & 0 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import { LoginDTO, LoginResponseDTO } from './dto/login.dto';
import { UserDTO } from 'src/user/dto/user.dto';
import { JwtService } from '@nestjs/jwt';
import { User } from 'src/user/entities/user.entity';
import { EmailService } from 'src/email/email.service';

@Injectable()
export class AuthService {
constructor(
private userService: UserService,
private jwtService: JwtService,
private emailService: EmailService,
) {}

async encryptPassword(password: string): Promise<string> {
Expand Down Expand Up @@ -60,6 +62,7 @@ export class AuthService {
password: hashedPassword,
};
const newUser = await this.userService.create(newUserData);

return plainToInstance(User, newUser);
}

Expand Down
6 changes: 6 additions & 0 deletions src/auth/rol.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum Role {
ADMIN = 'admin',
BRANCH_ADMIN = 'branch_admin',
CUSTOMER = 'customer',
DELIVERY = 'delivery',
}
5 changes: 5 additions & 0 deletions src/auth/roles.decorador.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { SetMetadata } from '@nestjs/common';
import { Role } from 'src/auth/rol.enum';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
48 changes: 48 additions & 0 deletions src/auth/roles.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from 'src/auth/roles.decorador';
import { Role } from './rol.enum'; // Asegúrate de que el enum está bien importado
import { Request } from 'express';

interface RequestWithUser extends Request {
user?: { role: Role };
}

@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}

canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);

if (!requiredRoles || requiredRoles.length === 0) {
return true;
}

const request = context.switchToHttp().getRequest<RequestWithUser>();
const user = request.user;

console.log('Required roles:', requiredRoles);
console.log('User:', user);

if (!user) {
throw new ForbiddenException('Access denied: No user found in request.');
}

if (!requiredRoles.includes(user.role)) {
throw new ForbiddenException(
`Access denied: You must have one of the following roles: ${requiredRoles.join(', ')}`,
);
}

return true;
}
}
18 changes: 18 additions & 0 deletions src/email/email.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EmailController } from './email.controller';

describe('EmailController', () => {
let controller: EmailController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [EmailController],
}).compile();

controller = module.get<EmailController>(EmailController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
97 changes: 97 additions & 0 deletions src/email/email.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import {
Controller,
Post,
Get,
Put,
Delete,
Body,
Param,
HttpCode,
UseGuards,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiNotFoundResponse,
} from '@nestjs/swagger';
import { EmailService } from './email.service';
import { EmailTemplate } from './entities/email-template.entity';
import { AuthGuard } from 'src/auth/auth.guard';
import { RolesGuard } from 'src/auth/roles.guard';
import { Roles } from 'src/auth/roles.decorador';
import { Role } from 'src/auth/rol.enum'; // Importa el enum Role

@ApiTags('Email Template')
@Controller('email')
export class EmailController {
constructor(private readonly emailService: EmailService) {}

@Post()
@UseGuards(AuthGuard, RolesGuard)
@Roles(Role.ADMIN)
@ApiOperation({ summary: 'Create an email template' })
@ApiResponse({
status: 201,
description: 'Template created successfully',
type: EmailTemplate,
})
async create(
@Body() createTemplateDto: { name: string; html: string },
): Promise<EmailTemplate> {
return await this.emailService.createTemplate(createTemplateDto);
}

@Get()
@UseGuards(AuthGuard, RolesGuard)
@Roles(Role.ADMIN)
@ApiOperation({ summary: 'Get all email templates' })
@ApiResponse({
status: 200,
description: 'List of templates',
type: [EmailTemplate],
})
async findAll(): Promise<EmailTemplate[]> {
return await this.emailService.findAllTemplates();
}

@Get(':name')
@UseGuards(AuthGuard, RolesGuard)
@Roles(Role.ADMIN)
@ApiOperation({ summary: 'Get a template by name' })
@ApiResponse({
status: 200,
description: 'Template found',
type: EmailTemplate,
})
@ApiNotFoundResponse({ description: 'Template not found' })
async findByName(@Param('name') name: string): Promise<EmailTemplate> {
return await this.emailService.findTemplateByName(name);
}

@Put(':id')
@UseGuards(AuthGuard, RolesGuard)
@Roles(Role.ADMIN)
@ApiOperation({ summary: 'Update an email template' })
@ApiResponse({
status: 200,
description: 'Updated template',
type: EmailTemplate,
})
async update(
@Param('id') id: string,
@Body() updateData: Partial<EmailTemplate>,
): Promise<EmailTemplate> {
return await this.emailService.updateTemplate(id, updateData);
}

@Delete(':id')
@UseGuards(AuthGuard, RolesGuard)
@Roles(Role.ADMIN)
@HttpCode(204)
@ApiOperation({ summary: 'Delete an email template' })
@ApiResponse({ status: 204, description: 'Template removed' })
async remove(@Param('id') id: string): Promise<void> {
return await this.emailService.removeTemplate(id);
}
}
13 changes: 12 additions & 1 deletion src/email/email.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { Module } from '@nestjs/common';
import { Module, forwardRef } from '@nestjs/common';
import { EmailService } from './email.service';
import { ResendHelper } from './resend/resend.helper';
import { TypeOrmModule } from '@nestjs/typeorm';
import { EmailTemplate } from './entities/email-template.entity';
import { EmailController } from './email.controller';
import { AuthModule } from 'src/auth/auth.module';
import { UserModule } from 'src/user/user.module';

@Module({
imports: [
TypeOrmModule.forFeature([EmailTemplate]),
forwardRef(() => AuthModule),
forwardRef(() => UserModule),
],
providers: [EmailService, ResendHelper],
controllers: [EmailController],
exports: [EmailService],
})
export class EmailModule {}
Loading