diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a5ccde0..297fa0f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -32,6 +32,7 @@ jobs: - name: Running test env: + IS_REMOTE: true DB_CONNECTION: ${{ secrets.DB_CONNECTION }} TEST_DB: ${{ secrets.TEST_DB }} SMTP_HOST: ${{ secrets.SMTP_HOST }} diff --git a/__test__/user.test.ts b/__test__/user.test.ts index 79b112c..bd3cafc 100644 --- a/__test__/user.test.ts +++ b/__test__/user.test.ts @@ -7,6 +7,10 @@ import * as mailServices from "../src/services/mail.service"; import sequelize, { connect } from "../src/config/dbConnection"; // import * as twoFAService from "../src/utils/2fa"; import { profile } from "console"; +import bcrypt from "bcrypt"; +import { roleService } from "../src/services/role.service"; +import { Role } from "../src/sequelize/models/roles"; +import exp from "constants"; const userData: any = { name: "yvanna5", @@ -15,12 +19,12 @@ const userData: any = { password: "test12345", }; + const dummySeller = { name: "dummy1234", username: "username1234", email: "soleilcyber00@gmail.com", password: "1234567890", - role: "seller", }; const userTestData = { newPassword: "Test@123", @@ -55,6 +59,25 @@ describe("Testing user Routes", () => { try { await connect(); await sequelize.query('TRUNCATE TABLE profiles, users CASCADE'); + const testAdmin = { + name: "admin", + username: "admin", + email: "admin1@example.com", + password: await bcrypt.hash("password", 10), + roleId: 3 + } + + await Role.destroy({ where: {}}); + const resetId = await sequelize.query('ALTER SEQUENCE "Roles_id_seq" RESTART WITH 1'); + + await Role.bulkCreate([ + { name: "buyer" }, + { name: "seller" }, + { name: "admin" }, + ]) + + await User.create(testAdmin); + const dummy = await request(app).post("/api/v1/users/register").send(dummySeller); } catch (error) { console.error('Error connecting to the database:', error); @@ -63,14 +86,17 @@ describe("Testing user Routes", () => { let token:any; + let adminToken: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); + expect(response.status).toBe(201); }, 20000); + test("should return 409 when registering with an existing email", async () => { User.create(userData); const response = await request(app) @@ -166,6 +192,32 @@ describe("Testing user Routes", () => { spyonOne.mockRestore(); }, 20000); + test("should login an Admin", async () =>{ + const response = await request(app).post("/api/v1/users/login").send({ + email: "admin1@example.com", + password: "password" + }) + adminToken = response.body.token; +}); + + test("should update dummyseller's role to seller", async () => { + const logDummySeller = await request(app).post("/api/v1/users/login").send({ + email: dummySeller.email, + password: dummySeller.password, + }); + expect(logDummySeller.status).toBe(200); + const dummySellerId = logDummySeller.body.userInfo.id; + + const response = await request(app) + .patch(`/api/v1/users/${dummySellerId}/role`) + .send({ + roleId: 2, + }) + .set("Authorization", "Bearer " + adminToken); + expect(response.status).toBe(200); + + }); + test("Should send otp verification code", async () => { const spy = jest.spyOn(mailServices, "sendEmailService"); const response = await request(app).post("/api/v1/users/login").send({ @@ -264,8 +316,109 @@ describe("Testing user authentication", () => { const response = await request(app).get("/api/v1/users/auth/google/callback"); expect(response.status).toBe(302); }); + + }) + +describe("Admin should be able to CRUD roles", () => { + let testRoleName = "testrole"; + let newRoleId: any; + test("should return 201 when a new role is created", async () => { + const response = await request(app) + .post("/api/v1/roles") + .send({ + name: testRoleName, + }) + .set("Authorization", "Bearer " + adminToken); + expect(response.status).toBe(201); + newRoleId = response.body.role.id; + + }); + + test("should return 400 when a role with the same name is created", async () => { + const response = await request(app) + .post("/api/v1/roles") + .send({ + name: testRoleName, + }) + .set("Authorization", "Bearer " + adminToken); + expect(response.status).toBe(400); + }); + + test("should return 404 when deleting a role which doesn't exist", async () => { + const response = await request(app) + .delete("/api/v1/roles/1000") + .set("Authorization", "Bearer " + adminToken); + expect(response.status).toBe(404); + }) + + test("should return 200 when all roles are fetched", async () => { + const response = await request(app) + .get("/api/v1/roles") + expect(response.status).toBe(200); + }); + + test("should return 200 when a role is updated", async () => { + const response = await request(app) + .patch("/api/v1/roles/" + newRoleId) + .send({ + name: "testRoled", + }) + .set("Authorization", "Bearer " + adminToken); + expect(response.status).toBe(200); + }); + test("should return 400 role already exists when a role is updated with an existing name", async () => { + const response = await request(app) + .patch("/api/v1/roles/" + newRoleId) + .send({ + name: "buyer" + }) + .set("Authorization", "Bearer " + adminToken); + expect(response.status).toBe(400); + expect(response.body.message).toBe('Role already exists') + }); + + test("should return 401 Unauthorized when trying to create or update role without Auth", async () =>{ + const response = await request(app) + .patch("/api/v1/roles/" + newRoleId) + .send({ + name: "testRoled", + }) + expect(response.status).toBe(401); + }) + + test("should return 200 when a role is deleted", async () => { + const response = await request(app) + .delete("/api/v1/roles/" + newRoleId) + .set("Authorization", "Bearer " + adminToken); + expect(response.status).toBe(200); + }); +}); + +test("should return 409 when updating a role for user who doesn't exist", async () => { + + const response = await request(app) + .patch("/api/v1/users/1000/role") + .send({ + roleId: 2, + }) + .set("Authorization", "Bearer " + adminToken); + + + expect(response.status).toBe(409); +}); + +test("should return 409 when updating a role for a role that doesn't exist", async () => { + const response = await request(app) + .patch("/api/v1/users/1/role") + .send({ + roleId: 1000, + }) + .set("Authorization", "Bearer " + adminToken); + expect(response.status).toBe(409); +}); + afterAll(async () => { try { await sequelize.query('TRUNCATE TABLE profiles, users CASCADE'); diff --git a/package.json b/package.json index 06ba1a5..79de2b0 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "seed": "npx sequelize-cli db:seed:all", "undo:migrate" : "npx sequelize-cli db:migrate:undo:all", "lint:fix": "npx eslint --fix .", - "test": "cross-env NODE_ENV=test jest --detectOpenHandles --coverage", + "test": "cross-env NODE_ENV=test npm run migrate && jest --detectOpenHandles --coverage", "prepare": "husky", "prettier": "prettier . --write" }, @@ -100,3 +100,4 @@ "swagger-ui-express": "^5.0.0" } } + diff --git a/src/controllers/roleControllers.ts b/src/controllers/roleControllers.ts new file mode 100644 index 0000000..32cc70c --- /dev/null +++ b/src/controllers/roleControllers.ts @@ -0,0 +1,64 @@ +import { Request, Response } from 'express'; +import { roleService } from '../services/role.service'; +import { Optional } from 'sequelize'; +import { IRole } from '../sequelize/models/roles'; + +export const roleController = { + createRole: async (req: Request, res: Response) => { + const { name, permissions } = req.body; + + const existingRole = await roleService.findRoleByName(name); + if (existingRole) { + return res.status(400).json({ message: 'Role already exists' }); + } + + const role = await roleService.createRole({ name, permissions }); + res.status(201).json({ + message: 'Role created successfully', + role: role + }); + }, + + getRoles: async (req: Request, res: Response) => { + + const roles = await roleService.getRoles(); + if (roles.length === 0) { + return res.status(404).json({ message: 'No roles found' }); + } + res.status(200).json({ + message: 'Roles fetched successfully', + count: roles.length, + roles: roles + }); + }, + + updateRole: async (req: Request, res: Response) => { + const { id } = req.params; + const {name} = req.body + const existingRole = await roleService.findRoleByName(name); + if (existingRole) { + return res.status(400).json({ message: 'Role already exists' }); + } + const updatedRoleData: Optional = req.body; + const updatedRole = await roleService.updateRole(Number(id), updatedRoleData); + if (!updatedRole) { + return res.status(404).json({ message: `Role with id: ${id} not found` }); + } + res.status(200).json({ + message: 'Role updated successfully', + updatedRole: updatedRole + }); + }, + + deleteRole: async (req: Request, res: Response) => { + const { id } = req.params; + const deleted = await roleService.deleteRole(Number(id)); + if (!deleted) { + return res.status(404).json({ message: 'Role not found' }); + } + res.status(200).json({ + status: 200, + message: 'Role deleted successfully' + }); + } +}; \ No newline at end of file diff --git a/src/controllers/userControllers.ts b/src/controllers/userControllers.ts index 20d452d..db405ea 100644 --- a/src/controllers/userControllers.ts +++ b/src/controllers/userControllers.ts @@ -12,6 +12,7 @@ import { verifyOtpTemplate } from "../email-templates/verifyotp"; import { getProfileServices, updateProfileServices } from "../services/user.service"; import uploadFile from "../utils/handleUpload"; +import { updateUserRoleService } from "../services/user.service"; export const fetchAllUsers = async (req: Request, res: Response) => { try { const users = await userService.getAllUsers(); @@ -53,7 +54,8 @@ export const userLogin = async (req: Request, res: Response) => { message: " User email or password is incorrect!", }); } else { - if (user.role.includes("seller")) { + // @ts-ignore + if (user.userRole.name === "seller") { const token = Math.floor(Math.random() * 90000 + 10000); //@ts-ignore await Token.create({ token: token, userId: user.id }); @@ -63,9 +65,18 @@ export const userLogin = async (req: Request, res: Response) => { message: "OTP verification code has been sent ,please use it to verify that it was you", }); } else { + const userInfo = { + + id: user.id, + email: user.email, + roleId: user.userRole?.id, + roleName: user.userRole!.name + + } return res.status(200).json({ status: 200, message: "Logged in", + userInfo: userInfo, token: accessToken, }); } @@ -77,8 +88,8 @@ export const createUserController = async (req: Request, res: Response) => { const { name, email, username, password, role } = req.body; try { - const { name, email, username, password, role } = req.body; - const user = await createUserService(name, email, username, password, role); + const { name, email, username, password } = req.body; + const user = await createUserService(name, email, username, password); if (!user || user == null) { return res.status(409).json({ status: 409, @@ -272,3 +283,20 @@ export const updateProfileController = async (req: Request, res: Response) => { res.status(500).json({ error: 'Internal server error' }); } } + +export const updateUserRole = async (req: Request, res: Response) => { + const { roleId } = req.body; + const userId = parseInt(req.params.id); + + try { + + const userToUpdate = await updateUserRoleService(userId, roleId); + + res.status(200).json({ + message: 'User role updated successfully', + }); + } + catch (error: any) { + res.status(500).json({ message: 'Role or User Not Found' }); + } +}; diff --git a/src/docs/roledoc.ts b/src/docs/roledoc.ts new file mode 100644 index 0000000..bf8cc4d --- /dev/null +++ b/src/docs/roledoc.ts @@ -0,0 +1,133 @@ +export const RoleSchema = { + type: "object", + properties:{ + name: { + type: "string" + } + } +} + +export const getRoles = { + tags: ["Roles"], + summary: "Get all roles", + responses: { + 200: { + description: "OK", + + }, + }, + +} + +export const createRole = { + tags: ["Roles"], + security: [{ bearerAuth: [] }], + summary: "Create a new role", + requestBody: { + required: true, + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 201: { + description: "Created", + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/Role", + }, + }, + }, + }, + }, +} + +export const updateRole = { + tags: ["Roles"], + security: [{ bearerAuth: [] }], + summary: "Update a role", + parameters: [ + { + name: "id", + in: "path", + required: true, + description: "Role ID", + schema: { + type: "number", + }, + }, + ], + requestBody: { + required: true, + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + description: "OK", + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/Role", + }, + }, + }, + }, + 400:{ + description: "Bad request", + }, + 500:{ + description: "Internal Server Error", + + } + }, +} + +export const deleteRole = { + tags: ["Roles"], + security: [{ bearerAuth: [] }], + summary: "Delete a role", + parameters: [ + { + name: "id", + in: "path", + required: true, + description: "User ID", + schema: { + type: "number", + }, + }, + ], + responses: { + 200: { + description: "OK", + content:{ + "application/json":{ + schema:{ + $ref: "#/components/schemas/Role", + } + } + } + }, + }, +} \ No newline at end of file diff --git a/src/docs/swagger.ts b/src/docs/swagger.ts index 0839e35..da457cd 100644 --- a/src/docs/swagger.ts +++ b/src/docs/swagger.ts @@ -12,9 +12,16 @@ import { getProfileUser, profileSchema, updateProfile, - verifyOTPToken - + verifyOTPToken, + updateUserRole } from "./users"; + import { + RoleSchema, + getRoles, + createRole, + updateRole, + deleteRole + } from "./roledoc"; const docRouter = express.Router(); @@ -39,6 +46,10 @@ const options = { tags: [ { name: "Users", description: "Endpoints related to users" }, + { + name: "Roles", + description: "Endpoints related to roles" + } ], paths: { @@ -61,10 +72,22 @@ const options = { "/api/v1/users/2fa-verify": { post: verifyOTPToken, }, + "/api/v1/roles": { + get: getRoles, + post: createRole, + }, + "/api/v1/roles/{id}": { + patch: updateRole, + delete: deleteRole, + }, + "/api/v1/users/{id}/role":{ + patch: updateUserRole + } }, components: { schemas: { + Role: RoleSchema, User: userSchema, Login: loginSchema, updatePassword: updatePasswordSchema, diff --git a/src/docs/users.ts b/src/docs/users.ts index af2e87a..61acd02 100644 --- a/src/docs/users.ts +++ b/src/docs/users.ts @@ -275,3 +275,47 @@ export const verifyOTPToken = { }, }, }; + +export const updateUserRole ={ + tags: ["Users"], + security: [{ bearerAuth: [] }], + summary: "Update user role", + parameters: [ + { + name: "id", + in: "path", + required: true, + description: "User ID", + schema: { + type: "number", + }, + }, + ], + requestBody: { + required: true, + content: { + "application/json": { + schema: { + type: "object", + properties: { + roleId: { + type: "number", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + description: "OK", + }, + 400: { + description: "Bad request", + }, + 404: { + description: "Not found", + }, + }, +}; + diff --git a/src/middleware/isLoggedIn.ts b/src/middleware/isLoggedIn.ts deleted file mode 100644 index 4119bf6..0000000 --- a/src/middleware/isLoggedIn.ts +++ /dev/null @@ -1,40 +0,0 @@ -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/isAdmin.ts b/src/middlewares/isAdmin.ts new file mode 100644 index 0000000..65d63af --- /dev/null +++ b/src/middlewares/isAdmin.ts @@ -0,0 +1,12 @@ +import { Request, Response, NextFunction } from 'express'; +import {Role} from '../sequelize/models/roles'; + +export const isAdmin = async (req: Request, res: Response, next: NextFunction) => { + // @ts-ignore + const roleId = req.user.roleId; + const role = await Role.findByPk(roleId); + if (role?.name !== 'admin') { + return res.status(403).json({ message: 'Only admins can perform this action' }); + } + next(); + }; \ No newline at end of file diff --git a/src/middleware/isImage.ts b/src/middlewares/isImage.ts similarity index 100% rename from src/middleware/isImage.ts rename to src/middlewares/isImage.ts diff --git a/src/middleware/multer.ts b/src/middlewares/multer.ts similarity index 100% rename from src/middleware/multer.ts rename to src/middlewares/multer.ts diff --git a/src/middlewares/roleExist.ts b/src/middlewares/roleExist.ts new file mode 100644 index 0000000..6df7467 --- /dev/null +++ b/src/middlewares/roleExist.ts @@ -0,0 +1,16 @@ +import { Request, Response, NextFunction } from 'express'; +import { Role } from '../sequelize/models/roles'; +import { Op } from 'sequelize'; + +export const roleExist = async (req: Request, res: Response, next: NextFunction) => { + const isInExistence = await Role.findOne({ where: { + id: req.body.roleId + } + } + ); + + if (!isInExistence) { + return res.status(404).json({ message: 'Provided RoleID is not found' }); + } + next(); +}; \ No newline at end of file diff --git a/src/middlewares/userExist.ts b/src/middlewares/userExist.ts new file mode 100644 index 0000000..879135a --- /dev/null +++ b/src/middlewares/userExist.ts @@ -0,0 +1,16 @@ +import {Request, Response, NextFunction} from 'express'; +import User from '../sequelize/models/users'; +import {Op} from 'sequelize'; + + +export const userExist = async (req: Request, res: Response, next: NextFunction) => { + const isInExistence = await User.findOne({ + where: { + id: req.params.id + } + }); + if (!isInExistence) { + return res.status(409).json({ message: 'Provided UserId not Found' }); + } + next(); +} \ No newline at end of file diff --git a/src/middleware/validator.ts b/src/middlewares/validator.ts similarity index 100% rename from src/middleware/validator.ts rename to src/middlewares/validator.ts diff --git a/src/routes/roleRoutes.ts b/src/routes/roleRoutes.ts new file mode 100644 index 0000000..132324a --- /dev/null +++ b/src/routes/roleRoutes.ts @@ -0,0 +1,15 @@ +import express from 'express'; +import { roleController } from '../controllers/roleControllers'; +import { isLoggedIn } from '../middlewares/isLoggedIn'; +import{isAdmin} from '../middlewares/isAdmin'; +import { validateSchema } from '../middlewares/validator'; +import {roleSchema} from '../schemas/roleSchema'; + +const RoleRouter = express.Router(); + +RoleRouter.post('/', isLoggedIn, isAdmin, validateSchema(roleSchema), roleController.createRole); +RoleRouter.get('/', roleController.getRoles); +RoleRouter.patch('/:id', isLoggedIn, isAdmin, validateSchema(roleSchema),roleController.updateRole); +RoleRouter.delete('/:id', isLoggedIn, isAdmin, roleController.deleteRole); + +export default RoleRouter; \ No newline at end of file diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index ef0d03d..a994303 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -1,6 +1,6 @@ import { Router } from "express"; import { fetchAllUsers, createUserController, userLogin, updatePassword, tokenVerification, handleSuccess, handleFailure,updateProfileController, getProfileController } from "../controllers/userControllers"; -import { emailValidation, validateSchema } from "../middleware/validator"; +import { emailValidation, validateSchema } from "../middlewares/validator"; import { isLoggedIn } from "../middlewares/isLoggedIn"; import { passwordUpdateSchema } from "../schemas/passwordUpdate"; import { isTokenFound } from "../middlewares/isTokenFound"; @@ -8,9 +8,14 @@ import { authenticateUser, callbackFn } from "../services/user.service"; require("../auth/auth"); import logInSchema from "../schemas/loginSchema"; import { profileSchemas, signUpSchema } from "../schemas/signUpSchema"; -import upload from "../middleware/multer"; -import isUploadedFileImage from "../middleware/isImage"; +import upload from "../middlewares/multer"; +import isUploadedFileImage from "../middlewares/isImage"; import bodyParser from "body-parser"; +import { isAdmin } from "../middlewares/isAdmin"; +import { roleUpdateSchema } from "../schemas/userRoleUpdateSchema"; +import { updateUserRole } from "../controllers/userControllers"; +import { roleExist } from "../middlewares/roleExist"; +import { userExist } from "../middlewares/userExist"; @@ -34,6 +39,8 @@ userRoutes.patch('/profile', updateProfileController ) +userRoutes.patch("/:id/role",isLoggedIn, isAdmin, validateSchema(roleUpdateSchema), userExist, roleExist, updateUserRole) + userRoutes.get("/auth/google", authenticateUser); userRoutes.get("/auth/google/callback", callbackFn); diff --git a/src/schemas/roleSchema.ts b/src/schemas/roleSchema.ts new file mode 100644 index 0000000..b0ff5cd --- /dev/null +++ b/src/schemas/roleSchema.ts @@ -0,0 +1,6 @@ + +import Joi from 'joi' + +export const roleSchema = Joi.object({ + name: Joi.string().min(3).max(20).required() +}).options({ allowUnknown: false }) \ No newline at end of file diff --git a/src/schemas/userRoleUpdateSchema.ts b/src/schemas/userRoleUpdateSchema.ts new file mode 100644 index 0000000..3069389 --- /dev/null +++ b/src/schemas/userRoleUpdateSchema.ts @@ -0,0 +1,7 @@ +import Joi from "joi"; + + +export const roleUpdateSchema = Joi.object({ + roleId: Joi.number().required() + }).options({ allowUnknown: false +}) \ No newline at end of file diff --git a/src/sequelize/config/config.js b/src/sequelize/config/config.js index c28f20f..24dcd98 100644 --- a/src/sequelize/config/config.js +++ b/src/sequelize/config/config.js @@ -1,6 +1,7 @@ const dotenv = require("dotenv"); dotenv.config(); +console.log(process.env.IS_REMOTE); module.exports = { development: { url: process.env.DB_CONNECTION, @@ -9,6 +10,13 @@ module.exports = { test: { url: process.env.TEST_DB, dialect: "postgres", + + dialectOptions: process.env.IS_REMOTE === "true"? { + ssl: { + require: true, + rejectUnauthorized: false, + }, + }: null, }, production: { url: process.env.DB_CONNECTION, diff --git a/src/sequelize/migrations/20240418083442-UserTests.js b/src/sequelize/migrations/20240418083442-UserTests.js deleted file mode 100644 index a71e1c5..0000000 --- a/src/sequelize/migrations/20240418083442-UserTests.js +++ /dev/null @@ -1,45 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/** @type {import('sequelize-cli').Migration} */ -module.exports = { - async up(queryInterface, Sequelize) { - await queryInterface.createTable("usersTests", { - id: { - allowNull: false, - autoIncrement: true, - primaryKey: true, - type: Sequelize.INTEGER, - }, - name: { - type: Sequelize.STRING, - allowNull: false, - }, - username: { - type: Sequelize.STRING, - allowNull: false, - }, - email: { - type: Sequelize.STRING, - allowNull: false, - validate: { - isEmail: true, - }, - }, - password: { - type: Sequelize.STRING, - allowNull: false, - }, - createdAt: { - allowNull: false, - type: Sequelize.DATE, - }, - updatedAt: { - allowNull: false, - type: Sequelize.DATE, - }, - }); - }, - - async down(queryInterface, Sequelize) { - await queryInterface.describeTable("usersTests"); - }, -}; diff --git a/src/sequelize/migrations/20240422124749-add-role-field-to-user.js b/src/sequelize/migrations/20240422124749-add-role-field-to-user.js deleted file mode 100644 index 4bf2ffc..0000000 --- a/src/sequelize/migrations/20240422124749-add-role-field-to-user.js +++ /dev/null @@ -1,15 +0,0 @@ -"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/20240429093200-delete-isMechnat-filed.js b/src/sequelize/migrations/20240429093200-delete-isMechnat-filed.js deleted file mode 100644 index 2ed1300..0000000 --- a/src/sequelize/migrations/20240429093200-delete-isMechnat-filed.js +++ /dev/null @@ -1,14 +0,0 @@ -"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/migrations/a20240502074100-roles.js b/src/sequelize/migrations/a20240502074100-roles.js new file mode 100644 index 0000000..424fbe8 --- /dev/null +++ b/src/sequelize/migrations/a20240502074100-roles.js @@ -0,0 +1,32 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up (queryInterface, Sequelize) { + await queryInterface.createTable('Roles', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + name: { + unique: true, + type: Sequelize.STRING, + allowNull: false, + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + + async down (queryInterface, Sequelize) { + await queryInterface.dropTable('Roles'); + } +}; diff --git a/src/sequelize/migrations/20240416115110-create-user.js b/src/sequelize/migrations/b20240502074237-user.js similarity index 91% rename from src/sequelize/migrations/20240416115110-create-user.js rename to src/sequelize/migrations/b20240502074237-user.js index 4276770..4656fdb 100644 --- a/src/sequelize/migrations/20240416115110-create-user.js +++ b/src/sequelize/migrations/b20240502074237-user.js @@ -37,14 +37,17 @@ module.exports = { type: Sequelize.DATE, }, }); - await queryInterface.addColumn('profiles', 'userId', { + + await queryInterface.addColumn('users', 'roleId', { type: Sequelize.INTEGER, + defaultValue: 1, references: { - model: 'users', + model: 'Roles', key: 'id' }, onUpdate: 'CASCADE', onDelete: 'CASCADE' + }); }, diff --git a/src/sequelize/migrations/20240423145916-userProfile.js b/src/sequelize/migrations/c20240423145916-userProfile.js similarity index 92% rename from src/sequelize/migrations/20240423145916-userProfile.js rename to src/sequelize/migrations/c20240423145916-userProfile.js index a757e27..c83e700 100644 --- a/src/sequelize/migrations/20240423145916-userProfile.js +++ b/src/sequelize/migrations/c20240423145916-userProfile.js @@ -10,6 +10,12 @@ module.exports = { }, userId: { type: Sequelize.INTEGER, + references:{ + model: 'users', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', allowNull: true, }, profileImage: { diff --git a/src/sequelize/models/Token.ts b/src/sequelize/models/Token.ts index 43fa4b8..bdc5cf4 100644 --- a/src/sequelize/models/Token.ts +++ b/src/sequelize/models/Token.ts @@ -37,7 +37,7 @@ Token.init( }, { sequelize, - modelName: "Token", + tableName: "tokens", }, ); diff --git a/src/sequelize/models/roles.ts b/src/sequelize/models/roles.ts new file mode 100644 index 0000000..3effce8 --- /dev/null +++ b/src/sequelize/models/roles.ts @@ -0,0 +1,38 @@ +import { DataTypes, Model } from 'sequelize'; +import sequelize from "../../config/dbConnection"; +import dotenv from 'dotenv'; +dotenv.config(); + +export interface IRole { + id?: number; + name: string; + permissions?: string; +} + + + +class Role extends Model { + public id!: number; + public name!: string; + public permissions?: string; +} + +Role.init({ + id: { + type: DataTypes.NUMBER, + autoIncrement: true, + allowNull: false, + primaryKey: true, + }, + name: { + unique: true, + type: new DataTypes.STRING(128), + allowNull: false, + }, + +}, { + tableName: 'Roles', + sequelize: sequelize, +}); + +export { Role }; \ No newline at end of file diff --git a/src/sequelize/models/users.ts b/src/sequelize/models/users.ts index d0902f4..302762b 100644 --- a/src/sequelize/models/users.ts +++ b/src/sequelize/models/users.ts @@ -1,14 +1,15 @@ import { Model, DataTypes } from 'sequelize'; import sequelize from '../../config/dbConnection'; import Profile from './profiles'; +import {Role} from './roles'; export interface UserAttributes { id?: number; name: string; username: string; email: string; - role?: string[]; password: string | undefined; + roleId: number | undefined; createdAt?: Date; updatedAt?: Date; } @@ -18,8 +19,8 @@ class User extends Model implements UserAttributes { name!: string; username!: string; email!: string; - role!: string[]; password!: string; + roleId!: number | undefined; createdAt!: Date | undefined; updatedAt!: Date | undefined; } @@ -44,14 +45,19 @@ User.init( allowNull: false, type: DataTypes.STRING, }, - role: { - type: DataTypes.ARRAY(DataTypes.STRING), - defaultValue: ["buyer"], - }, password: { allowNull: true, type: DataTypes.STRING, }, + roleId: { + type: DataTypes.NUMBER, + allowNull: false, + defaultValue: 1, + references:{ + model: 'Roles', + key: 'id' + } + }, createdAt: { allowNull: false, type: DataTypes.DATE, @@ -65,6 +71,17 @@ User.init( sequelize, modelName: 'users', }); + // associations with users table + + User.belongsTo(Role, { + foreignKey: 'roleId', + as: 'userRole' + }) + Role.hasMany(User, { + foreignKey: 'roleId', + as: 'users' + }) + User.hasOne(Profile, { foreignKey: 'userId', diff --git a/src/sequelize/seeders/20240412144111-demo-user.js b/src/sequelize/seeders/20240412144111-demo-user.js deleted file mode 100644 index 4043e7f..0000000 --- a/src/sequelize/seeders/20240412144111-demo-user.js +++ /dev/null @@ -1,27 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/** @type {import('sequelize-cli').Migration} */ -module.exports = { - up: (queryInterface, Sequelize) => { - return queryInterface.bulkInsert("Users", [ - { - name: "Rukundo Soleil", - username: "soleil00", - email: "soleil@soleil00.com", - password: "soleil00", - createdAt: new Date(), - updatedAt: new Date(), - }, - { - name: "test user", - username: "yes", - email: "soleil@soleil0w.com", - password: "soleil0w0", - createdAt: new Date(), - updatedAt: new Date(), - }, - ]); - }, - down: (queryInterface, Sequelize) => { - return queryInterface.bulkDelete("Users", null, {}); - }, -}; diff --git a/src/sequelize/seeders/b20240412144111-demo-user.js b/src/sequelize/seeders/b20240412144111-demo-user.js new file mode 100644 index 0000000..57cc297 --- /dev/null +++ b/src/sequelize/seeders/b20240412144111-demo-user.js @@ -0,0 +1,54 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + up: async (queryInterface, Sequelize) => { + const bcrypt = require("bcrypt"); + + + + return Promise.all([queryInterface.bulkInsert("Roles",[ + { + name: "buyer", + createdAt: new Date(), + updatedAt: new Date(), + }, + { + name: "seller", + createdAt: new Date(), + updatedAt: new Date(), + }, + { + name: "admin", + createdAt: new Date(), + updatedAt: new Date(), + }, + ]), + + queryInterface.bulkInsert("users", [ + { + name: "Rukundo Soleil", + username: "soleil00", + email: "soleil@soleil00.com", + password: await bcrypt.hash("soleil00", 10), + roleId: 3, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + name: "test user", + username: "yes", + email: "soleil@soleil0w.com", + password: await bcrypt.hash("soleil00", 10), + roleId: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, + ]) + ]); + + }, + down: async (queryInterface, Sequelize) => { + + return queryInterface.bulkDelete("users", null, {}); + }, +}; diff --git a/src/services/mail.service.ts b/src/services/mail.service.ts index f0c90f7..07db427 100644 --- a/src/services/mail.service.ts +++ b/src/services/mail.service.ts @@ -16,7 +16,6 @@ export const sendEmailService = async (user: IUser, subject: string, template: a 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/role.service.ts b/src/services/role.service.ts new file mode 100644 index 0000000..64999b3 --- /dev/null +++ b/src/services/role.service.ts @@ -0,0 +1,34 @@ +import { Role, IRole } from '../sequelize/models/roles'; +import { Optional } from 'sequelize'; + +class RoleService { + async createRole(role: Optional): Promise { + const newRole = await Role.create(role); + return newRole; + } + + async getRoles(): Promise { + const roles = await Role.findAll(); + return roles; + } + + async findRoleByName(name: string): Promise { + const role = await Role.findOne({ where: { name } }); + return role; + } + + async updateRole(id: number, role: Partial): Promise { + const [numberOfAffectedRows, updatedRoles] = await Role.update(role, { where: { id }, returning: true }); + if (numberOfAffectedRows === 0) { + return null; + } + return updatedRoles[0]; + } + + async deleteRole(id: number): Promise { + const numberOfDestroyedRows = await Role.destroy({ where: { id } }); + return numberOfDestroyedRows > 0; + } +} + +export const roleService = new RoleService(); \ No newline at end of file diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 28bdc42..c6aebf2 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -3,6 +3,8 @@ import { hashedPassword } from "../utils/hashPassword"; import passport from "passport"; import { Op } from "sequelize"; import Profile, { ProfileAttributes } from "../sequelize/models/profiles"; +import { Role } from "../sequelize/models/roles"; +import { error } from "console"; export const authenticateUser = passport.authenticate("google", { @@ -16,7 +18,9 @@ export const callbackFn = passport.authenticate("google", { export const getAllUsers = async () => { try { - const users = await User.findAll(); + const users = await User.findAll({ + attributes: ['id', 'name', 'username', 'email', 'roleId', 'createdAt', 'updatedAt'], + }); if (users.length === 0) { console.log("no user"); } @@ -31,13 +35,14 @@ export const loggedInUser = async (email: string) => { try { const user: any = await User.findOne({ where: { email: email }, + include: [{model: Role, as: "userRole"}] }); return user; } catch (err: any) { throw new Error(err.message); } }; -export const createUserService = async (name: string, email: string, username: string, password: string, role: string): Promise => { +export const createUserService = async (name: string, email: string, username: string, password: string): Promise => { const existingUser = await User.findOne({ where: { email } }); if (existingUser) { return null; @@ -45,32 +50,7 @@ export const createUserService = async (name: string, email: string, username: s const hashPassword = await hashedPassword(password); let user; - if (role !== "" || role !== null) { - user = await User.create({ - name, - email, - username, - password: hashPassword, - role: [role], - }); - await Profile.create({ - // @ts-ignore - userId: user.id, - profileImage:"", - fullName: "", - email: "", - gender: "", - birthdate: "", - preferredLanguage: "", - preferredCurrency: "", - street: "", - city: "", - state: "", - postalCode:"", - country: "", - }); - return user; - } else { + user = await User.create({ name, email, @@ -96,7 +76,7 @@ export const createUserService = async (name: string, email: string, username: s return user; } -}; + export const getUserByEmail = async (email: string): Promise => { const user = await User.findOne({ @@ -148,4 +128,33 @@ export const updateProfileServices = async ( } catch (error) { throw new Error("Error in update profile"); } +}; + +export const updateUserRoleService = async (userId: number, newRoleId: number): Promise => { + try{ + // check if the role exists + const role = await Role.findOne({where: {id: newRoleId}}) + if (!role){ + throw new Error(`Role with id: ${newRoleId} not found`); } + // update the role of the user + const [numberOfAffectedRows, updatedUser] = await User.update( + {roleId: newRoleId}, + {where:{ + id: userId } + ,returning: true, + }); + + if (numberOfAffectedRows === 0){ + throw new Error(`User with id: ${userId} not found`) + } + + + return updatedUser[0]; + } + catch(error: any){ + throw new Error(`Error in service`); + } + +}; + diff --git a/src/types.ts b/src/types.ts index 8d90b75..3922733 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,10 +1,12 @@ +import { IRole } from "./sequelize/models/roles"; export interface IUser { id?: number; name: string; username: string; email: string; password: string; - role: string[]; + roleId?: number; + userRole?:IRole; createdAt?: Date; updatedAt?: Date; } diff --git a/src/utils/server.ts b/src/utils/server.ts index 1a07004..da62756 100644 --- a/src/utils/server.ts +++ b/src/utils/server.ts @@ -5,6 +5,7 @@ import homeRoute from "../routes/homeRoutes"; import docRouter from "../docs/swagger"; import passport from "passport"; import session from "express-session"; +import RoleRouter from "../routes/roleRoutes"; const app = express(); @@ -30,5 +31,6 @@ app.use( app.use("/", homeRoute); app.use("/api/v1", appROutes); app.use("/docs", docRouter); +app.use("/api/v1/roles", RoleRouter); export default app;