Commit e627251c authored by Phạm Quang Bảo's avatar Phạm Quang Bảo

feat(challenge_4): add api send otp and verify otp

parent 96ed263f
...@@ -20,10 +20,12 @@ ...@@ -20,10 +20,12 @@
}, },
"dependencies": { "dependencies": {
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"dotenv": "^17.4.2",
"express": "^5.2.1", "express": "^5.2.1",
"express-automatic-routes": "^1.1.0", "express-automatic-routes": "^1.1.0",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"module-alias": "^2.3.4", "module-alias": "^2.3.4",
"nodemailer": "^8.0.7",
"pg": "^8.20.0", "pg": "^8.20.0",
"pg-hstore": "^2.3.4", "pg-hstore": "^2.3.4",
"sequelize": "^6.37.8", "sequelize": "^6.37.8",
...@@ -36,6 +38,7 @@ ...@@ -36,6 +38,7 @@
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/jsonwebtoken": "^9.0.10", "@types/jsonwebtoken": "^9.0.10",
"@types/node": "^25.7.0", "@types/node": "^25.7.0",
"@types/nodemailer": "^8.0.0",
"@types/swagger-jsdoc": "^6.0.4", "@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.8", "@types/swagger-ui-express": "^4.1.8",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
......
...@@ -11,6 +11,9 @@ importers: ...@@ -11,6 +11,9 @@ importers:
bcrypt: bcrypt:
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.0 version: 6.0.0
dotenv:
specifier: ^17.4.2
version: 17.4.2
express: express:
specifier: ^5.2.1 specifier: ^5.2.1
version: 5.2.1 version: 5.2.1
...@@ -23,6 +26,9 @@ importers: ...@@ -23,6 +26,9 @@ importers:
module-alias: module-alias:
specifier: ^2.3.4 specifier: ^2.3.4
version: 2.3.4 version: 2.3.4
nodemailer:
specifier: ^8.0.7
version: 8.0.7
pg: pg:
specifier: ^8.20.0 specifier: ^8.20.0
version: 8.20.0 version: 8.20.0
...@@ -54,6 +60,9 @@ importers: ...@@ -54,6 +60,9 @@ importers:
'@types/node': '@types/node':
specifier: ^25.7.0 specifier: ^25.7.0
version: 25.7.0 version: 25.7.0
'@types/nodemailer':
specifier: ^8.0.0
version: 8.0.0
'@types/swagger-jsdoc': '@types/swagger-jsdoc':
specifier: ^6.0.4 specifier: ^6.0.4
version: 6.0.4 version: 6.0.4
...@@ -308,6 +317,9 @@ packages: ...@@ -308,6 +317,9 @@ packages:
'@types/node@25.7.0': '@types/node@25.7.0':
resolution: {integrity: sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==} resolution: {integrity: sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==}
'@types/nodemailer@8.0.0':
resolution: {integrity: sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA==}
'@types/qs@6.15.1': '@types/qs@6.15.1':
resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==}
...@@ -449,6 +461,10 @@ packages: ...@@ -449,6 +461,10 @@ packages:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
dotenv@17.4.2:
resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==}
engines: {node: '>=12'}
dottie@2.0.7: dottie@2.0.7:
resolution: {integrity: sha512-7lAK2A0b3zZr3UC5aE69CPdCFR4RHW1o2Dr74TqFykxkUCBXSRJum/yPc7g8zRHJqWKomPLHwFLLoUnn8PXXRg==} resolution: {integrity: sha512-7lAK2A0b3zZr3UC5aE69CPdCFR4RHW1o2Dr74TqFykxkUCBXSRJum/yPc7g8zRHJqWKomPLHwFLLoUnn8PXXRg==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
...@@ -697,6 +713,10 @@ packages: ...@@ -697,6 +713,10 @@ packages:
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
hasBin: true hasBin: true
nodemailer@8.0.7:
resolution: {integrity: sha512-pkjE4mkBzQjdJT4/UmlKl3pX0rC9fZmjh7c6C9o7lv66Ac6w9WCnzPzhbPNxwZAzlF4mdq4CSWB5+FbK6FWCow==}
engines: {node: '>=6.0.0'}
object-inspect@1.13.4: object-inspect@1.13.4:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
...@@ -1196,6 +1216,10 @@ snapshots: ...@@ -1196,6 +1216,10 @@ snapshots:
dependencies: dependencies:
undici-types: 7.21.0 undici-types: 7.21.0
'@types/nodemailer@8.0.0':
dependencies:
'@types/node': 25.7.0
'@types/qs@6.15.1': {} '@types/qs@6.15.1': {}
'@types/range-parser@1.2.7': {} '@types/range-parser@1.2.7': {}
...@@ -1322,6 +1346,8 @@ snapshots: ...@@ -1322,6 +1346,8 @@ snapshots:
dependencies: dependencies:
esutils: 2.0.3 esutils: 2.0.3
dotenv@17.4.2: {}
dottie@2.0.7: {} dottie@2.0.7: {}
dunder-proto@1.0.1: dunder-proto@1.0.1:
...@@ -1596,6 +1622,8 @@ snapshots: ...@@ -1596,6 +1622,8 @@ snapshots:
node-gyp-build@4.8.4: {} node-gyp-build@4.8.4: {}
nodemailer@8.0.7: {}
object-inspect@1.13.4: {} object-inspect@1.13.4: {}
on-finished@2.4.1: on-finished@2.4.1:
......
import { LoginProvider } from "#providers/LoginProvider.js"; import { AuthService } from "#services/authService";
import { Application } from "express" import { Application } from "express"
import { Resource } from "express-automatic-routes" import { Resource } from "express-automatic-routes"
export default (_express: Application) => { export default (_express: Application) => {
const loginProvider = new LoginProvider(); const authService = new AuthService();
return <Resource>{ return <Resource>{
/** /**
...@@ -31,7 +31,7 @@ export default (_express: Application) => { ...@@ -31,7 +31,7 @@ export default (_express: Application) => {
post: { post: {
handler: async (req, res) => { handler: async (req, res) => {
try { try {
const login = await loginProvider.loginUser(req.body); const login = await authService.loginUser(req.body);
return res.status(200).json(login); return res.status(200).json(login);
} catch (error) { } catch (error) {
......
import { authMiddleware } from "#middlewares/authorization"; import { authMiddleware } from "#middlewares/authorization";
import { ProfileProvider } from "#providers/ProfileProvider.js"; import { AuthService } from "#services/authService";
import { Application } from "express"; import { Application } from "express";
import { Resource } from "express-automatic-routes"; import { Resource } from "express-automatic-routes";
export default (_express: Application) => { export default (_express: Application) => {
const profileProvider = new ProfileProvider(); const authService = new AuthService();
return <Resource>{ return <Resource>{
/** /**
...@@ -30,7 +30,7 @@ export default (_express: Application) => { ...@@ -30,7 +30,7 @@ export default (_express: Application) => {
try { try {
const userId = (req as any).user.id; const userId = (req as any).user.id;
const profile = await profileProvider.profileProvider(userId); const profile = await authService.profileUser(userId);
return res.status(200).json(profile); return res.status(200).json(profile);
} catch (error) { } catch (error) {
......
import { RegisterProvider } from "#providers/RegisterProvider.js"; import { AuthService } from "#services/authService";
import { Application } from "express"; import { Application } from "express";
import { Resource } from "express-automatic-routes"; import { Resource } from "express-automatic-routes";
export default (_express: Application) => { export default (_express: Application) => {
const registerProvider = new RegisterProvider(); const authService = new AuthService();
return <Resource>{ return <Resource>{
/** /**
...@@ -41,7 +41,7 @@ export default (_express: Application) => { ...@@ -41,7 +41,7 @@ export default (_express: Application) => {
return res.status(400).json({ error: 'Mật khẩu phải có ít nhất 6 ký tự' }); return res.status(400).json({ error: 'Mật khẩu phải có ít nhất 6 ký tự' });
} }
const register = await registerProvider.registerUser(req.body); const register = await authService.registerUser(req.body);
return res.status(201).json(register); return res.status(201).json(register);
} catch (error) { } catch (error) {
console.error('Error registering user:', error); console.error('Error registering user:', error);
......
import { authMiddleware } from "#middlewares/authorization";
import { AuthService } from "#services/authService.js";
import { MailService } from "#services/mailService.js";
import { Application } from "express";
import { Resource } from "express-automatic-routes";
export default (_express: Application) => {
const authService = new AuthService();
const mailService = new MailService();
return <Resource>{
/**
* @openapi
* /api/v1.0/auth/send-otp:
* post:
* tags: [Auth]
* security:
* - bearerAuth: []
* description: Gửi mã OTP đến email của người dùng
* responses:
* 201:
* description: Gửi mã OTP thành công
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/sendOtpResponse"
*/
post: {
middleware: [authMiddleware],
handler: async (req, res) => {
try {
const userId = (req as any).user.id;
const email = (req as any).user.email;
// get otp
const otp = await authService.otpUser(userId);
// send mail
const mailResult = await mailService.sendOtpEmail(email, otp);
if (!mailResult || !mailResult.success) {
console.error('Gửi mail thất bại:', mailResult?.error);
return res.status(500).json({
email: email,
message: 'Không thể gửi email OTP. Vui lòng thử lại sau.',
error: mailResult?.error
});
}
return res.status(200).json({
email: email,
message: 'OTP đã được gửi đến email của bạn'
});
} catch (error) {
return res.status(500).json({ error: (error as Error).message });
}
}
}
}
}
\ No newline at end of file
import { authMiddleware } from "#middlewares/authorization";
import { AuthService } from "#services/authService.js";
import { Application } from "express";
import { Resource } from "express-automatic-routes";
export default (_express: Application) => {
const authService = new AuthService();
return <Resource>{
/**
* @openapi
* /api/v1.0/auth/verify-otp:
* post:
* tags: [Auth]
* security:
* - bearerAuth: []
* description: Xác minh mã OTP của người dùng
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/verifyOtpInput"
* responses:
* 201:
* description: Xác minh mã OTP thành công
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/verifyOtpResponse"
*/
post: {
middleware: [authMiddleware],
handler: async (req, res) => {
try {
const userId = (req as any).user.id;
const verifyResult = await authService.verifyOtp(userId, req.body.otp);
res.status(201).json(verifyResult);
} catch (error) {
return res.status(500).json({ error: (error as Error).message });
}
}
}
}
}
\ No newline at end of file
...@@ -308,6 +308,48 @@ ...@@ -308,6 +308,48 @@
"example": "123 Main St, Anytown, USA" "example": "123 Main St, Anytown, USA"
} }
} }
},
"sendOtpResponse": {
"type": "object",
"example": {
"email": "phamquangbao@example.com",
"message": "OTP đã được gửi đến email của bạn"
},
"properties": {
"email": {
"type": "string",
"format": "email",
"example": "phamquangbao@example.com"
},
"message": {
"type": "string",
"example": "OTP đã được gửi đến email của bạn"
}
}
},
"verifyOtpResponse": {
"type": "object",
"example": {
"message": "Xác minh OTP thành công"
},
"properties": {
"message": {
"type": "string",
"example": "Xác minh OTP thành công"
}
}
},
"verifyOtpInput": {
"type": "object",
"example": {
"otp": "123456"
},
"properties": {
"otp": {
"type": "string",
"example": "123456"
}
}
} }
}, },
"parameters": { "parameters": {
...@@ -437,6 +479,72 @@ ...@@ -437,6 +479,72 @@
} }
} }
}, },
"/api/v1.0/auth/send-otp": {
"post": {
"tags": [
"Auth"
],
"security": [
{
"bearerAuth": []
}
],
"description": "Gửi mã OTP đến email của người dùng",
"responses": {
"201": {
"description": "Gửi mã OTP thành công",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/sendOtpResponse"
}
}
}
}
}
}
}
},
"/api/v1.0/auth/verify-otp": {
"post": {
"tags": [
"Auth"
],
"security": [
{
"bearerAuth": []
}
],
"description": "Xác minh mã OTP của người dùng",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/verifyOtpInput"
}
}
}
},
"responses": {
"201": {
"description": "Xác minh mã OTP thành công",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/verifyOtpResponse"
}
}
}
}
}
}
}
},
"/api/v1.0/classes/{id}": { "/api/v1.0/classes/{id}": {
"get": { "get": {
"tags": [ "tags": [
......
...@@ -2,8 +2,11 @@ import express from 'express'; ...@@ -2,8 +2,11 @@ import express from 'express';
import { resolve } from 'path'; import { resolve } from 'path';
import _autoroutes from 'express-automatic-routes'; import _autoroutes from 'express-automatic-routes';
import swaggerUi from 'swagger-ui-express'; import swaggerUi from 'swagger-ui-express';
import dotenv from 'dotenv';
import swaggerFile from '#/docs/swagger/swagger-output.json'; import swaggerFile from '#/docs/swagger/swagger-output.json';
dotenv.config();
const app = express() const app = express()
const port = 3000 const port = 3000
......
...@@ -2,7 +2,7 @@ import jwt from 'jsonwebtoken'; ...@@ -2,7 +2,7 @@ import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
//demo //demo
const JWT_SECRET = '1234567890'; const JWT_SECRET = process.env.JWT_SECRET || '';
export const authMiddleware = (req: any, res: Response, next: NextFunction) => { export const authMiddleware = (req: any, res: Response, next: NextFunction) => {
const authHeader = req.headers.authorization; const authHeader = req.headers.authorization;
......
...@@ -7,11 +7,14 @@ export interface user_authAttributes { ...@@ -7,11 +7,14 @@ export interface user_authAttributes {
user_id?: string; user_id?: string;
password_hash?: string; password_hash?: string;
created_at?: Date; created_at?: Date;
otp_code?: string;
otp_expiry?: Date;
active?: boolean;
} }
export type user_authPk = "id"; export type user_authPk = "id";
export type user_authId = user_auth[user_authPk]; export type user_authId = user_auth[user_authPk];
export type user_authOptionalAttributes = "id" | "user_id" | "password_hash" | "created_at"; export type user_authOptionalAttributes = "id" | "user_id" | "password_hash" | "created_at" | "otp_code" | "otp_expiry" | "active";
export type user_authCreationAttributes = Optional<user_authAttributes, user_authOptionalAttributes>; export type user_authCreationAttributes = Optional<user_authAttributes, user_authOptionalAttributes>;
export class user_auth extends Model<user_authAttributes, user_authCreationAttributes> implements user_authAttributes { export class user_auth extends Model<user_authAttributes, user_authCreationAttributes> implements user_authAttributes {
...@@ -19,6 +22,9 @@ export class user_auth extends Model<user_authAttributes, user_authCreationAttri ...@@ -19,6 +22,9 @@ export class user_auth extends Model<user_authAttributes, user_authCreationAttri
user_id?: string; user_id?: string;
password_hash?: string; password_hash?: string;
created_at?: Date; created_at?: Date;
otp_code?: string;
otp_expiry?: Date;
active?: boolean;
// user_auth belongsTo users via user_id // user_auth belongsTo users via user_id
user!: users; user!: users;
...@@ -50,6 +56,18 @@ export class user_auth extends Model<user_authAttributes, user_authCreationAttri ...@@ -50,6 +56,18 @@ export class user_auth extends Model<user_authAttributes, user_authCreationAttri
type: DataTypes.DATE, type: DataTypes.DATE,
allowNull: true, allowNull: true,
defaultValue: Sequelize.Sequelize.fn('now') defaultValue: Sequelize.Sequelize.fn('now')
},
otp_code: {
type: DataTypes.STRING(6),
allowNull: true
},
otp_expiry: {
type: DataTypes.DATE,
allowNull: true
},
active: {
type: DataTypes.BOOLEAN,
allowNull: true
} }
}, { }, {
tableName: 'user_auth', tableName: 'user_auth',
......
import { models } from "#models/sequelize-config.js";
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
interface LoginInput {
email: string;
password: string;
}
export class LoginProvider {
// demo
private readonly JWT_SECRET = '1234567890';
async loginUser(input: LoginInput) {
const user = await models.users.findOne({
where: { email: input.email },
});
if (!user) {
throw new Error('Email hoặc mật khẩu không đúng');
}
const auth = await models.user_auth.findOne({
where: { user_id: user?.id }
});
const passwordMatch = await bcrypt.compare(input.password, auth?.password_hash || '');
if (!passwordMatch) {
throw new Error('Email hoặc mật khẩu không đúng');
}
const payload = {
id: user.id,
email: user.email,
role_id: user.role_id
};
const token = jwt.sign(payload, this.JWT_SECRET, {
expiresIn: '1h',
});
return { accessToken: token };
}
}
\ No newline at end of file
import { models } from "#models/sequelize-config.js";
export class ProfileProvider {
async profileProvider(userId: number) {
const user = await models.users.findByPk(userId);
if (!user) {
throw new Error('Người dùng không tồn tại');
}
return user;
}
}
\ No newline at end of file
import { models, sequelize } from '#models/sequelize-config.js';
import bcrypt from 'bcrypt';
interface RegisterInput {
name: string;
email: string;
password: string;
}
export class RegisterProvider {
async registerUser(input: RegisterInput) {
const salt = await bcrypt.genSalt(10);
const passwordHash = await bcrypt.hash(input.password, salt);
try {
const result = await sequelize.transaction(async (t) => {
const registeredUser = await models.users.create(
{
name: input.name,
email: input.email,
},
{ transaction: t }
);
await models.user_auth.create(
{
user_id: registeredUser.id,
password_hash: passwordHash,
},
{ transaction: t }
);
return {
id: registeredUser.id,
name: registeredUser.name,
email: registeredUser.email,
};
});
return result;
} catch (error) {
console.error('Error during user registration:', error);
throw new Error('Failed to register user');
}
}
}
\ No newline at end of file
import { models, sequelize } from "#models/sequelize-config.js";
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
interface LoginInput {
email: string;
password: string;
}
interface RegisterInput {
name: string;
email: string;
password: string;
}
export class AuthService {
// demo
private readonly JWT_SECRET = process.env.JWT_SECRET || '';
async loginUser(input: LoginInput) {
const user = await models.users.findOne({
where: { email: input.email },
});
if (!user) {
throw new Error('Email hoặc mật khẩu không đúng');
}
const auth = await models.user_auth.findOne({
where: { user_id: user?.id }
});
const passwordMatch = await bcrypt.compare(input.password, auth?.password_hash || '');
if (!passwordMatch) {
throw new Error('Email hoặc mật khẩu không đúng');
}
const payload = {
id: user.id,
email: user.email,
role_id: user.role_id
};
const token = jwt.sign(payload, this.JWT_SECRET, {
expiresIn: '1h',
});
return { accessToken: token };
}
async registerUser(input: RegisterInput) {
const salt = await bcrypt.genSalt(10);
const passwordHash = await bcrypt.hash(input.password, salt);
try {
const result = await sequelize.transaction(async (t) => {
const registeredUser = await models.users.create(
{
name: input.name,
email: input.email,
},
{ transaction: t }
);
await models.user_auth.create(
{
user_id: registeredUser.id,
password_hash: passwordHash,
},
{ transaction: t }
);
return {
id: registeredUser.id,
name: registeredUser.name,
email: registeredUser.email,
};
});
return result;
} catch (error) {
console.error('Error during user registration:', error);
throw new Error('Failed to register user');
}
}
async profileUser(userId: number) {
const user = await models.users.findByPk(userId);
if (!user) {
throw new Error('Người dùng không tồn tại');
}
return user;
}
// otp
async otpUser(userId: string) {
// create otp
const generateOTP = () => {
const otp = crypto.randomInt(100000, 1000000);
return otp.toString().padStart(6, '0');
};
try {
const otpCode = generateOTP();
await models.user_auth.update(
{
otp_code: otpCode,
otp_expiry: new Date(Date.now() + 5 * 60 * 1000),
active: false,
},
{
where: { user_id: userId },
},
);
return otpCode;
} catch (error) {
console.error('Error during OTP generation:', error);
throw new Error('Failed to generate OTP');
}
}
// verify otp
async verifyOtp(userId: string, otp: string) {
try {
const userAuth = await models.user_auth.findOne(
{ where: { user_id: userId, otp_code: otp } },
);
if (!userAuth) {
throw new Error('Mã OTP không đúng');
} else if (userAuth.otp_expiry && userAuth.otp_expiry < new Date()) {
throw new Error('Mã OTP đã hết hạn');
} else {
await models.user_auth.update(
{
otp_code: '',
otp_expiry: null as any,
active: true,
},
{ where: { user_id: userId } }
);
return { message: 'Xác minh OTP thành công' };
}
} catch (error) {
console.error('Error during OTP verification:', error);
throw new Error('Failed to verify OTP');
}
}
}
\ No newline at end of file
import nodemailer from 'nodemailer';
export class MailService {
private transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS
}
});
async sendOtpEmail(toEmail: string, otp: string) {
try {
const mailOptions = {
from: `"Hệ thống xác thực" <${process.env.EMAIL_USER}>`,
to: toEmail,
subject: 'Mã xác thực OTP của bạn',
html: `
<div style="font-family: sans-serif; line-height: 1.5; color: #333;">
<h2>Xác minh tài khoản</h2>
<p>Chào bạn,</p>
<p>Mã OTP để hoàn tất đăng ký/đăng nhập của bạn là:</p>
<p style="font-size: 32px; font-weight: bold; color: #007bff; letter-spacing: 4px;">
${otp}
</p>
<p>Mã này sẽ hết hạn sau <b>5 phút</b>.</p>
<p>Nếu bạn không thực hiện yêu cầu này, vui lòng bỏ qua email.</p>
<hr />
<small>Đây là email tự động, vui lòng không trả lời.</small>
</div>
`,
};
await this.transporter.sendMail(mailOptions);
return {
success: true,
};
} catch (error) {
console.error('Lỗi gửi mail:', error);
return { success: false, error: (error as Error).message };
}
};
}
\ No newline at end of file
...@@ -6,6 +6,8 @@ import type { Options } from 'swagger-jsdoc'; ...@@ -6,6 +6,8 @@ import type { Options } from 'swagger-jsdoc';
import registerSchemas from './register/schemas.js'; import registerSchemas from './register/schemas.js';
import loginSchemas from './login/schemas.js'; import loginSchemas from './login/schemas.js';
import authProfileSchemas from './authProfile/schema.js'; import authProfileSchemas from './authProfile/schema.js';
import sendOTPSchemas from './sendOTP/schema.js';
import verifyOTPSchemas from './verifyOTP/schema.js';
const swaggerOptions: Options = { const swaggerOptions: Options = {
definition: { definition: {
...@@ -28,6 +30,8 @@ const swaggerOptions: Options = { ...@@ -28,6 +30,8 @@ const swaggerOptions: Options = {
...registerSchemas, ...registerSchemas,
...loginSchemas, ...loginSchemas,
...authProfileSchemas, ...authProfileSchemas,
...sendOTPSchemas,
...verifyOTPSchemas,
}, },
parameters: { parameters: {
filters: { filters: {
......
const sendOTPSchemas = {
sendOtpResponse: {
type: 'object',
example: {
email: 'phamquangbao@example.com',
message: 'OTP đã được gửi đến email của bạn',
},
properties: {
email: {
type: 'string',
format: 'email',
example: 'phamquangbao@example.com',
},
message: {
type: 'string',
example: 'OTP đã được gửi đến email của bạn',
},
},
},
};
export default sendOTPSchemas;
\ No newline at end of file
const verifyOTPSchemas = {
verifyOtpResponse: {
type: 'object',
example: {
message: 'Xác minh OTP thành công',
},
properties: {
message: {
type: 'string',
example: 'Xác minh OTP thành công',
},
},
},
verifyOtpInput: {
type: 'object',
example: {
otp: '123456',
},
properties: {
otp: {
type: 'string',
example: '123456',
},
},
},
};
export default verifyOTPSchemas;
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment