diff --git a/apps/user-service/src/email.service.ts b/apps/user-service/src/email.service.ts deleted file mode 100644 index ea48957..0000000 --- a/apps/user-service/src/email.service.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; -import { InjectModel } from '@nestjs/mongoose'; -import { Model } from 'mongoose'; -import * as dotenv from 'dotenv'; -import * as nodemailer from 'nodemailer'; -import { VerificationTokenDocument, VerificationToken } from './models/email.confirmation'; - -dotenv.config(); - -@Injectable() -export class EmailService { - private readonly logger = new Logger(EmailService.name); - - constructor( - @InjectModel(VerificationToken.name) private readonly verificationTokenModel: Model, - ) {} - - async createEmailToken(email: string): Promise { - 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; - } - } - - async sendEmailVerification(email: string): Promise { - try { - const model = await this.findEmailVerification(email); - if (!model || !model.token) { - throw new HttpException('REGISTER.USER_NOT_REGISTERED', HttpStatus.FORBIDDEN); - } - this.logger.debug(`Email Token: ${model.token}`); - - const sent = await this.sendVerificationEmail(email, model.token); - 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; - } - } - - private async findEmailVerification(email: string): Promise { - try { - const result = await this.verificationTokenModel.findOne({ email }).exec(); - this.logger.debug(`Email verification found for ${email}`); - return result; - } catch (error) { - this.logger.error(`Error finding email verification: ${error.message}`); - throw error; - } - } - - private isRecentlySent(timestamp: Date): boolean { - if (!timestamp) { - return false; - } - const minutesElapsed = (new Date().getTime() - new Date(timestamp).getTime()) / 60000; - return minutesElapsed < 15; - } - - private async updateOrCreateEmailVerification(email: string): Promise { - try { - const emailToken = (Math.floor(Math.random() * 9000000) + 1000000).toString(); - - const filter = { email }; - const update = { - token: emailToken, - timestamp: new Date(), - }; - const options = { - upsert: true, - new: true, - setDefaultsOnInsert: true, - }; - - const result = await this.verificationTokenModel.findOneAndUpdate(filter, update, options).exec(); - if (result) { - this.logger.debug(`Email verification record updated or created for ${email}`); - } else { - this.logger.error(`Failed to update or create email verification record for ${email}`); - throw new Error(`Failed to update or create email verification record for ${email}`); - } - } catch (error) { - this.logger.error(`Error updating or creating email verification: ${error.message}`); - throw error; - } - } - - private async sendVerificationEmail(email: string, emailToken: string): Promise { - const transporter = nodemailer.createTransport({ - host: process.env.EMAIL_HOST, - port: parseInt(process.env.EMAIL_PORT || '0', 10), - secure: process.env.EMAIL_SECURE === 'true', - auth: { - user: process.env.EMAIL_USERNAME, - pass: process.env.EMAIL_PASSWORD, - }, - }); - - const mailOptions = { - from: `"Company" <${process.env.COMPANY_EMAIL}>`, - to: email, - subject: 'Verify Email', - text: 'Verify Email', - html: `Hi!

Thanks for your registration

` + - `Click here to activate your account`, - }; - - this.logger.debug(`Payload sent: ${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; - } - } - - async findEmailConfirmationWithToken(token: string): Promise { - try { - const emailConfirmation = await this.verificationTokenModel.findOne({ token }).exec(); - if (emailConfirmation) { - this.logger.debug(`Email confirmation found for token: ${token}`); - } else { - this.logger.debug(`Email confirmation not found for token: ${token}`); - } - return emailConfirmation; - } catch (error) { - this.logger.error(`Error finding email confirmation: ${error.message}`); - throw error; - } - } -} diff --git a/apps/user-service/src/email/email.service.ts b/apps/user-service/src/email/email.service.ts new file mode 100644 index 0000000..9cf6967 --- /dev/null +++ b/apps/user-service/src/email/email.service.ts @@ -0,0 +1,76 @@ +import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; +import * as dotenv from 'dotenv'; +import { EmailVerificationRepository } from '../repository/email.repository'; +import { EmailUtils } from './utils'; +import { VerificationToken } from '../models/email.confirmation'; + +dotenv.config(); + +@Injectable() +export class EmailService { + private readonly logger = new Logger(EmailService.name); + + constructor( + private readonly emailRepository: EmailVerificationRepository, + private readonly emailUtils: EmailUtils, + ) {} + + async createEmailToken(email: string): Promise { + try { + const emailVerification = await this.emailRepository.findByEmail(email); + + if (emailVerification && this.isRecentlySent(emailVerification.timestamp)) { + throw new HttpException('LOGIN.EMAIL_SENT_RECENTLY', HttpStatus.INTERNAL_SERVER_ERROR); + } else { + await this.emailRepository.upsertVerificationToken(email, this.generateEmailToken(), new Date()); + this.logger.debug(`Email token created for ${email}`); + return true; + } + } catch (error) { + this.logger.error(`Error creating email token: ${error.message}`); + throw error; + } + } + + async sendEmailVerification(email: string): Promise { + try { + const model = await this.emailRepository.findByEmail(email); + if (!model || !model.token) { + throw new HttpException('REGISTER.USER_NOT_REGISTERED', HttpStatus.FORBIDDEN); + } + this.logger.debug(`Email Token: ${model.token}`); + + const sent = await this.emailUtils.sendVerificationEmail(email, model.token); + 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; + } + } + + private isRecentlySent(timestamp: Date): boolean { + if (!timestamp) { + return false; + } + const minutesElapsed = (new Date().getTime() - new Date(timestamp).getTime()) / 60000; + return minutesElapsed < 15; + } + + private generateEmailToken(): string { + return (Math.floor(Math.random() * 9000000) + 1000000).toString(); + } + + async findEmailConfirmationWithToken(token: string): Promise { + try { + return await this.emailRepository.findByToken(token); + } catch (error) { + this.logger.error(`Error finding email confirmation: ${error.message}`); + throw error; + } + } +} \ No newline at end of file diff --git a/apps/user-service/src/email/mailer.transport.ts b/apps/user-service/src/email/mailer.transport.ts new file mode 100644 index 0000000..affcb06 --- /dev/null +++ b/apps/user-service/src/email/mailer.transport.ts @@ -0,0 +1,27 @@ +import nodemailer from 'nodemailer'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +class NodemailerTransporter { + private static instance: nodemailer.Transporter | null = null; + + private constructor() {} // Prevents instantiation + + static getInstance(): nodemailer.Transporter { + if (!NodemailerTransporter.instance) { + NodemailerTransporter.instance = nodemailer.createTransport({ + host: process.env.EMAIL_HOST, + port: parseInt(process.env.EMAIL_PORT || '0', 10), + secure: process.env.EMAIL_SECURE === 'true', + auth: { + user: process.env.EMAIL_USERNAME, + pass: process.env.EMAIL_PASSWORD, + }, + }); + } + return NodemailerTransporter.instance; + } +} + +export default NodemailerTransporter; diff --git a/apps/user-service/src/email/utils.ts b/apps/user-service/src/email/utils.ts new file mode 100644 index 0000000..246195a --- /dev/null +++ b/apps/user-service/src/email/utils.ts @@ -0,0 +1,36 @@ +import * as nodemailer from 'nodemailer'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + + +export class EmailUtils { + private transporter = nodemailer.createTransport({ + host: process.env.EMAIL_HOST, + port: parseInt(process.env.EMAIL_PORT || '0', 10), + secure: process.env.EMAIL_SECURE === 'true', + auth: { + user: process.env.EMAIL_USERNAME, + pass: process.env.EMAIL_PASSWORD, + }, + }); + + async sendVerificationEmail(email: string, emailToken: string): Promise { + const mailOptions = { + from: `"Company" <${process.env.COMPANY_EMAIL}>`, + to: email, + subject: 'Verify Email', + text: 'Verify Email', + html: `Hi!

Thanks for your registration

` + + `Click here to activate your account`, + }; + + try { + const info = await this.transporter.sendMail(mailOptions); + return true; + } catch (error) { + console.error(`Error sending email: ${error.message}`); + return false; + } + } +} diff --git a/apps/user-service/src/models/user.schema.ts b/apps/user-service/src/models/user.schema.ts index 83d407c..38ef8d5 100644 --- a/apps/user-service/src/models/user.schema.ts +++ b/apps/user-service/src/models/user.schema.ts @@ -12,9 +12,7 @@ export type UserDocument = User & Document; timestamps: true, }) export class User { - save(): User | PromiseLike { - throw new Error('Method not implemented.'); - } + @Prop({ required: true }) firstName: string; diff --git a/apps/user-service/src/models/user.type.ts b/apps/user-service/src/models/user.type.ts index fe01805..dd585d4 100644 --- a/apps/user-service/src/models/user.type.ts +++ b/apps/user-service/src/models/user.type.ts @@ -5,10 +5,10 @@ import { Document } from 'mongoose'; @Schema() export class UserType { - - - @Prop({ required: true}) + @Prop({ required: true }) type: string; } +export type UserTypeDocument = UserType & Document; + export const UserTypeSchema = SchemaFactory.createForClass(UserType); diff --git a/apps/user-service/src/repository/email.repository.ts b/apps/user-service/src/repository/email.repository.ts new file mode 100644 index 0000000..a864cd0 --- /dev/null +++ b/apps/user-service/src/repository/email.repository.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { VerificationToken, VerificationTokenDocument } from '../models/email.confirmation'; + +@Injectable() +export class EmailVerificationRepository { + constructor( + @InjectModel(VerificationToken.name) private readonly verificationTokenModel: Model, + ) {} + + async findByEmail(email: string): Promise { + try { + return await this.verificationTokenModel.findOne({ email }).exec(); + } catch (error) { + throw new Error(`Error finding email verification by email ${email}: ${error.message}`); + } + } + + async findByToken(token: string): Promise { + try { + return await this.verificationTokenModel.findOne({ token }).exec(); + } catch (error) { + throw new Error(`Error finding email verification by token ${token}: ${error.message}`); + } + } + + async upsertVerificationToken(email: string, token: string, timestamp: Date): Promise { + try { + const filter = { email }; + const update = { token, timestamp }; + const options = { upsert: true, new: true, setDefaultsOnInsert: true }; + await this.verificationTokenModel.findOneAndUpdate(filter, update, options).exec(); + } catch (error) { + throw error; + } + } + + async deleteByEmail(email: string): Promise { + try { + await this.verificationTokenModel.deleteOne({ email }).exec(); + } catch (error) { + throw new Error(`Error deleting email verification for ${email}: ${error.message}`); + } + } +} diff --git a/apps/user-service/src/repository/user-type.repository.ts b/apps/user-service/src/repository/user-type.repository.ts new file mode 100644 index 0000000..f1aac05 --- /dev/null +++ b/apps/user-service/src/repository/user-type.repository.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { UserType, UserTypeDocument } from '../models/user.type'; + +@Injectable() +export class UserTypeRepository { + constructor(@InjectModel(UserType.name) private readonly userTypeModel: Model) {} + + async findById(userTypeId: string): Promise { + try { + return await this.userTypeModel.findById(userTypeId).exec(); + } catch (error) { + throw error; + } + } + + async findAll(): Promise { + try { + return await this.userTypeModel.find().exec(); + } catch (error) { + throw error; + } + } + + async findByType(type: string): Promise { + try { + return await this.userTypeModel.findOne({ type }).exec(); + } catch (error) { + throw error; + } + } + + async createUserType(userType: Partial): Promise { + try { + const newUserType = new this.userTypeModel(userType); + return await newUserType.save(); + } catch (error) { + throw error; + } + } +} diff --git a/apps/user-service/src/user-service.controller.ts b/apps/user-service/src/user-service.controller.ts index 6366691..3b4cc87 100644 --- a/apps/user-service/src/user-service.controller.ts +++ b/apps/user-service/src/user-service.controller.ts @@ -5,6 +5,7 @@ import { User } from './models/user.schema'; import { GetUserEvent } from '@app/shared/events/user/user.get'; import { UserTypeDto } from '@app/shared/events/user/user.type.dto'; + @Controller('users') export class UserServiceController { constructor(private readonly userService: UserServiceService) {} diff --git a/apps/user-service/src/user-service.module.ts b/apps/user-service/src/user-service.module.ts index a494cc3..4f84a61 100644 --- a/apps/user-service/src/user-service.module.ts +++ b/apps/user-service/src/user-service.module.ts @@ -6,8 +6,11 @@ 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'; +import { EmailService } from './email/email.service'; import { UserRepository } from './repository/user.repository'; +import { EmailVerificationRepository } from './repository/email.repository'; +import { UserTypeRepository } from './repository/user-type.repository'; +import { EmailUtils } from './email/utils'; dotenv.config(); @@ -19,7 +22,7 @@ dotenv.config(); forwardRef(() => AuthModule), ], controllers: [UserServiceController], - providers: [UserServiceService, EmailService, UserRepository], + providers: [UserServiceService, EmailService, UserRepository, EmailVerificationRepository, UserTypeRepository, EmailUtils], exports: [UserServiceService], }) export class UserServiceModule implements OnModuleInit{ diff --git a/apps/user-service/src/user-service.service.ts b/apps/user-service/src/user-service.service.ts index 6a5bb5c..7667677 100644 --- a/apps/user-service/src/user-service.service.ts +++ b/apps/user-service/src/user-service.service.ts @@ -1,16 +1,14 @@ import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; -import { InjectModel } from '@nestjs/mongoose'; -import { Model } from 'mongoose'; import { User } from './models/user.schema'; import * as bcrypt from 'bcrypt'; import { UserTypeDto } from '@app/shared/events/user/user.type.dto'; import { GetUserEvent } from '@app/shared/events/user/user.get'; -import { UserType } from './models/user.type'; -import { UserCreateCommand } from '@app/shared/commands/auth/user.create.cmd'; -import { EmailService } from './email.service'; import { ResponseSuccess } from '@app/shared/dto/response.dto'; import { IResponse } from '@app/shared/interfaces/response.interface'; import { UserRepository } from './repository/user.repository'; +import { EmailService } from './email/email.service'; +import { UserTypeRepository } from './repository/user-type.repository'; +import { UserCreateCommand } from '@app/shared/commands/auth/user.create.cmd'; @@ -21,19 +19,14 @@ export class UserServiceService { constructor( private readonly userRepository: UserRepository, - @InjectModel(User.name) private readonly userModel: Model, - @InjectModel(UserType.name) private readonly userTypeModel: Model, + private readonly userTypeRepository: UserTypeRepository, private readonly emailService: EmailService ) {} - async seedUserTypes() { + async seedUserTypes(): Promise { try { const userTypes = ['Driver', 'User']; - - if (!this.userTypeModel) { - throw new Error('userTypeModel is not initialized'); - } - + for (const type of userTypes) { await this.seedUserType(type); } @@ -42,25 +35,29 @@ export class UserServiceService { } } + private async seedUserType(type: string): Promise { - const existingType = await this.userTypeModel.findOne({ type }).exec(); - if (!existingType) { - const userType = new this.userTypeModel({ type }); - await userType.save(); - this.logger.log(`User type '${type}' seeded successfully.`); - } else { - this.logger.debug(`User type '${type}' already exists.`); + try { + const existingType = await this.userTypeRepository.findByType(type); + if (!existingType) { + const userType = await this.userTypeRepository.createUserType({ type }); + this.logger.log(`User type '${type}' seeded successfully.`); + } else { + this.logger.debug(`User type '${type}' already exists.`); + } + } catch (error) { + this.logger.error(`Error seeding user type '${type}': ${error.message}`); } } async findUserTypeById(userTypeId: string): Promise { try { - const userType = await this.userTypeModel.findById(userTypeId).exec(); + const userType = await this.userTypeRepository.findById(userTypeId); if (!userType) return null; return { id: userType.id, - name: userType.type + name: userType.type, }; } catch (error) { this.logger.error(`Error finding user type by ID: ${error.message}`); @@ -83,26 +80,27 @@ export class UserServiceService { } } - private async createUserInstanceAndSave(userType: UserTypeDto, command: UserCreateCommand, hashedPassword: string): Promise { + async createUserInstanceAndSave(userType: UserTypeDto | null, command: UserCreateCommand, hashedPassword: string): Promise { try { - const newUser = new this.userModel({ + if (!userType) { + throw new Error('User type not found'); + } + + const newUser = { firstName: command.firstName, lastName: command.lastName, email: command.email, password: hashedPassword, - userType: { type: userType.name }, + userType: { type: userType.name }, // Assuming userType.name is correct for your schema verified: false, - }); + }; - const savedUser = await newUser.save(); - this.logger.log(`User '${savedUser.email}' created successfully.`); - return savedUser; + return await this.userRepository.save(newUser); } catch (error) { this.logger.error(`Error creating and saving user: ${error.message}`); throw error; } } - private async sendVerificationEmail(email: string): Promise { try { await this.emailService.createEmailToken(email); @@ -137,7 +135,7 @@ export class UserServiceService { } user.verified = true; - await user.save(); + await this.userRepository.save(user); this.logger.log(`User '${emailConfirmation.email}' verified successfully.`); return true; @@ -190,10 +188,10 @@ export class UserServiceService { async findUserByEmail(email: string): Promise { try { - return await this.userModel.findOne({ email }).exec(); + return await this.userRepository.findByEmail(email); } catch (error) { this.logger.error(`Error finding user by email ${email}: ${error.message}`); throw error; } } -} +} \ No newline at end of file