Skip to content

Commit

Permalink
Merge pull request #4 from anasabbal/NRA-06
Browse files Browse the repository at this point in the history
Nra 06
  • Loading branch information
anasabbal authored Jun 20, 2024
2 parents 8a84378 + 49aa741 commit 981d367
Show file tree
Hide file tree
Showing 14 changed files with 315 additions and 17 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ on:
push:
branches:
- master
- develop
pull_request:
branches:
- master
- develop

jobs:
build:
Expand Down
11 changes: 11 additions & 0 deletions apps/gateway/src/rest/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { UserService } from '../services/user-service';
import { UserCreateCommand } from '@app/shared/commands/auth/user.create.cmd';
import { DriverCreateCmd } from '@app/shared/commands/driver/driver.create.cmd';
import { AuthService } from '../services/auth.service';
import { IResponse } from '@app/shared/interfaces/response.interface';
import { ResponseSuccess } from '@app/shared/dto/response.dto';



Expand All @@ -25,4 +27,13 @@ export class AuthController {
async login(@Body() loginUserDto: any) {
return this.authService.login(loginUserDto);
}
@Get('verify/:token')
async verifyUser(@Param('token') token: string): Promise<IResponse> {
try {
const result = await this.authService.verifyUser(token);
return new ResponseSuccess("EMAIL_CONFIRMATION.VERIFIED_SUCCESSFULLY");
} catch (error) {
throw error;
}
}
}
10 changes: 9 additions & 1 deletion apps/gateway/src/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class AuthService {
const userType = await this.userService.findUserTypeById(userTypeId);
console.log(`User type with id ${userType.id} found`);
if(userType.id == userTypeId){
return await this.driverService.createDriver(command);
return await this.userService.createUser(command, userTypeId);
}else {
return await this.userService.createUser(command, userTypeId);
}
Expand Down Expand Up @@ -57,4 +57,12 @@ export class AuthService {
}
}
}
async verifyUser(token: string): Promise<any> {
try {
const result = await this.userClient.send({ cmd: 'verify-user'}, token).toPromise();
return result;
}catch(error){
throw new InternalServerErrorException('Failed to Verify');
}
}
}
8 changes: 7 additions & 1 deletion apps/user-service/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { AuthService } from './auth.service';
import { UserCreateCommand } from '@app/shared/commands/auth/user.create.cmd';
import { UserLoginCmd } from '@app/shared/commands/auth/user.login.cmd';
import { JwtAuthGuard } from './jwt.guard';
import { ResponseSuccess } from '@app/shared/dto/response.dto';
import { IResponse } from '@app/shared/interfaces/response.interface';

