Skip to content

Commit

Permalink
Merge pull request #9 from anasabbal/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
anasabbal authored Jun 21, 2024
2 parents e161dd9 + 71402c7 commit 3650ab2
Show file tree
Hide file tree
Showing 11 changed files with 268 additions and 190 deletions.
149 changes: 0 additions & 149 deletions apps/user-service/src/email.service.ts

This file was deleted.

76 changes: 76 additions & 0 deletions apps/user-service/src/email/email.service.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
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<boolean> {
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<VerificationToken | null> {
try {
return await this.emailRepository.findByToken(token);
} catch (error) {
this.logger.error(`Error finding email confirmation: ${error.message}`);
throw error;
}
}
}
27 changes: 27 additions & 0 deletions apps/user-service/src/email/mailer.transport.ts
Original file line number Diff line number Diff line change
@@ -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;
36 changes: 36 additions & 0 deletions apps/user-service/src/email/utils.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
const mailOptions = {
from: `"Company" <${process.env.COMPANY_EMAIL}>`,
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>`,
};

try {
const info = await this.transporter.sendMail(mailOptions);
return true;
} catch (error) {
console.error(`Error sending email: ${error.message}`);
return false;
}
}
}
4 changes: 1 addition & 3 deletions apps/user-service/src/models/user.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ export type UserDocument = User & Document;
timestamps: true,
})
export class User {
save(): User | PromiseLike<User> {
throw new Error('Method not implemented.');
}

@Prop({ required: true })
firstName: string;

Expand Down
6 changes: 3 additions & 3 deletions apps/user-service/src/models/user.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
46 changes: 46 additions & 0 deletions apps/user-service/src/repository/email.repository.ts
Original file line number Diff line number Diff line change
@@ -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<VerificationTokenDocument>,
) {}

async findByEmail(email: string): Promise<VerificationToken | null> {
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<VerificationToken | null> {
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<void> {
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<void> {
try {
await this.verificationTokenModel.deleteOne({ email }).exec();
} catch (error) {
throw new Error(`Error deleting email verification for ${email}: ${error.message}`);
}
}
}
Loading

0 comments on commit 3650ab2

Please sign in to comment.