diff --git a/package.json b/package.json index 2cecc1d..9a40a5e 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,10 @@ "author": "Abdou-Raouf ATARMLA", "license": "ISC", "dependencies": { + "@types/connect-flash": "^0.0.40", + "@types/express-session": "^1.18.0", + "bcrypt": "^5.1.1", + "connect-flash": "^0.1.1", "cors": "^2.8.5", "dotenv": "^16.4.5", "ejs": "^3.1.10", @@ -39,7 +43,10 @@ "express-brute": "^1.0.1", "express-brute-mongo": "^1.0.0", "express-brute-redis": "^0.0.1", + "express-list-endpoints": "^7.1.0", "express-rate-limit": "^7.3.1", + "express-session": "^1.18.0", + "handlebars": "^4.7.8", "helmet": "^7.1.0", "ioredis": "^5.4.1", "joi": "^17.13.3", @@ -52,12 +59,14 @@ "devDependencies": { "@commitlint/cli": "^19.3.0", "@commitlint/config-conventional": "^19.2.2", + "@types/bcrypt": "^5.0.2", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/express-brute": "^1.0.5", "@types/express-brute-mongo": "^0.0.39", "@types/jsonwebtoken": "^9.0.6", "@types/morgan": "^1.9.9", + "@types/nodemailer": "^6.4.15", "@typescript-eslint/eslint-plugin": "^5.57.1", "@typescript-eslint/parser": "^5.57.1", "eslint": "^8.56.0", diff --git a/src/app/controllers/auth.controller.ts b/src/app/controllers/auth.controller.ts new file mode 100644 index 0000000..3332a55 --- /dev/null +++ b/src/app/controllers/auth.controller.ts @@ -0,0 +1,163 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Request, Response, NextFunction } from 'express'; +import ApiResponse from '../utils/handlers/api-reponse'; +import { ErrorResponseType } from '../utils/types'; +import AuthService from '../services/auth.service'; + +class AuthController { + static async register( + req: Request, + res: Response, + next: NextFunction, + ): Promise { + try { + const response = await AuthService.register(req.body); + if (response.success) { + ApiResponse.success(res, response, 201); + } else { + throw response; + } + } catch (error) { + ApiResponse.error(res, error as ErrorResponseType); + } + } + + static async verifyAccount( + req: Request, + res: Response, + next: NextFunction, + ): Promise { + try { + const response = await AuthService.verifyAccount(req.body); + if (response.success) { + ApiResponse.success(res, response); + } else { + throw response; + } + } catch (error) { + ApiResponse.error(res, error as ErrorResponseType); + } + } + + static async loginWithPassword( + req: Request, + res: Response, + next: NextFunction, + ): Promise { + try { + const response = await AuthService.loginWithPassword(req.body); + if (response.success) { + ApiResponse.success(res, response); + } else { + throw response; + } + } catch (error) { + ApiResponse.error(res, error as ErrorResponseType); + } + } + + static async generateLoginOtp( + req: Request, + res: Response, + next: NextFunction, + ): Promise { + try { + const response = await AuthService.generateLoginOtp(req.body.email); + if (response.success) { + ApiResponse.success(res, response); + } else { + throw response; + } + } catch (error) { + ApiResponse.error(res, error as ErrorResponseType); + } + } + + static async loginWithOtp( + req: Request, + res: Response, + next: NextFunction, + ): Promise { + try { + const response = await AuthService.loginWithOtp(req.body); + if (response.success) { + ApiResponse.success(res, response); + } else { + throw response; + } + } catch (error) { + ApiResponse.error(res, error as ErrorResponseType); + } + } + + static async refreshToken( + req: Request, + res: Response, + next: NextFunction, + ): Promise { + try { + const response = await AuthService.refresh(req.body.refreshToken); + if (response.success) { + ApiResponse.success(res, response); + } else { + throw response; + } + } catch (error) { + ApiResponse.error(res, error as ErrorResponseType); + } + } + + static async logout( + req: Request, + res: Response, + next: NextFunction, + ): Promise { + try { + const { accessToken, refreshToken } = req.body; + const response = await AuthService.logout(accessToken, refreshToken); + if (response.success) { + ApiResponse.success(res, response, 202); + } else { + throw response; + } + } catch (error) { + ApiResponse.error(res, error as ErrorResponseType); + } + } + + static async forgotPassword( + req: Request, + res: Response, + next: NextFunction, + ): Promise { + try { + const response = await AuthService.forgotPassword(req.body.email); + if (response.success) { + ApiResponse.success(res, response); + } else { + throw response; + } + } catch (error) { + ApiResponse.error(res, error as ErrorResponseType); + } + } + + static async resetPassword( + req: Request, + res: Response, + next: NextFunction, + ): Promise { + try { + const response = await AuthService.resetPassword(req.body); + if (response.success) { + ApiResponse.success(res, response); + } else { + throw response; + } + } catch (error) { + ApiResponse.error(res, error as ErrorResponseType); + } + } +} + +export default AuthController; diff --git a/src/app/controllers/user.controller.ts b/src/app/controllers/user.controller.ts index 3c602e8..de4c9ea 100644 --- a/src/app/controllers/user.controller.ts +++ b/src/app/controllers/user.controller.ts @@ -38,6 +38,60 @@ class UserController { ApiResponse.error(res, error as ErrorResponseType); } } + + static async getUserById( + req: Request, + res: Response, + next: NextFunction, + ): Promise { + try { + const userId = req.params.id; + const response = await UserService.findOne({ + _id: userId, + }); + + if (response.success) { + ApiResponse.success(res, response); + } else { + throw response; + } + } catch (error) { + ApiResponse.error(res, error as ErrorResponseType); + } + } } export default UserController; + +/** + * static async createUser( + req: Request, + res: Response, + next: NextFunction, + ): Promise { + try { + const response = (await UserService.create( + req.body, + )) as SuccessResponseType; + if (response.success) { + // Send email to the newly created user + try { + await mailService.sendMail({ + to: response.document.email, + subject: 'Welcome to Our Service', + text: 'Your account has been successfully created. Welcome aboard!', + }); + console.log('Welcome email sent successfully'); + } catch (emailError) { + console.error('Error sending welcome email:', emailError); + } + + ApiResponse.success(res, response, 201); + } else { + throw response; + } + } catch (error) { + ApiResponse.error(res, error as ErrorResponseType); + } + } + */ diff --git a/src/app/routes/auth.routes.ts b/src/app/routes/auth.routes.ts new file mode 100644 index 0000000..5571c4e --- /dev/null +++ b/src/app/routes/auth.routes.ts @@ -0,0 +1,16 @@ +import { Router } from 'express'; +import AuthController from '../controllers/auth.controller'; + +const router = Router(); + +router.post('/register', AuthController.register); +router.post('/verify-account', AuthController.verifyAccount); +router.post('/login', AuthController.loginWithPassword); +router.post('/generate-login-otp', AuthController.generateLoginOtp); +router.post('/login-with-otp', AuthController.loginWithOtp); +router.post('/refresh-token', AuthController.refreshToken); +router.post('/logout', AuthController.logout); +router.post('/forgot-password', AuthController.forgotPassword); +router.patch('/reset-password', AuthController.resetPassword); + +export default router; diff --git a/src/app/routes/routes.ts b/src/app/routes/routes.ts index c5b8c35..0e949d0 100644 --- a/src/app/routes/routes.ts +++ b/src/app/routes/routes.ts @@ -2,11 +2,13 @@ import { Router } from 'express'; import userRouter from './user.routes'; import appRoutes from './app.routes'; import otpRoutes from './otp.routes'; +import authRoutes from './auth.routes'; const router = Router(); router.use('/', appRoutes); router.use('/users', userRouter); router.use('/otp', otpRoutes); +router.use('/auth', authRoutes); export default router; diff --git a/src/app/routes/user.routes.ts b/src/app/routes/user.routes.ts index 8956de1..d7225b3 100644 --- a/src/app/routes/user.routes.ts +++ b/src/app/routes/user.routes.ts @@ -3,13 +3,10 @@ import UserController from '../controllers/user.controller'; import { validate } from '../utils/middlewares/validate'; import { createUserSchema } from '../utils/validators/user'; -const userRouter = Router(); +const router = Router(); -userRouter.post('/', validate(createUserSchema), (req, res, next) => - UserController.createUser(req, res, next), -); -userRouter.get('/', (req, res, next) => - UserController.getAllUsers(req, res, next), -); +router.post('/', validate(createUserSchema), UserController.createUser); +router.get('/', UserController.getAllUsers); +router.get('/:id', UserController.getUserById); -export default userRouter; +export default router; diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts index 167af66..1319f04 100644 --- a/src/app/services/auth.service.ts +++ b/src/app/services/auth.service.ts @@ -1,42 +1,62 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import UserService from '../services/user.service'; +import OTPService from '../services/otp.service'; +import JwtService from './shared/jwt.service'; +import MailServiceUtilities from './shared/mail/mail.service.utility'; import { - SuccessResponseType, ErrorResponseType, + IOTPModel, IUserModel, + SuccessResponseType, } from '../utils/types'; import ErrorResponse from '../utils/handlers/error/response'; -import JwtService from './shared/jwt.service'; +import config from '../../config'; class AuthService { - static async register( + async register( payload: any, ): Promise | ErrorResponseType> { try { const { email } = payload; - const existingUserResponse = (await UserService.findOne({ + const userResponse = (await UserService.findOne({ email, })) as SuccessResponseType; - if (existingUserResponse.success && existingUserResponse.document) { - return { - success: false, - error: new ErrorResponse( - 'VALIDATION_ERROR', - 'The entered email is already registered.', - ), - }; + + if (userResponse.success && userResponse.document) { + throw new ErrorResponse( + 'UNIQUE_FIELD_ERROR', + 'The entered email is already registered.', + ); } const createUserResponse = (await UserService.create( payload, )) as SuccessResponseType; - if (!createUserResponse.success) { - return createUserResponse; + + if (!createUserResponse.success || !createUserResponse.document) { + throw createUserResponse.error; } - // TODO: Generate and send OTP for account verification + await MailServiceUtilities.sendAccountCreationEmail({ + to: email, + firstname: createUserResponse.document.firstname, + }); + + const otpResponse = (await OTPService.generate( + email, + config.otp.purposes.ACCOUNT_VERIFICATION.code, + )) as SuccessResponseType; - return { success: true, document: createUserResponse.document }; + if (!otpResponse.success || !otpResponse.document) { + throw otpResponse.error; + } + + return { + success: true, + document: { + user: createUserResponse.document, + otp: otpResponse.document, + }, + }; } catch (error) { return { success: false, @@ -51,37 +71,89 @@ class AuthService { } } - static async verifyAccount( + async verifyAccount( payload: any, - ): Promise | ErrorResponseType> { + ): Promise | ErrorResponseType> { try { const { email, code } = payload; - - const isVerifiedResponse = (await UserService.isVerified( + const userResponse = (await UserService.findOne({ email, - )) as SuccessResponseType; - if (isVerifiedResponse.success && isVerifiedResponse.document?.verified) { - return { - success: false, - error: new ErrorResponse( - 'VALIDATION_ERROR', - 'Account is already verified.', - ), - }; + })) as SuccessResponseType; + + if (!userResponse.success || !userResponse.document) { + throw new ErrorResponse('NOT_FOUND_ERROR', 'User not found.'); } - // TODO: Validate OTP for account verification + if (userResponse.document.verified) { + return { success: true }; // If already verified, return success without further actions + } - const verifyUserResponse = (await UserService.markAsVerified( + const validateOtpResponse = await OTPService.validate( email, - )) as SuccessResponseType; + code, + config.otp.purposes.ACCOUNT_VERIFICATION.code, + ); + + if (!validateOtpResponse.success) { + throw validateOtpResponse.error; + } + + const verifyUserResponse = await UserService.markAsVerified(email); + if (!verifyUserResponse.success) { - return verifyUserResponse; + throw verifyUserResponse.error; + } + + return { success: true }; + } catch (error) { + return { + success: false, + error: + error instanceof ErrorResponse + ? error + : new ErrorResponse( + 'INTERNAL_SERVER_ERROR', + (error as Error).message, + ), + }; + } + } + + async generateLoginOtp( + email: string, + ): Promise | ErrorResponseType> { + try { + const userResponse = (await UserService.findOne({ + email, + })) as SuccessResponseType; + + if (!userResponse.success || !userResponse.document) { + throw new ErrorResponse('NOT_FOUND_ERROR', 'User not found.'); + } + + const user = userResponse.document; + + if (!user.verified) { + throw new ErrorResponse('UNAUTHORIZED', 'Unverified account.'); + } + + if (!user.active) { + throw new ErrorResponse( + 'FORBIDDEN', + 'Inactive account, please contact admins.', + ); } - // TODO: Send verification success email + const otpResponse = await OTPService.generate( + email, + config.otp.purposes.LOGIN_CONFIRMATION.code, + ); - return { success: true, document: verifyUserResponse.document }; + if (!otpResponse.success) { + throw otpResponse.error; + } + + return otpResponse; } catch (error) { return { success: false, @@ -96,52 +168,41 @@ class AuthService { } } - static async login( + async loginWithPassword( payload: any, ): Promise | ErrorResponseType> { try { const { email, password } = payload; - const existingUserResponse = (await UserService.findOne({ + const userResponse = (await UserService.findOne({ email, })) as SuccessResponseType; - if (!existingUserResponse.success || !existingUserResponse.document) { - return { - success: false, - error: new ErrorResponse('UNAUTHORIZED', 'Invalid credentials.'), - }; - } - const user = existingUserResponse.document; + if (!userResponse.success || !userResponse.document) { + throw new ErrorResponse('UNAUTHORIZED', 'Invalid credentials.'); + } + const user = userResponse.document; const isValidPasswordResponse = (await UserService.isValidPassword( user.id, password, )) as SuccessResponseType<{ isValid: boolean }>; + if ( !isValidPasswordResponse.success || !isValidPasswordResponse.document?.isValid ) { - return { - success: false, - error: new ErrorResponse('UNAUTHORIZED', 'Invalid credentials.'), - }; + throw new ErrorResponse('UNAUTHORIZED', 'Invalid credentials.'); } if (!user.verified) { - return { - success: false, - error: new ErrorResponse('UNAUTHORIZED', 'Unverified account.'), - }; + throw new ErrorResponse('UNAUTHORIZED', 'Unverified account.'); } if (!user.active) { - return { - success: false, - error: new ErrorResponse( - 'FORBIDDEN', - 'Inactive account, please contact admins.', - ), - }; + throw new ErrorResponse( + 'FORBIDDEN', + 'Inactive account, please contact admins.', + ); } const accessToken = await JwtService.signAccessToken(user.id); @@ -149,7 +210,10 @@ class AuthService { return { success: true, - document: { token: { accessToken, refreshToken }, user }, + document: { + token: { access: accessToken, refresh: refreshToken }, + user, + }, }; } catch (error) { return { @@ -165,24 +229,82 @@ class AuthService { } } - static async refresh( + async loginWithOtp( + payload: any, + ): Promise | ErrorResponseType> { + try { + const { email, code } = payload; + const userResponse = (await UserService.findOne({ + email, + })) as SuccessResponseType; + + if (!userResponse.success || !userResponse.document) { + throw new ErrorResponse('UNAUTHORIZED', 'Invalid credentials.'); + } + + const user = userResponse.document; + + const validateOtpResponse = await OTPService.validate( + email, + code, + config.otp.purposes.LOGIN_CONFIRMATION.code, + ); + + if (!validateOtpResponse.success) { + throw validateOtpResponse.error; + } + + if (!user.verified) { + throw new ErrorResponse('UNAUTHORIZED', 'Unverified account.'); + } + + if (!user.active) { + throw new ErrorResponse( + 'FORBIDDEN', + 'Inactive account, please contact admins.', + ); + } + + const accessToken = await JwtService.signAccessToken(user.id); + const refreshToken = await JwtService.signRefreshToken(user.id); + + return { + success: true, + document: { + token: { access: accessToken, refresh: refreshToken }, + user, + }, + }; + } catch (error) { + return { + success: false, + error: + error instanceof ErrorResponse + ? error + : new ErrorResponse( + 'INTERNAL_SERVER_ERROR', + (error as Error).message, + ), + }; + } + } + + async refresh( refreshToken: string, ): Promise | ErrorResponseType> { try { if (!refreshToken) { - return { - success: false, - error: new ErrorResponse('BAD_REQUEST', 'Refresh token is required.'), - }; + throw new ErrorResponse('BAD_REQUEST', 'Refresh token is required.'); } const userId = await JwtService.verifyRefreshToken(refreshToken); const accessToken = await JwtService.signAccessToken(userId); + // Refresh token change to ensure rotation const newRefreshToken = await JwtService.signRefreshToken(userId); return { success: true, - document: { token: { accessToken, refreshToken: newRefreshToken } }, + document: { token: { access: accessToken, refresh: newRefreshToken } }, }; } catch (error) { return { @@ -198,21 +320,37 @@ class AuthService { } } - static async logout( + async logout( + accessToken: string, refreshToken: string, ): Promise | ErrorResponseType> { try { - if (!refreshToken) { - return { - success: false, - error: new ErrorResponse('BAD_REQUEST', 'Refresh token is required.'), - }; + if (!refreshToken || !accessToken) { + throw new ErrorResponse( + 'BAD_REQUEST', + 'Refresh and access token are required.', + ); } - const userId = await JwtService.verifyRefreshToken(refreshToken); - await JwtService.removeFromRedis(userId); + const { userId: userIdFromRefresh } = + await JwtService.checkRefreshToken(refreshToken); + const { userId: userIdFromAccess } = + await JwtService.checkAccessToken(accessToken); - return { success: true, document: null }; + if (userIdFromRefresh !== userIdFromAccess) { + throw new ErrorResponse( + 'UNAUTHORIZED', + 'Access token does not match refresh token.', + ); + } + + // Blacklist the access token + await JwtService.blacklistToken(accessToken); + + // Remove the refresh token from Redis + await JwtService.removeFromRedis(userIdFromRefresh); + + return { success: true }; } catch (error) { return { success: false, @@ -227,49 +365,45 @@ class AuthService { } } - static async forgotPassword( + async forgotPassword( email: string, ): Promise | ErrorResponseType> { try { if (!email) { - return { - success: false, - error: new ErrorResponse('BAD_REQUEST', 'Email should be provided.'), - }; + throw new ErrorResponse('BAD_REQUEST', 'Email should be provided.'); } - const existingUserResponse = (await UserService.findOne({ + const userResponse = (await UserService.findOne({ email, })) as SuccessResponseType; - if (!existingUserResponse.success || !existingUserResponse.document) { - return { - success: false, - error: new ErrorResponse('NOT_FOUND_ERROR', 'User not found.'), - }; + + if (!userResponse.success || !userResponse.document) { + throw new ErrorResponse('NOT_FOUND_ERROR', 'User not found.'); } - const user = existingUserResponse.document; + const user = userResponse.document; if (!user.verified) { - return { - success: false, - error: new ErrorResponse('UNAUTHORIZED', 'Unverified account.'), - }; + throw new ErrorResponse('UNAUTHORIZED', 'Unverified account.'); } if (!user.active) { - return { - success: false, - error: new ErrorResponse( - 'FORBIDDEN', - 'Inactive account, please contact admins.', - ), - }; + throw new ErrorResponse( + 'FORBIDDEN', + 'Inactive account, please contact admins.', + ); } - // TODO: Generate and send OTP for password reset + const otpResponse = await OTPService.generate( + email, + config.otp.purposes.FORGOT_PASSWORD.code, + ); + + if (!otpResponse.success) { + throw otpResponse.error; + } - return { success: true, document: null }; + return { success: true }; } catch (error) { return { success: false, @@ -284,52 +418,54 @@ class AuthService { } } - static async resetPassword( + async resetPassword( payload: any, ): Promise | ErrorResponseType> { try { + // We suppose a verification about new password and confirmation password have already been done const { email, code, newPassword } = payload; - const existingUserResponse = (await UserService.findOne({ + const userResponse = (await UserService.findOne({ email, })) as SuccessResponseType; - if (!existingUserResponse.success || !existingUserResponse.document) { - return { - success: false, - error: new ErrorResponse('NOT_FOUND_ERROR', 'User not found.'), - }; + + if (!userResponse.success || !userResponse.document) { + throw new ErrorResponse('NOT_FOUND_ERROR', 'User not found.'); } - const user = existingUserResponse.document; + const user = userResponse.document; if (!user.verified) { - return { - success: false, - error: new ErrorResponse('UNAUTHORIZED', 'Unverified account.'), - }; + throw new ErrorResponse('UNAUTHORIZED', 'Unverified account.'); } if (!user.active) { - return { - success: false, - error: new ErrorResponse( - 'FORBIDDEN', - 'Inactive account, please contact admins.', - ), - }; + throw new ErrorResponse( + 'FORBIDDEN', + 'Inactive account, please contact admins.', + ); } - // TODO: Validate OTP for password reset + const validateOtpResponse = await OTPService.validate( + email, + code, + config.otp.purposes.FORGOT_PASSWORD.code, + ); - const updatePasswordResponse = (await UserService.updatePassword( + if (!validateOtpResponse.success) { + throw validateOtpResponse.error; + } + + const updatePasswordResponse = await UserService.updatePassword( user.id, newPassword, - )) as SuccessResponseType; + ); + if (!updatePasswordResponse.success) { - return updatePasswordResponse; + throw updatePasswordResponse.error; } - return { success: true, document: null }; + return { success: true }; } catch (error) { return { success: false, @@ -345,4 +481,4 @@ class AuthService { } } -export default AuthService; +export default new AuthService(); diff --git a/src/app/services/otp.service.ts b/src/app/services/otp.service.ts index 2cb4c84..63b0ffe 100644 --- a/src/app/services/otp.service.ts +++ b/src/app/services/otp.service.ts @@ -12,7 +12,7 @@ import UserService from './user.service'; import { generateRandomOTP } from '../../helpers/generator'; import { BaseService } from './base.service'; import { IUserModel } from '../utils/types'; -import MailService from './shared/mail.service'; +import MailServiceUtilities from './shared/mail/mail.service.utility'; class OTPService extends BaseService { constructor() { @@ -43,7 +43,7 @@ class OTPService extends BaseService { purpose, }); - const mailResponse = await MailService.sendOtp({ + const mailResponse = await MailServiceUtilities.sendOtp({ to: user.email, code: otp.code, purpose, diff --git a/src/app/services/shared/jwt.service.ts b/src/app/services/shared/jwt.service.ts index 06adace..c5e9d86 100644 --- a/src/app/services/shared/jwt.service.ts +++ b/src/app/services/shared/jwt.service.ts @@ -245,6 +245,68 @@ class JwtService { }); }); } + + checkAccessToken(accessToken: string): Promise<{ userId: string }> { + return new Promise((resolve, reject) => { + JWT.verify( + accessToken, + this.accessTokenSecret, + (err: any, payload: any) => { + if (err) { + const message = + err.name === 'JsonWebTokenError' ? 'Unauthorized' : err.message; + const errorResponse = new ErrorResponse('UNAUTHORIZED', message); + return reject(errorResponse); + } + + const userId = payload?.aud as string; + + resolve({ userId }); + }, + ); + }); + } + + checkRefreshToken(refreshToken: string): Promise<{ userId: string }> { + return new Promise((resolve, reject) => { + JWT.verify( + refreshToken, + this.refreshTokenSecret, + (err: any, payload: any) => { + if (err) { + const errorResponse = new ErrorResponse( + 'UNAUTHORIZED', + 'Unauthorized', + ); + return reject(errorResponse); + } + + const userId = payload?.aud as string; + + client.get(userId, (redisErr: any, result: any) => { + if (redisErr) { + console.error(redisErr.message); + const errorResponse = new ErrorResponse( + 'INTERNAL_SERVER_ERROR', + 'Internal Server Error', + ); + return reject(errorResponse); + } + + if (refreshToken === result) { + return resolve({ userId }); + } + + const errorResponse = new ErrorResponse( + 'UNAUTHORIZED', + 'Unauthorized', + ); + return reject(errorResponse); + }); + }, + ); + }); + } } export default new JwtService(); diff --git a/src/app/services/shared/mail.service.ts b/src/app/services/shared/mail/mail.service.ts similarity index 70% rename from src/app/services/shared/mail.service.ts rename to src/app/services/shared/mail/mail.service.ts index cd40645..5b31710 100644 --- a/src/app/services/shared/mail.service.ts +++ b/src/app/services/shared/mail/mail.service.ts @@ -2,9 +2,9 @@ import nodemailer, { Transporter } from 'nodemailer'; import handlebars from 'handlebars'; import fs from 'fs'; import path from 'path'; -import config from '../../../config'; -import { ErrorResponseType, SuccessResponseType } from '../../utils/types'; -import ErrorResponse from '../../utils/handlers/error/response'; +import config from '../../../../config'; +import { ErrorResponseType, SuccessResponseType } from '../../../utils/types'; +import ErrorResponse from '../../../utils/handlers/error/response'; class MailService { private transporter: Transporter; @@ -45,7 +45,7 @@ class MailService { if (htmlTemplate) { const templatePath = path.join( __dirname, - '../../templates/mail/', + '../../../../templates/mail/', `${htmlTemplate}.html`, ); const templateSource = fs.readFileSync(templatePath, 'utf-8'); @@ -78,31 +78,6 @@ class MailService { }; } } - - async sendOtp({ - to, - code, - purpose, - }: { - to: string; - code: string; - purpose: string; - }): Promise | ErrorResponseType> { - const otpPurpose = config.otp.purposes[purpose]; - if (!otpPurpose) { - return { - success: false, - error: new ErrorResponse('BAD_REQUEST', 'Invalid OTP purpose provided'), - }; - } - - const subject = otpPurpose.title; - const text = `${otpPurpose.message} ${code}\n\nThis code is valid for ${ - config.otp.expiration / 60000 - } minutes.`; - - return await this.sendMail({ to, subject, text }); - } } export default new MailService(); diff --git a/src/app/services/shared/mail/mail.service.utility.ts b/src/app/services/shared/mail/mail.service.utility.ts new file mode 100644 index 0000000..010e865 --- /dev/null +++ b/src/app/services/shared/mail/mail.service.utility.ts @@ -0,0 +1,52 @@ +import config from '../../../../config'; +import ErrorResponse from '../../../utils/handlers/error/response'; +import { ErrorResponseType, SuccessResponseType } from '../../../utils/types'; +import MailService from './mail.service'; + +class MailServiceUtilities { + static async sendOtp({ + to, + code, + purpose, + }: { + to: string; + code: string; + purpose: string; + }): Promise | ErrorResponseType> { + const otpPurpose = config.otp.purposes[purpose]; + if (!otpPurpose) { + return { + success: false, + error: new ErrorResponse('BAD_REQUEST', 'Invalid OTP purpose provided'), + }; + } + + const subject = otpPurpose.title; + const text = `${otpPurpose.message} ${code}\n\nThis code is valid for ${ + config.otp.expiration / 60000 + } minutes.`; + + return await MailService.sendMail({ to, subject, text }); + } + + static async sendAccountCreationEmail({ + to, + firstname, + }: { + to: string; + firstname: string; + }): Promise | ErrorResponseType> { + const subject = 'Welcome to Our Service'; + const htmlTemplate = 'welcome'; + const templateData = { firstname }; + + return await MailService.sendMail({ + to, + subject, + htmlTemplate, + templateData, + }); + } +} + +export default MailServiceUtilities; diff --git a/src/app/services/user.service.ts b/src/app/services/user.service.ts index e8dc4a2..7010c12 100644 --- a/src/app/services/user.service.ts +++ b/src/app/services/user.service.ts @@ -67,7 +67,7 @@ class UserService extends BaseService { )) as SuccessResponseType; if (!updateResponse.success) { - throw updateResponse.error!; + throw updateResponse.error; } return { diff --git a/src/templates/mail/welcome.html b/src/templates/mail/welcome.html index dc63d0d..a94c082 100644 --- a/src/templates/mail/welcome.html +++ b/src/templates/mail/welcome.html @@ -1,10 +1,46 @@ - + - Welcome + + + Welcome to Our Service + -

Hello, {{name}}!

-

Welcome to our service. We are glad to have you with us.

+
+

Welcome to Our Service, {{firstname}}!

+

Your account has been successfully created. Welcome aboard!

+

+ Please verify your email address to complete the registration process. +

+

If you have any questions, feel free to contact our support team.

+ +