@Controller('auth')
export class AuthController {
Expand All @@ -24,7 +26,6 @@ export class AuthController {
return "User created successfully";
}


@MessagePattern({ cmd: 'login' })
async login(@Payload() req: UserLoginCmd): Promise<any> {
this.logger.log(`Logging in user: ${req.email}`);
Expand All @@ -37,4 +38,9 @@ export class AuthController {
this.logger.log(`Request is: ${req}`);
return req.user;
}
@MessagePattern('verify-user')
async verifyUser(token: string): Promise<IResponse> {
await this.authService.verifyEmail(token);
return new ResponseSuccess("EMAIL_CONFIRMATION.VERIFIED_SUCCESSFULLY");
}
}
13 changes: 8 additions & 5 deletions apps/user-service/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { BadRequestException, Inject, Injectable, Logger, forwardRef } from '@nestjs/common';
import { UserServiceService } from '../user-service.service';
import { JwtService } from '@nestjs/jwt';
import { User } from '../models/user.schema';
import * as bcrypt from 'bcrypt';
import { validate } from 'class-validator';
import { UserCreateCommand } from '@app/shared/commands/auth/user.create.cmd';
import { IResponse } from '@app/shared/interfaces/response.interface';


@Injectable()
export class AuthService {
Expand All @@ -18,14 +18,14 @@ export class AuthService {
private jwtService: JwtService
){}

async register(userTypeId: string, command: UserCreateCommand): Promise<User> {
async register(userTypeId: string, command: UserCreateCommand): Promise<IResponse> {
const errors = await validate(command);
if (errors.length > 0) {
throw new BadRequestException(errors);
}
try {
const newUser = await this.usersService.create(userTypeId, command); // await here
return newUser;
const result = await this.usersService.create(userTypeId, command);
return result;
} catch (error) {
this.logger.error(`Failed to register user: ${command.email}`, error.stack);
throw error;
Expand All @@ -41,5 +41,8 @@ export class AuthService {
email: user.email
};
}
async verifyEmail(token: string): Promise<boolean> {
return this.usersService.verifyUser(token);
}
}

4 changes: 3 additions & 1 deletion apps/user-service/src/auth/config/for-feature.db.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { EmailConfirmation, EmailConfirmationSchema } from "../../models/email.confirmation";
import { User, UserSchema } from "../../models/user.schema";
import { UserType, UserTypeSchema } from "../../models/user.type";

Expand All @@ -6,5 +7,6 @@ import { UserType, UserTypeSchema } from "../../models/user.type";

export default [
{ name: User.name, schema: UserSchema },
{ name: UserType.name, schema: UserTypeSchema }
{ name: UserType.name, schema: UserTypeSchema },
{ name: EmailConfirmation.name, schema: EmailConfirmationSchema}
];
126 changes: 126 additions & 0 deletions apps/user-service/src/email.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common";
import { EmailConfirmation } from "./models/email.confirmation";
import { InjectModel } from "@nestjs/mongoose";
import { Model } from "mongoose";
import * as dotenv from 'dotenv';
import * as nodemailer from 'nodemailer';

dotenv.config();

@Injectable()
export class EmailService {
private readonly logger = new Logger(EmailService.name);

constructor(
@InjectModel('EmailConfirmation') private readonly emailConfirmationModel: Model<EmailConfirmation>
) {}

async createEmailToken(email: string): Promise<boolean> {
try {
const emailVerification = await this.findEmailVerification(email);

if (emailVerification && this.isRecentlySent(emailVerification.timestamp)) {
throw new HttpException('LOGIN.EMAIL_SENT_RECENTLY', HttpStatus.INTERNAL_SERVER_ERROR);
} else {
await this.updateOrCreateEmailVerification(email);
this.logger.debug(`Email token created for ${email}`);
return true;
}
} catch (error) {
this.logger.error(`Error creating email token: ${error.message}`);
throw error; // Re-throw the error to propagate it up the call stack
}
}

async sendEmailVerification(email: string): Promise<boolean> {
try {
const model = await this.findEmailVerification(email);
/*if (!model || !model.emailToken) {
throw new HttpException('REGISTER.USER_NOT_REGISTERED', HttpStatus.FORBIDDEN);
}*/
this.logger.debug(`Email Token : ${model.emailToken}`);

const sent = await this.sendVerificationEmail(email, model.emailToken);
if (sent) {
this.logger.debug(`Email verification sent to ${email}`);
} else {
this.logger.warn(`Failed to send email verification to ${email}`);
}
return sent;
} catch (error) {
this.logger.error(`Error sending email verification: ${error.message}`);
throw error; // Re-throw the error to propagate it up the call stack
}
}


private async findEmailVerification(email: string): Promise<EmailConfirmation | null> {
return await this.emailConfirmationModel.findOne({ email: email }).exec();
}

private isRecentlySent(timestamp: Date): boolean {
if (!timestamp) {
return false; // Return false if timestamp is undefined or null
}
const minutesElapsed = (new Date().getTime() - timestamp.getTime()) / 60000;
return minutesElapsed < 15;
}

private async updateOrCreateEmailVerification(email: string): Promise<void> {
try {
// Generate a new email token
const emailToken = (Math.floor(Math.random() * 9000000) + 1000000).toString();

// Use findOneAndUpdate with upsert: true, and ensure to only update existing properties
await this.emailConfirmationModel.findOneAndUpdate(
{ email: email },
{
emailToken: emailToken,
timestamp: new Date()
},
{
upsert: true, // Create new document if email does not exist
new: true, // Return updated document if upserted
setDefaultsOnInsert: true, // Ensure default values are set on insert
strict: false // Allow fields not in schema (if needed)
}
).exec();

this.logger.debug(`Email verification record updated or created for ${email}`);
} catch (error) {
this.logger.error(`Error updating or creating email verification: ${error.message}`);
throw error; // Re-throw the error to propagate it up the call stack
}
}

private async sendVerificationEmail(email: string, emailToken: string): Promise<boolean> {
const transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST,
port: parseInt(process.env.EMAIL_PORT || '0'), // parse port to integer
secure: process.env.EMAIL_SECURE === 'true', // parse secure to boolean
auth: {
user: process.env.EMAIL_USERNAME,
pass: process.env.EMAIL_PASSWORD
}
});

const mailOptions = {
from: `"Company" <${process.env.COMPANY}>`,
to: email,
subject: 'Verify Email',
text: 'Verify Email',
html: `Hi! <br><br> Thanks for your registration<br><br>` +
`<a href=${process.env.EMAIL_URL}:${process.env.PORT}/auth/verify/${emailToken}>Click here to activate your account</a>`
};
this.logger.debug(`Payload send ${mailOptions.html}`);

try {
const info = await transporter.sendMail(mailOptions);
this.logger.debug(`Message sent: ${info.messageId}`);
return true;
} catch (error) {
this.logger.error(`Error sending email: ${error.message}`);
return false;
}
}
}
24 changes: 24 additions & 0 deletions apps/user-service/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,30 @@
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { UserServiceModule } from './user-service.module';
import * as nodemailer from 'nodemailer';
import * as dotenv from 'dotenv';

dotenv.config();


async function testSMTPConnection() {
const transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST,
port: parseInt(process.env.EMAIL_PORT || '0'), // parse port to integer
secure: process.env.EMAIL_SECURE === 'false', // parse secure to boolean
auth: {
user: process.env.EMAIL_USERNAME,
pass: process.env.EMAIL_PASSWORD
}
});

try {
await transporter.verify(); // Verify connection configuration
console.log('SMTP connection successful.');
} catch (error) {
console.error('Error connecting to SMTP server:', error);
}
}
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
UserServiceModule,
Expand All @@ -13,5 +36,6 @@ async function bootstrap() {
},
);
await app.listen();
testSMTPConnection();
}
bootstrap();
22 changes: 22 additions & 0 deletions apps/user-service/src/models/email.confirmation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Schema, SchemaFactory } from "@nestjs/mongoose";



export type EmailConfirmationDocument = EmailConfirmation & Document;


@Schema({
toJSON: {
getters: true,
virtuals: true,
},
timestamps: true,
})
export class EmailConfirmation {
email: string;
emailToken: string;

timestamp: Date
}

export const EmailConfirmationSchema = SchemaFactory.createForClass(EmailConfirmation);
5 changes: 5 additions & 0 deletions apps/user-service/src/models/user.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export class User {

@Prop({ type: UserTypeSchema })
userType: UserType;

@Prop({ type: Object, default: {} })
auth: {
email: { valid: boolean }
};
}

export const UserSchema = SchemaFactory.createForClass(User);
3 changes: 2 additions & 1 deletion apps/user-service/src/user-service.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AuthModule } from './auth/auth.module';
import forFeatureDb from './auth/config/for-feature.db';
import { DatabaseModule } from '@app/database';
import * as dotenv from 'dotenv';
import { EmailService } from './email.service';

dotenv.config();

Expand All @@ -17,7 +18,7 @@ dotenv.config();
forwardRef(() => AuthModule),
],
controllers: [UserServiceController],
providers: [UserServiceService],
providers: [UserServiceService, EmailService],
exports: [UserServiceService],
})
export class UserServiceModule implements OnModuleInit{
Expand Down
Loading

0 comments on commit 981d367

Please sign in to comment.