diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3d36e52..8120725 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -34,6 +34,13 @@ jobs: env: DB_CONNECTION: ${{ secrets.DB_CONNECTION }} TEST_DB: ${{ secrets.TEST_DB }} + SMTP_HOST: ${{ secrets.SMTP_HOST }} + SMTP_USER: ${{ secrets.SMTP_USER }} + SMTP_PASS: ${{ secrets.SMTP_PASS }} + SMTP_PORT: ${{ secrets.SMTP_PORT }} + GOOGLE_CLIENT_ID: ${{secrets.GOOGLE_CLIENT_ID}}, + GOOGLE_CLIENT_SECRET: ${{secrets.GOOGLE_CLIENT_SECRET}}, + GOOGLE_CALLBACK_URL: ${{secrets.GOOGLE_CALLBACK_URL}}, run: npm run test - name: Build application diff --git a/__test__/user.test.ts b/__test__/user.test.ts index b6ab8e4..9323212 100644 --- a/__test__/user.test.ts +++ b/__test__/user.test.ts @@ -3,20 +3,34 @@ import { beforeAll, afterAll, jest, test } from "@jest/globals"; import app from "../src/utils/server"; import User from "../src/sequelize/models/users"; import * as userServices from "../src/services/user.service"; +import * as mailServices from "../src/services/mail.service"; import sequelize, { connect } from "../src/config/dbConnection"; const userData: any = { - name: "yvanna", - username: "testuser", - email: "test1@gmail.com", - password: "test1234", + name: "yvanna5", + username: "testuser5", + email: "test15@gmail.com", + password: "test12345", +}; + +const dummySeller = { + name: "dummyseller", + username: "username", + email: "srukundo02@gmail.com", + password: "1234567890", + role: "seller", +}; +const userTestData = { + newPassword: "Test@123", + confirmPassword: "Test@123", + wrongPassword: "Test456", }; const loginData: any = { email: "test1@gmail.com", password: "test1234", }; -describe("Testing a user Routes", () => { +describe("Testing user Routes", () => { beforeAll(async () => { try { await connect(); @@ -30,17 +44,22 @@ describe("Testing a user Routes", () => { await User.destroy({ truncate: true }); await sequelize.close(); }); + let token: any; describe("Testing user authentication", () => { test("should return 201 and create a new user when registering successfully", async () => { - const response = await request(app).post("/api/v1/users/register").send(userData); + const response = await request(app) + .post("/api/v1/users/register") + .send(userData); expect(response.status).toBe(201); - }, 40000); + }, 20000); test("should return 409 when registering with an existing email", async () => { User.create(userData); - const response = await request(app).post("/api/v1/users/register").send(userData); + const response = await request(app) + .post("/api/v1/users/register") + .send(userData); expect(response.status).toBe(409); - }, 40000); + }, 20000); test("should return 400 when registering with an invalid credential", async () => { const userData = { @@ -48,31 +67,115 @@ describe("Testing a user Routes", () => { name: "", username: "existinguser", }; - const response = await request(app).post("/api/v1/users/register").send(userData); + const response = await request(app) + .post("/api/v1/users/register") + .send(userData); expect(response.status).toBe(400); - }, 40000); + }, 20000); + }); + + test("should return all users in db --> given '/api/v1/users'", async () => { + const spy = jest.spyOn(User, "findAll"); + const spy2 = jest.spyOn(userServices, "getAllUsers"); + const response = await request(app).get("/api/v1/users"); + expect(spy).toHaveBeenCalled(); + expect(spy2).toHaveBeenCalled(); + }, 20000); + test("Should return status 401 to indicate Unauthorized user", async () => { + const loggedInUser = { + email: userData.email, + password: "test", + }; + const spyonOne = jest.spyOn(User, "findOne").mockResolvedValueOnce({ + //@ts-ignore + email: userData.email, + password: loginData.password, + }); + const response = await request(app) + .post("/api/v1/users/login") + .send(loggedInUser); + expect(response.body.status).toBe(401); + spyonOne.mockRestore(); + }, 20000); + + test("Should send otp verification code", async () => { + const spy = jest.spyOn(mailServices, "sendEmailService"); + const response = await request(app).post("/api/v1/users/login").send({ + email: dummySeller.email, + password: dummySeller.password, + }); + + expect(response.body.message).toBe("OTP verification code has been sent ,please use it to verify that it was you"); + // expect(spy).toHaveBeenCalled(); + }, 40000); + + test("should log a user in to retrieve a token", async () => { + const response = await request(app).post("/api/v1/users/login").send({ + email: userData.email, + password: userData.password, + }); + expect(response.status).toBe(200); + token = response.body.token; + }); + + test("should return 400 when adding an extra field while updating password", async () => { + const response = await request(app) + .put("/api/v1/users/passwordupdate") + .send({ + oldPassword: userData.password, + newPassword: userTestData.newPassword, + confirmPassword: userTestData.confirmPassword, + role: "seller", + }) + .set("Authorization", "Bearer " + token); + expect(response.status).toBe(400); + }); + + test("should return 401 when updating password without authorization", async () => { + const response = await request(app) + .put("/api/v1/users/passwordupdate") + .send({ + oldPassword: userData.password, + newPassword: userTestData.newPassword, + confirmPassword: userTestData.confirmPassword, + }); + expect(response.status).toBe(401); + }); + + test("should return 200 when password is updated", async () => { + const response = await request(app) + .put("/api/v1/users/passwordupdate") + .send({ + oldPassword: userData.password, + newPassword: userTestData.newPassword, + confirmPassword: userTestData.confirmPassword, + }) + .set("Authorization", "Bearer " + token); + expect(response.status).toBe(200); + }); + + test("should return 400 when confirm password and new password doesn't match", async () => { + const response = await request(app) + .put("/api/v1/users/passwordupdate") + .send({ + oldPassword: userData.password, + newPassword: userTestData.newPassword, + confirmPassword: userTestData.wrongPassword, + }) + .set("Authorization", "Bearer " + token); + expect(response.status).toBe(400); }); -}); -test("should return all users in db --> given '/api/v1/users'", async () => { - const spy = jest.spyOn(User, "findAll"); - const spy2 = jest.spyOn(userServices, "getAllUsers"); - const response = await request(app).get("/api/v1/users"); - expect(spy).toHaveBeenCalled(); - expect(spy2).toHaveBeenCalled(); -}, 40000); -test("Should return status 401 to indicate Unauthorized user", async () => { - const loggedInUser = { - email: userData.email, - password: "test", - }; - const spyonOne = jest.spyOn(User, "findOne").mockResolvedValueOnce({ - //@ts-ignore - email: userData.email, - password: loginData.password, + test("should return 400 when old password is incorrect", async () => { + const response = await request(app) + .put("/api/v1/users/passwordupdate") + .send({ + oldPassword: userTestData.wrongPassword, + newPassword: userTestData.newPassword, + confirmPassword:userTestData.wrongPassword, + }) + .set("Authorization", "Bearer " + token); + expect(response.status).toBe(400); }); - const response = await request(app).post("/api/v1/users/login").send(loggedInUser); - expect(response.body.status).toBe(401); - spyonOne.mockRestore(); }); diff --git a/src/controllers/userControllers.ts b/src/controllers/userControllers.ts index 6152856..48634f4 100644 --- a/src/controllers/userControllers.ts +++ b/src/controllers/userControllers.ts @@ -1,14 +1,18 @@ import { Request, Response } from "express"; import * as userService from "../services/user.service"; import { generateToken } from "../utils/jsonwebtoken"; -import { comparePasswords } from "../helpers/comparePassword"; -import { loggedInUser } from "../services/user.service"; -import { createUserService } from "../services/user.service"; +import * as mailService from "../services/mail.service"; +import { IUser, STATUS, SUBJECTS } from "../types"; +import { comparePasswords } from "../utils/comparePassword"; +import { loggedInUser} from "../services/user.service"; +import { createUserService, getUserByEmail, updateUserPassword } from "../services/user.service"; +import { hashedPassword } from "../utils/hashPassword"; +import Token, { TokenAttributes } from "../sequelize/models/Token"; +import User from "../sequelize/models/users"; +import { verifyOtpTemplate } from "../email-templates/verifyotp"; export const fetchAllUsers = async (req: Request, res: Response) => { try { - // const users = await userService.getAllUsers(); - const users = await userService.getAllUsers(); if (users.length <= 0) { @@ -32,14 +36,15 @@ export const fetchAllUsers = async (req: Request, res: Response) => { export const userLogin = async (req: Request, res: Response) => { const { email, password } = req.body; - const user = await loggedInUser(email); - const accessToken = await generateToken(user); - if (!user) { + const user: IUser = await loggedInUser(email); + let accessToken; + if (!user || user === null) { res.status(404).json({ status: 404, message: "User Not Found ! Please Register new ancount", }); } else { + accessToken = await generateToken(user); const match = await comparePasswords(password, user.password); if (!match) { res.status(401).json({ @@ -52,30 +57,115 @@ export const userLogin = async (req: Request, res: Response) => { message: "Logged in", token: accessToken, }); + if (user.role.includes("seller")) { + const token = Math.floor(Math.random() * 90000 + 10000); + //@ts-ignore + await Token.create({ token: token, userId: user.id }); + await mailService.sendEmailService(user, SUBJECTS.CONFIRM_2FA, verifyOtpTemplate(token), token); + return res.status(200).json({ + status: STATUS.PENDING, + message: "OTP verification code has been sent ,please use it to verify that it was you", + }); + } else { + return res.status(200).json({ + status: 200, + message: "Logged in", + token: accessToken, + }); + } } } }; export const createUserController = async (req: Request, res: Response) => { try { - const { name, email, username, password } = req.body; - const user = await createUserService(name, email, username, password); - - if (!user) { + const { name, email, username, password, role } = req.body; + const user = await createUserService(name, email, username, password, role); + if (!user || user == null) { return res.status(409).json({ status: 409, message: "Username or email already exists", }); } - - res.status(201).json({ + return res.status(201).json({ status: 201, - message: "User successfully created.", + message: "User successfully created." }); } catch (err: any) { if (err.name === "UnauthorizedError" && err.message === "User already exists") { return res.status(409).json({ error: "User already exists" }); } - res.status(500).json({ error: err }); + return res.status(500).json({ error: err }); + } +}; + +export const updatePassword = async (req: Request, res: Response) => { + const { oldPassword, newPassword, confirmPassword } = req.body; + try { + // @ts-ignore + const user = await getUserByEmail(req.user.email); + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + const isPasswordValid = await comparePasswords(oldPassword, user.password); + if (!isPasswordValid) { + return res.status(400).json({ message: 'Old password is incorrect' }); + } + + if (newPassword !== confirmPassword) { + return res.status(400).json({ message: 'New password and confirm password do not match' }); + } + + if(await comparePasswords(newPassword, user.password)){ + return res.status(400).json({ message: 'New password is similar to the old one. Please use a new password'}) + } + + const password = await hashedPassword(newPassword); + await updateUserPassword(user, password) + return res.status(200).json({ message: 'Password updated successfully' }); + + } catch(err: any){ + return res.status(500).json({ + message: err.message + }) + } +}; + +export const tokenVerification = async (req: any, res: Response) => { + const foundToken: TokenAttributes = req.token; + + try { + const tokenCreationTime = new Date(String(foundToken?.createdAt)).getTime(); + const currentTime = new Date().getTime(); + const timeDifference = currentTime - tokenCreationTime; + + if (timeDifference > 600000) { + await Token.destroy({ where: { userId: foundToken.userId } }); + return res.status(401).json({ + message: "Token expired", + }); + } + + const user: IUser | null = await User.findOne({ where: { id: foundToken.userId } }); + + if (user) { + const token = await generateToken(user); + + await Token.destroy({ where: { userId: foundToken.userId } }); + + return res.status(200).json({ + message: "Login successful", + token, + user, + }); + } else { + return res.status(404).json({ + message: "User not found", + }); + } + } catch (error: any) { + return res.status(500).json({ + message: error.message, + }); } }; diff --git a/src/docs/swagger.ts b/src/docs/swagger.ts index f5fc119..38ea8da 100644 --- a/src/docs/swagger.ts +++ b/src/docs/swagger.ts @@ -1,7 +1,7 @@ import express from "express"; import { serve, setup } from "swagger-ui-express"; import { env } from "../utils/env"; -import { createUsers, getUsers, loginAsUser, userSchema, loginSchema } from "./users"; +import { createUsers, getUsers, loginAsUser, userSchema, loginSchema, updatePasswordSchema, passwordUpdate, verifyOTPToken } from "./users"; const docRouter = express.Router(); @@ -43,12 +43,19 @@ const options = { "/api/v1/users/login": { post: loginAsUser, }, + "/api/v1/users/passwordupdate": { + put: passwordUpdate, + }, + "/api/v1/users/2fa-verify": { + post: verifyOTPToken, + }, }, components: { schemas: { User: userSchema, Login: loginSchema, + updatePassword: updatePasswordSchema, }, securitySchemes: { bearerAuth: { diff --git a/src/docs/users.ts b/src/docs/users.ts index ae2c33c..7f29ec2 100644 --- a/src/docs/users.ts +++ b/src/docs/users.ts @@ -17,6 +17,21 @@ export const userSchema = { }, }; +export const updatePasswordSchema = { + type: "object", + properties: { + oldPassword: { + type: "string", + }, + newPassword: { + type: "string", + }, + confirmPassword: { + type: "string", + }, + }, +}; + export const loginSchema = { properties: { email: { @@ -96,5 +111,70 @@ export const loginAsUser = { 200: { description: "OK", }, + 400: { + description: "Bad request missing or extra filed", + }, + 404: { + description: "Account not found", + }, + 409: { + description: "Invalid credentials", + }, + 500: { + description: "Internal server error", + }, + }, +}; + +export const passwordUpdate = { + tags: ["Users"], + security: [{ bearerAuth: [] }], + summary: "Update Password", + requestBody: { + required: true, + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/updatePassword", + }, + }, + }, + }, + responses: { + 200: { + description: "OK", + }, + 400: { + description: "Bad Request", + }, + }, +}; +export const verifyOTPToken = { + tags: ["Users"], + summary: "verify OTP token for seller during login process", + requestBody: { + required: true, + content: { + "application/json": { + schema: { + properties: { + token: { + type: "number", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + description: "Successfuly logged in ", + }, + 403: { + description: "forbidden token expired", + }, + 404: { + description: "Inavalid token or not found", + }, }, }; diff --git a/src/email-templates/verifyotp.ts b/src/email-templates/verifyotp.ts new file mode 100644 index 0000000..f924e49 --- /dev/null +++ b/src/email-templates/verifyotp.ts @@ -0,0 +1,29 @@ +export const verifyOtpTemplate = (token: number) => { + return ` + + + + + Account Verification + + +
+

Verify that It's you

+ +

We noticed a login attempt to your Eagle E-commerce account. If this was you, please verify your new device using the following one-time verification code

+ +

+

+

${token}

+
+

This verification code is valid for 10 minutes.

+

If you don't recognize this login attempt, someone may be trying to access your account. We recommend you change your password immediately.

+
+

Your account is safe 😎.

+
+
+ + + +`; +}; diff --git a/src/middlewares/isLoggedIn.ts b/src/middlewares/isLoggedIn.ts new file mode 100644 index 0000000..068b844 --- /dev/null +++ b/src/middlewares/isLoggedIn.ts @@ -0,0 +1,40 @@ +import { getUserByEmail } from "../services/user.service"; +import { Request, Response, NextFunction } from "express"; +import { decodeToken } from "../utils/jsonwebtoken" + +export const isLoggedIn = async (req: Request, res: Response, next: NextFunction) => { + let token: string | undefined = undefined; + try{ + if ( + req.headers.authorization && + req.headers.authorization.startsWith("Bearer ") + ) { + token = req.headers.authorization.split(" ")[1]; + } + if (!token) { + return res.status(401).json({ + status: "Unauthorized", + message: "You are not logged in. Please login to continue.", + }); + } + if (typeof token !== "string") { + throw new Error("Token is not a string."); + } + const decoded: any = await decodeToken(token) + const loggedUser: any = await getUserByEmail(decoded.email); + if (!loggedUser) { + return res.status(401).json({ + status: "Unauthorized", + message: "Token has expired. Please login again.", + }); + } + // @ts-ignore + req.user = loggedUser; + next(); + } catch (error: any) { + return res.status(401).json({ + status: "failed", + error: error.message + " Token has expired. Please login again.", + }); + } +} \ No newline at end of file diff --git a/src/middlewares/isTokenFound.ts b/src/middlewares/isTokenFound.ts new file mode 100644 index 0000000..3d4e395 --- /dev/null +++ b/src/middlewares/isTokenFound.ts @@ -0,0 +1,22 @@ +import { NextFunction, Request, Response } from "express"; +import Token from "../sequelize/models/Token"; + +export const isTokenFound = async (req: any, res: Response, next: NextFunction) => { + const { token } = req.body; + try { + const foundToken = await Token.findOne({ where: { token: token } }); + + if (foundToken) { + req.token = foundToken; + next(); + } else { + return res.status(404).json({ + message: "Invalid token", + }); + } + } catch (error: any) { + return res.status(500).json({ + message: error.message, + }); + } +}; diff --git a/src/routes/homeRoutes.ts b/src/routes/homeRoutes.ts index b26f96b..fc3b720 100644 --- a/src/routes/homeRoutes.ts +++ b/src/routes/homeRoutes.ts @@ -1,4 +1,5 @@ import { Request, Response, Router } from "express"; +import Token from "../sequelize/models/Token"; const homeRoute = Router(); diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index 5b4e507..30291ba 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -1,14 +1,25 @@ import { Router } from "express"; -import { fetchAllUsers, createUserController, userLogin } from "../controllers/userControllers"; +import { fetchAllUsers, createUserController, userLogin, updatePassword, tokenVerification } from "../controllers/userControllers"; import { emailValidation, validateSchema } from "../middleware/validator"; import signUpSchema from "../schemas/signUpSchema"; +import { isLoggedIn } from "../middlewares/isLoggedIn"; +import { passwordUpdateSchema } from "../schemas/passwordUpdate"; +import { isTokenFound } from "../middlewares/isTokenFound"; const userRoutes = Router(); userRoutes.get("/", fetchAllUsers); +userRoutes.post('/login',userLogin); +userRoutes.post("/register", + emailValidation, + validateSchema(signUpSchema), + createUserController +) +userRoutes.put("/passwordupdate", isLoggedIn, validateSchema(passwordUpdateSchema), updatePassword) + userRoutes.post("/login", userLogin); userRoutes.post("/register", emailValidation, validateSchema(signUpSchema), createUserController); - -userRoutes.post("/register", createUserController); +userRoutes.put("/passwordupdate", isLoggedIn, validateSchema(passwordUpdateSchema), updatePassword); +userRoutes.post("/2fa-verify", isTokenFound, tokenVerification); export default userRoutes; diff --git a/src/schemas/passwordUpdate.ts b/src/schemas/passwordUpdate.ts new file mode 100644 index 0000000..168653e --- /dev/null +++ b/src/schemas/passwordUpdate.ts @@ -0,0 +1,10 @@ +import Joi from "joi"; + +export const passwordUpdateSchema = Joi.object({ + oldPassword: Joi.string() + .min(6).max(20).required(), + newPassword: Joi.string() + .min(6).max(20).required(), + confirmPassword: Joi.string() + .min(6).max(20).required() +}).options({ allowUnknown: false }) \ No newline at end of file diff --git a/src/schemas/signUpSchema.ts b/src/schemas/signUpSchema.ts index 7d331d6..d45b2eb 100644 --- a/src/schemas/signUpSchema.ts +++ b/src/schemas/signUpSchema.ts @@ -5,6 +5,7 @@ export const signUpSchema = Joi.object({ username: Joi.string().min(4).required(), email: Joi.string().min(6).required().email(), password: Joi.string().min(6).max(20).required(), + role: Joi.string().optional(), }).options({ allowUnknown: false }); export default signUpSchema; diff --git a/src/sequelize/migrations/20240422124749-add-role-field-to-user.js b/src/sequelize/migrations/20240422124749-add-role-field-to-user.js new file mode 100644 index 0000000..4bf2ffc --- /dev/null +++ b/src/sequelize/migrations/20240422124749-add-role-field-to-user.js @@ -0,0 +1,15 @@ +"use strict"; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.addColumn("users", "role", { + type: Sequelize.ARRAY(Sequelize.STRING), + defaultValue: ["buyer"], + }); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.removeColumn("users", "role"); + }, +}; diff --git a/src/sequelize/migrations/20240429064011-add-token-model.js b/src/sequelize/migrations/20240429064011-add-token-model.js new file mode 100644 index 0000000..3c42340 --- /dev/null +++ b/src/sequelize/migrations/20240429064011-add-token-model.js @@ -0,0 +1,29 @@ +"use strict"; + +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable("tokens", { + token: { + type: Sequelize.INTEGER, + allowNull: false, + primaryKey: true, + }, + userId: { + type: Sequelize.INTEGER, + allowNull: false, + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.dropTable("tokens"); + }, +}; diff --git a/src/sequelize/migrations/20240429093200-delete-isMechnat-filed.js b/src/sequelize/migrations/20240429093200-delete-isMechnat-filed.js new file mode 100644 index 0000000..2ed1300 --- /dev/null +++ b/src/sequelize/migrations/20240429093200-delete-isMechnat-filed.js @@ -0,0 +1,14 @@ +"use strict"; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.removeColumn("users", "twoFAEnabled"); + await queryInterface.removeColumn("users", "isMerchant"); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.addColumn("users", "twoFAEnabled"); + await queryInterface.addColumn("users", "isMerchant"); + }, +}; diff --git a/src/sequelize/models/Token.ts b/src/sequelize/models/Token.ts new file mode 100644 index 0000000..43fa4b8 --- /dev/null +++ b/src/sequelize/models/Token.ts @@ -0,0 +1,44 @@ +import { DataTypes, Model } from "sequelize"; +import sequelize from "../../config/dbConnection"; + +export interface TokenAttributes { + token: number; + userId: number; + createdAt?: Date; + updatedAt?: Date; +} + +class Token extends Model implements TokenAttributes { + public token!: number; + public userId!: number; + public createdAt!: Date; + public updatedAt!: Date; +} + +Token.init( + { + token: { + type: DataTypes.INTEGER, + primaryKey: true, + allowNull: false, + }, + userId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + }, + updatedAt: { + allowNull: false, + type: DataTypes.DATE, + }, + }, + { + sequelize, + modelName: "Token", + }, +); + +export default Token; diff --git a/src/sequelize/models/users.ts b/src/sequelize/models/users.ts index b928163..f7d23b8 100644 --- a/src/sequelize/models/users.ts +++ b/src/sequelize/models/users.ts @@ -6,6 +6,7 @@ export interface UserAttributes { name: string; username: string; email: string; + role?: string[]; password: string; createdAt?: Date; updatedAt?: Date; @@ -16,6 +17,7 @@ class User extends Model implements UserAttributes { name!: string; username!: string; email!: string; + role!: string[]; password!: string; createdAt!: Date | undefined; updatedAt1: Date | undefined; @@ -41,6 +43,10 @@ User.init( allowNull: false, type: DataTypes.STRING, }, + role: { + type: DataTypes.ARRAY(DataTypes.STRING), + defaultValue: ["buyer"], + }, password: { allowNull: false, type: DataTypes.STRING, diff --git a/src/services/mail.service.ts b/src/services/mail.service.ts new file mode 100644 index 0000000..f0c90f7 --- /dev/null +++ b/src/services/mail.service.ts @@ -0,0 +1,23 @@ +import Token from "../sequelize/models/Token"; +import { IUser, SUBJECTS } from "../types"; +import { env } from "../utils/env"; +import { generateMagicLinkToken } from "../utils/jsonwebtoken"; +import transporter from "../utils/transporter"; +import { verifyOtpTemplate } from "../email-templates/verifyotp"; + +export const sendEmailService = async (user: IUser, subject: string, template: any, token: number) => { + try { + const mailOptions = { + from: env.smtp_user, + to: user.email, + subject: subject, + html: template, + }; + + const info = await transporter.sendMail(mailOptions); + //@ts-ignore + console.log(info.response); + } catch (error: any) { + throw new Error(error.message); + } +}; diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 59c9fed..8401490 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -20,33 +20,37 @@ export const loggedInUser = async (email: string) => { const user: any = await User.findOne({ where: { email: email }, }); - if (!user) { - return false; - } else { - return user; - } + return user; } catch (err: any) { throw new Error(err.message); } }; - -export const createUserService = async (name: string, email: string, username: string, password: string): Promise => { - const existingUser = await User.findOne({ - where: { - [Op.or]: [{ email }, { username }], - }, - }); +export const createUserService = async (name: string, email: string, username: string, password: string, role: string): Promise => { + const existingUser = await User.findOne({ where: { email } }); if (existingUser) { return null; } const hashPassword = await hashedPassword(password); - const user = await User.create({ - name, - email, - username, - password: hashPassword, - }); - return user; + let user; + + if (role !== "" || role !== null) { + user = await User.create({ + name, + email, + username, + password: hashPassword, + role: [role], + }); + return user; + } else { + user = await User.create({ + name, + email, + username, + password: hashPassword, + }); + return user; + } }; export const getUserByEmail = async (email: string): Promise => { @@ -55,3 +59,8 @@ export const getUserByEmail = async (email: string): Promise => { }); return user; }; + +export const updateUserPassword = async (user: User, password: string) => { + const update = await User.update({ password: password}, { where: { id: user.id}}) + return update +}; diff --git a/src/types.ts b/src/types.ts index 3fdbbd9..6374c85 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,7 +4,7 @@ export interface IUser { username: string; email: string; password: string; - isMerchant?: boolean; - createdAt: Date; - updatedAt: Date; + role: string[]; + createdAt?: Date; + updatedAt?: Date; } diff --git a/src/helpers/comparePassword.ts b/src/utils/comparePassword.ts similarity index 100% rename from src/helpers/comparePassword.ts rename to src/utils/comparePassword.ts diff --git a/src/utils/jsonwebtoken.ts b/src/utils/jsonwebtoken.ts index 6bea145..48eda7d 100644 --- a/src/utils/jsonwebtoken.ts +++ b/src/utils/jsonwebtoken.ts @@ -1,6 +1,6 @@ import { IUser } from "../types"; import { env } from "../utils/env"; -import { sign } from "jsonwebtoken"; +import { sign, verify } from "jsonwebtoken"; export const generateToken = async (user: IUser) => { const accessToken = sign( @@ -13,3 +13,9 @@ export const generateToken = async (user: IUser) => { ); return accessToken; }; + +export const decodeToken = async (token: string) => { + const decoded = await verify(token, `${env.jwt_secret}`); + return decoded; +} +