diff --git a/.env.example b/.env.example index 19bce1c0..15341dfe 100644 --- a/.env.example +++ b/.env.example @@ -42,3 +42,17 @@ CLOUDINARY_SECRET="" #USER ADMIN CREDENTIALS ADMIN_PASSWORD="" ADMIN_PHONE="" + +# DOCKER CONFIGURATION +DEV_DB_HOST="db" +# admin seeder +firstName = "" +lastName = "" +email = "" +password = "" +phone = "" +verified = "", +status = "" +ADMIN_ROLE="" +gender="" + diff --git a/package.json b/package.json index 10b04590..5b24540d 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,9 @@ "prepare": "husky install && npx husky add .husky/pre-commit \"npx lint-staged\"", "migrate": "npx sequelize-cli db:migrate", "migrate:undo": "npx sequelize-cli db:migrate:undo", + "migrate:undo:all": "npx sequelize-cli db:migrate:undo:all", "seed": "npx sequelize-cli db:seed:all", - "seed:undo": "npx sequelize-cli db:seed:undo" + "seed:undo": "npx sequelize-cli db:seed:undo:all" }, "keywords": [], "author": "", @@ -44,7 +45,7 @@ "pg": "^8.11.5", "pg-hstore": "^2.3.4", "randomstring": "^1.3.0", - "sequelize": "^6.37.2", + "sequelize": "^6.37.3", "swagger-ui-express": "^5.0.0", "uuid": "^9.0.1", "winston": "^3.13.0", diff --git a/src/controllers/permissionController.ts b/src/controllers/permissionController.ts new file mode 100644 index 00000000..8e735ec3 --- /dev/null +++ b/src/controllers/permissionController.ts @@ -0,0 +1,83 @@ +import { Request, Response } from 'express'; +import { sendInternalErrorResponse } from '../validations'; +import Permission from '../database/models/permission'; +import { validateFields } from '../validations/index'; + +// create a permission +export const createPermission = async (req: Request, res: Response): Promise => { + try { + const missingFields = validateFields(req, ['name']); + if (missingFields.length) { + res.status(400).json({ ok: false, errorMessage: `Missing required fields: ${missingFields.join(', ')}` }); + return; + } + const createdPermission = await Permission.create({ name: req.body.name }); + res.status(201).json({ ok: true, data: createdPermission }); + } catch (error) { + sendInternalErrorResponse(res, error); + return; + } +}; +// get all permissions +export const getAllPermissions = async (req: Request, res: Response): Promise => { + try { + const permissions = await Permission.findAll({ attributes: ['id', 'name'] }); + res.status(200).json({ ok: true, data: permissions }); + } catch (error) { + sendInternalErrorResponse(res, error); + return; + } +}; +// get a single permission +export const getSinglePermission = async (req: Request, res: Response): Promise => { + try { + const permission = await Permission.findByPk(req.params.id); + if (!permission) { + res.status(404).json({ ok: false, errorMessage: 'Permission not found' }); + return; + } + res.status(200).json({ ok: true, data: permission }); + } catch (error) { + sendInternalErrorResponse(res, error); + return; + } +}; +// update a permission +export const updatePermission = async (req: Request, res: Response): Promise => { + try { + const missingFields = validateFields(req, ['name']); + if (missingFields.length) { + res.status(400).json({ ok: false, errorMessage: `Missing required fields: ${missingFields.join(', ')}` }); + return; + } + const permissionToUpdate = await Permission.findByPk(req.params.id); + if (!permissionToUpdate) { + res.status(404).json({ ok: false, errorMessage: 'Permission not found' }); + return; + } + if (!req.body.name) res.status(400).json({ ok: false, errorMessage: 'Name is required' }); + + permissionToUpdate.name = req.body.name; + + await permissionToUpdate.save(); + res.status(200).json({ ok: true, data: permissionToUpdate }); + } catch (error) { + sendInternalErrorResponse(res, error); + return; + } +}; +// delete a permission +export const deletePermission = async (req: Request, res: Response): Promise => { + try { + const permissionToDelete = await Permission.findByPk(req.params.id); + if (!permissionToDelete) { + res.status(404).json({ ok: false, errorMessage: 'Permission not found' }); + return; + } + await permissionToDelete.destroy(); + res.status(200).json({ ok: true, message: 'permission deleted successfully!' }); + } catch (error) { + sendInternalErrorResponse(res, error); + return; + } +}; diff --git a/src/controllers/roleControllers.ts b/src/controllers/roleControllers.ts index dd7840fb..aa19e33d 100644 --- a/src/controllers/roleControllers.ts +++ b/src/controllers/roleControllers.ts @@ -1,26 +1,45 @@ import logger from '../logs/config'; import { Request, Response } from 'express'; import Role from '../database/models/role'; +import Permission from '../database/models/permission'; import { sendInternalErrorResponse, validateFields } from '../validations'; +import sequelize from '../database/models/index'; const createRole = async (req: Request, res: Response): Promise => { try { - if (validateFields(req, ['name']).length !== 0) { - res.status(400).json({ ok: false, errorMessage: 'Role name is required' }); + const { name, permissionIds } = req.body; + const missingFields = validateFields(req, ['name', 'permissionIds']); + if (missingFields.length > 0) { + res.status(400).json({ ok: false, message: `Required fields: ${missingFields.join(', ')}` }); return; } - const { name, displayName } = req.body; - const createdRole = await Role.create({ name, displayName }); - res.status(201).json({ ok: true, data: createdRole }); + + const permissions = await Permission.findAll({ where: { id: permissionIds } }); + if (permissions.length !== permissionIds.length) { + logger.error('Adding role: One or more permissions not found'); + res.status(404).json({ ok: false, message: 'Roles create permissions not found' }); + return; + } + + const transaction = await sequelize.transaction(); + try { + const role = await Role.create({ name }, { transaction }); + await (role as any).addPermissions(permissions, { transaction }); + await transaction.commit(); + res.status(201).json({ ok: true, message: 'Role created successfully' }); + } catch (err) { + logger.error('Error creating role'); + await transaction.rollback(); + throw err; + } } catch (error) { logger.error(error); sendInternalErrorResponse(res, error); - return; } }; const getAllRoles = async (req: Request, res: Response): Promise => { try { - const roles = await Role.findAll(); + const roles = await Role.findAll({ include: { model: Permission, attributes: ['name'] } }); res.status(200).json({ ok: true, data: roles }); } catch (error) { logger.error(error); @@ -29,11 +48,10 @@ const getAllRoles = async (req: Request, res: Response): Promise => { } }; const getSingleRole = async (req: Request, res: Response): Promise => { - const { id } = req.params; try { - const role = await Role.findByPk(id); + const role = await Role.findByPk(req.params.id, { include: { model: Permission, attributes: ['name'] } }); if (!role) { - res.status(404).json({ ok: false, errorMessage: 'Roles can not be found' }); + res.status(404).json({ ok: false, message: 'Roles can not be found' }); return; } res.status(200).json({ ok: true, data: role }); @@ -44,22 +62,29 @@ const getSingleRole = async (req: Request, res: Response): Promise => { } }; const updateRole = async (req: Request, res: Response): Promise => { - const { id } = req.params; - const contentsToUpdate = { ...req.body }; try { - const roleToupdate = await Role.findByPk(id); - if (!roleToupdate) { - res.status(404).json({ ok: false, errorMessage: 'Role not found' }); - return; - } - if (contentsToUpdate.name) { - roleToupdate.name = contentsToUpdate.name; - } - if (contentsToUpdate.displayName) { - roleToupdate.displayName = contentsToUpdate.displayName; + const transaction = await sequelize.transaction(); + try { + const role = await Role.findByPk(req.params.id, { transaction }); + if (!role) { + logger.error(`Role not found`); + res.status(404).json({ ok: false, errorMessage: 'Role not found' }); + return; + } + const missingFields = validateFields(req, ['name', 'permissionIds']); + if (missingFields.length > 0) { + res.status(400).json({ ok: false, errorMessage: `the required fields: ${missingFields.join(', ')}` }); + } + role.name = req.body.name; + const updatedRole = await role.save({ transaction }); + await (updatedRole as any).setPermissions(req.body.permissionIds, { transaction }); + await transaction.commit(); + res.status(200).json({ ok: true, message: 'role updated successfully' }); + } catch (err) { + logger.error('error updating role'); + await transaction.rollback(); + throw err; } - await roleToupdate.save(); - res.status(200).json({ ok: true, data: roleToupdate }); } catch (error) { logger.error(error); sendInternalErrorResponse(res, error); @@ -67,13 +92,12 @@ const updateRole = async (req: Request, res: Response): Promise => { } }; const deleteRole = async (req: Request, res: Response): Promise => { - const { id } = req.params; try { - const deletedCount = await Role.destroy({ where: { id } }); + const deletedCount = await Role.destroy({ where: { id: req.params.id } }); if (deletedCount === 1) { - res.status(200).json({ ok: true, data: deletedCount }); + res.status(200).json({ ok: true, message: 'Role deleted successfully' }); } else { - res.status(404).json({ ok: false, errorMessage: 'Role not found' }); + res.status(404).json({ ok: false, message: 'Role not found' }); } } catch (error) { logger.error(error); diff --git a/src/controllers/sellerRequestController.ts b/src/controllers/sellerRequestController.ts new file mode 100644 index 00000000..4eafb853 --- /dev/null +++ b/src/controllers/sellerRequestController.ts @@ -0,0 +1,122 @@ +import { Request, Response } from 'express'; +import { sendInternalErrorResponse } from '../validations'; +import logger from '../logs/config'; +import VendorRequest from '../database/models/sellerRequest'; +import uploadImage from '../helpers/claudinary'; +import User from '../database/models/user'; +const fileUploadService = async (req: Request) => { + const store: string[] = []; + for (const file of req.files as Express.Multer.File[]) { + store.push(await uploadImage(file.buffer)); + } + return store; +}; +export const createSellerRequest = async (req: Request, res: Response): Promise => { + try { + const vendorId = (req.user as User).id; + const exisistingUser = await VendorRequest.findOne({ where: { vendorId } }); + if (exisistingUser) { + logger.error('user request exists!'); + res.status(400).json({ ok: false, message: 'This user has sent the request already!' }); + return; + } + if (!req.files || +req.files.length < 6) { + res.status(400).json({ message: 'Please upload all required documents(6)' }); + return; + } + if (req.body.agreement !== 'true') { + res.status(400).json({ message: 'Please agree to the terms and conditions' }); + return; + } + const documents = await fileUploadService(req); + + logger.info('Documents uploaded successfully!'); + const { agreement } = req.body; + + const result = await VendorRequest.create({ + vendorId, + agreement, + documents, + }); + res.status(201).json({ ok: true, data: result }); + } catch (error) { + logger.error(error); + sendInternalErrorResponse(res, error); + } +}; +export const getSellerRequest = async (req: Request, res: Response): Promise => { + try { + const request = await VendorRequest.findByPk(req.params.id); + if (!request) { + res.status(404).json({ ok: false, message: `No request found by id: ${req.params.id}` }); + } + res.status(200).json({ ok: true, data: request }); + } catch (error) { + logger.error(error); + sendInternalErrorResponse(res, error); + } +}; +export const updateSellerRequest = async (req: Request, res: Response): Promise => { + try { + if (req.body.agreement !== 'true') { + res.status(400).json({ + ok: false, + errorMessage: `Please agree to our terms and conditions first`, + }); + return; + } + const request = await VendorRequest.findByPk(req.params.id); + if (request === null) { + res.status(404).json({ + ok: false, + message: 'request not found', + }); + } + if (!req.files || +req.files.length !== 6) { + res.status(400).json({ ok: false, message: '6 files are required' }); + return; + } + const docs = await fileUploadService(req); + request!.documents = docs; + req.body.agreement === 'true' + ? (request!.agreement = req.body.agreement) + : (request!.agreement = request!.agreement); + await request?.save(); + + res.status(200).json({ ok: true, message: 'request updated successfully', data: request }); + } catch (error) { + logger.error(error); + sendInternalErrorResponse(res, error); + } +}; +export const deleteSellerRequest = async (req: Request, res: Response): Promise => { + try { + const request = await VendorRequest.findByPk(req.params.id); + if (request === null) { + res.status(404).json({ + ok: false, + message: 'request not found', + }); + } + await request?.destroy(); + + res.status(200).json({ ok: true, message: 'Vendor request deleted successfully' }); + } catch (error) { + logger.error(error); + sendInternalErrorResponse(res, error); + } +}; +export const getAllSellerRequests = async (req: Request, res: Response): Promise => { + try { + const requests = await VendorRequest.findAll({ + include: { + model: User, + }, + }); + + res.status(200).json({ ok: true, data: requests }); + } catch (error) { + logger.error(error); + sendInternalErrorResponse(res, error); + } +}; diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index 8d2cbfb4..9ae9af53 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -32,7 +32,7 @@ export const signupUser = async (req: Request, res: Response) => { if (!validatePassword(password)) { return res.status(400).json({ ok: false, - error: 'Password contains at least 1 letter, 1 number, and 1 special character, minumun 8 characters', + error: 'Password should contains at least 1 letter, 1 number, and 1 special character, minumun 8 characters', }); } diff --git a/src/database/migrations/20240401104514-create-permissions-table.js b/src/database/migrations/20240401104514-create-permissions-table.js new file mode 100644 index 00000000..c419bcc3 --- /dev/null +++ b/src/database/migrations/20240401104514-create-permissions-table.js @@ -0,0 +1,32 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('Permissions', { + id: { + type: Sequelize.UUID, + defaultValue: Sequelize.UUIDV4, + primaryKey: true, + }, + name: { + type: Sequelize.STRING, + allowNull: false, + defaultValue: 'read', + unique: true, + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.dropTable('Permissions'); + }, +}; diff --git a/src/database/migrations/20240413203456-create-role.js b/src/database/migrations/20240413203456-create-role.js index 1daebee7..a0421195 100644 --- a/src/database/migrations/20240413203456-create-role.js +++ b/src/database/migrations/20240413203456-create-role.js @@ -4,21 +4,19 @@ module.exports = { async up(queryInterface, Sequelize) { await queryInterface.createTable('Roles', { id: { - allowNull: false, - primaryKey: true, type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, - unique: true, + primaryKey: true, }, name: { - type: Sequelize.STRING, + type: Sequelize.ENUM('admin', 'buyer', 'seller'), allowNull: false, unique: true, + set(value) { + this.setDataValue('name', value.toLowerCase()); + }, }, - displayName: { - type: Sequelize.STRING, - allowNull: true, - }, + createdAt: { allowNull: false, type: Sequelize.DATE, diff --git a/src/database/migrations/20240505075748-role_permissions.js b/src/database/migrations/20240505075748-role_permissions.js new file mode 100644 index 00000000..b84d9006 --- /dev/null +++ b/src/database/migrations/20240505075748-role_permissions.js @@ -0,0 +1,40 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('role_permissions', { + roleId: { + type: Sequelize.UUID, + allowNull: true, + references: { + model: 'Roles', + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + permissionId: { + type: Sequelize.UUID, + allowNull: true, + references: { + model: 'Permissions', + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + async down(queryInterface, Sequelize) { + await queryInterface.dropTable('role_permissions'); + }, +}; diff --git a/src/database/migrations/20240506142003-create_vendor_request_table.js b/src/database/migrations/20240506142003-create_vendor_request_table.js new file mode 100644 index 00000000..56ea0340 --- /dev/null +++ b/src/database/migrations/20240506142003-create_vendor_request_table.js @@ -0,0 +1,49 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('VendorRequest', { + id: { + type: Sequelize.UUID, + primaryKey: true, + defaultValue: Sequelize.UUIDV4, + }, + vendorId: { + type: Sequelize.UUID, + references: { + model: 'Users', + key: 'id', + }, + onDelete: 'CASCADE', + onUpdate: 'RESTRICT', + }, + documents: { + type: Sequelize.ARRAY(Sequelize.STRING), + allowNull: false, + }, + status: { + type: Sequelize.ENUM('pending', 'approved', 'rejected'), + defaultValue: 'pending', + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + }, + agreement: { + type: Sequelize.BOOLEAN, + defaultValue: false, + }, + }); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.dropTable('VendorRequest'); + }, +}; diff --git a/src/database/models/index.ts b/src/database/models/index.ts index 3181deea..6a510047 100644 --- a/src/database/models/index.ts +++ b/src/database/models/index.ts @@ -1,7 +1,6 @@ 'use strict'; import { Sequelize, Dialect } from 'sequelize'; - const env: string = process.env.NODE_ENV || 'development'; const config = require('../config/config.js'); @@ -11,7 +10,7 @@ const dbConfig = config[env]; const sequelize = new Sequelize(dbConfig.database!, dbConfig.username!, dbConfig.password!, { dialect: dbConfig.dialect! as Dialect, host: dbConfig.host || 'localhost', - port: dbConfig.port, + port: dbConfig.port || 5432, }); export default sequelize; diff --git a/src/database/models/order.ts b/src/database/models/order.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/database/models/orderItem.ts b/src/database/models/orderItem.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/database/models/permission.ts b/src/database/models/permission.ts new file mode 100644 index 00000000..1cec2b1c --- /dev/null +++ b/src/database/models/permission.ts @@ -0,0 +1,42 @@ +import { Model, DataTypes, Optional } from 'sequelize'; +import sequelize from './index'; +// create a permission interface with the following attributes, id, name +export interface PermissionAttributes { + id: string; + name: string; + createdAt?: Date; + updatedAt?: Date; +} +// create a permission creation interface with the following attributes, name, createdAt, updatedAt +export interface PermissionCreationAttributes extends Optional {} +export default class Permission + extends Model + implements PermissionAttributes +{ + public id!: string; + public name!: string; + public readonly createdAt!: Date; + public readonly updatedAt!: Date; +} +Permission.init( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + }, + name: { + type: DataTypes.STRING(120), + allowNull: false, + unique: true, + set(value: string) { + this.setDataValue('name', value.toLowerCase()); + }, + }, + }, + + { + sequelize: sequelize, + timestamps: true, + } +); diff --git a/src/database/models/role.ts b/src/database/models/role.ts index 4d76ec01..5f258982 100644 --- a/src/database/models/role.ts +++ b/src/database/models/role.ts @@ -1,5 +1,7 @@ import { Model, Optional, DataTypes, UUIDV4 } from 'sequelize'; import sequelize from './index'; +import Permission from './permission'; +import RolePermission from './rolePermissions'; export interface RoleAttributes { id: string; @@ -21,22 +23,29 @@ Role.init( { id: { type: DataTypes.UUID, - defaultValue: UUIDV4, primaryKey: true, + defaultValue: UUIDV4, }, name: { - type: DataTypes.STRING, + type: DataTypes.ENUM('admin', 'buyer', 'guest', 'vendor'), allowNull: false, - }, - displayName: { - type: DataTypes.STRING, - allowNull: true, + unique: true, + set(value: string) { + this.setDataValue('name', value.toLowerCase()); + }, }, }, + { sequelize: sequelize, timestamps: true, } ); - +Role.belongsToMany(Permission, { + through: RolePermission, + foreignKey: 'roleId', + onDelete: 'CASCADE', + onUpdate: 'RESTRICT', +}); +Permission.belongsToMany(Role, { through: RolePermission, foreignKey: 'permissionId' }); export default Role; diff --git a/src/database/models/rolePermissions.ts b/src/database/models/rolePermissions.ts new file mode 100644 index 00000000..a86955a3 --- /dev/null +++ b/src/database/models/rolePermissions.ts @@ -0,0 +1,36 @@ +import { Model, DataTypes } from 'sequelize'; +import sequelize from './index'; + +export default class RolePermission extends Model { + public roleId!: number; + public permissionId!: number; +} + +RolePermission.init( + { + roleId: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'roles', + key: 'id', + }, + }, + permissionId: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'permissions', + key: 'id', + }, + }, + }, + { + tableName: 'role_permissions', + sequelize, + } +); + +// (async () => { +// await sequelize.sync(); +// })(); diff --git a/src/database/models/sellerRequest.ts b/src/database/models/sellerRequest.ts new file mode 100644 index 00000000..039f8319 --- /dev/null +++ b/src/database/models/sellerRequest.ts @@ -0,0 +1,62 @@ +import { Model, DataTypes, Optional } from 'sequelize'; +import sequelize from './index'; +interface VendorRequestAttributes { + id: string; + vendorId: string; + status?: string; // pending, approved, rejected + createdAt?: Date; + updatedAt?: Date; + documents: string[]; + agreement: boolean; +} +interface VendorRequestCreationAttributes extends Optional {} +class VendorRequest + extends Model + implements VendorRequestAttributes +{ + public id!: string; + public vendorId!: string; + public status!: string; + public documents!: string[]; + public agreement!: boolean; + public readonly createdAt?: Date; + public readonly updatedAt?: Date; +} +VendorRequest.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + vendorId: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: 'Users', + key: 'id', + }, + onDelete: 'CASCADE', + onUpdate: 'RESTRICT', + }, + status: { + type: DataTypes.ENUM('pending', 'approved', 'rejected'), + defaultValue: 'pending', + }, + documents: { + type: DataTypes.ARRAY(DataTypes.STRING), + allowNull: false, + }, + agreement: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + }, + { + sequelize: sequelize, + tableName: 'VendorRequest', + } +); + +export default VendorRequest; diff --git a/src/database/models/user.ts b/src/database/models/user.ts index 69ffd0b0..f247898f 100644 --- a/src/database/models/user.ts +++ b/src/database/models/user.ts @@ -4,6 +4,7 @@ import sequelize from './index'; import Role from './role'; import getDefaultRole from '../../helpers/defaultRoleGenerator'; import logger from '../../logs/config'; +import VendorRequest from './sellerRequest'; enum UserStatus { ACTIVE = 'active', @@ -115,8 +116,8 @@ User.init( defaultValue: UserStatus.ACTIVE, }, RoleId: { - type: DataTypes.UUID, - defaultValue: UUIDV4, + type: DataTypes.INTEGER, + allowNull: false, references: { model: Role, key: 'id', @@ -128,7 +129,7 @@ User.init( defaultValue: false, }, }, - { sequelize: sequelize, timestamps: true } + { sequelize: sequelize, timestamps: true, modelName: 'User' } ); User.beforeCreate(async (user: User) => { @@ -142,5 +143,6 @@ User.beforeCreate(async (user: User) => { }); User.belongsTo(Role); - +VendorRequest.belongsTo(User, { foreignKey: 'vendorId' }); +User.hasOne(VendorRequest, { foreignKey: 'vendorId' }); export default User; diff --git a/src/database/seeders/20240427082911-create-default-role.js b/src/database/seeders/20240427082911-create-default-role.js index 5cb9369d..3ac39631 100644 --- a/src/database/seeders/20240427082911-create-default-role.js +++ b/src/database/seeders/20240427082911-create-default-role.js @@ -5,18 +5,10 @@ const { v4: uuidv4 } = require('uuid'); module.exports = { up: async (queryInterface, Sequelize) => { - return queryInterface.bulkInsert('Roles', [ + await queryInterface.bulkInsert('Roles', [ { id: uuidv4(), - name: 'buyer', - displayName: 'Buyer Role', - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: '6ef1e121-304a-4f08-ad4e-cd07f9578b52', - name: 'admin', - displayName: 'Admin Role', + name: process.env.ADMIN_ROLE, createdAt: new Date(), updatedAt: new Date(), }, @@ -24,6 +16,6 @@ module.exports = { }, down: async (queryInterface, Sequelize) => { - return queryInterface.bulkDelete('Roles', { name: 'buyer' }); + return queryInterface.bulkDelete('Roles', null, {}); }, }; diff --git a/src/database/seeders/20240429200629-add-seller-role.js b/src/database/seeders/20240429200629-add-seller-role.js deleted file mode 100644 index fe1c1301..00000000 --- a/src/database/seeders/20240429200629-add-seller-role.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; -const { v4: uuidv4 } = require('uuid'); - -/** @type {import('sequelize-cli').Migration} */ - -module.exports = { - up: async (queryInterface, Sequelize) => { - return queryInterface.bulkInsert('Roles', [ - { - id: uuidv4(), - name: 'seller', - displayName: 'Seller Role', - createdAt: new Date(), - updatedAt: new Date(), - }, - ]); - }, - - down: async (queryInterface, Sequelize) => { - return queryInterface.bulkDelete('Roles', { name: 'seller' }); - }, -}; diff --git a/src/database/seeders/20240501105101-permissions.js b/src/database/seeders/20240501105101-permissions.js new file mode 100644 index 00000000..238eb888 --- /dev/null +++ b/src/database/seeders/20240501105101-permissions.js @@ -0,0 +1,37 @@ +'use strict'; +const { v4: uuidv4 } = require('uuid'); + +module.exports = { + up: async (queryInterface, Sequelize) => { + const defaultPermissions = dummies => { + const permissions = []; + for (let i = 0; i < dummies.length; i++) { + permissions.push({ + id: uuidv4(), + name: dummies[i], + createdAt: new Date(), + updatedAt: new Date(), + }); + } + return permissions; + }; + const permissions = [ + 'view items', + 'manage cart', + 'buy items', + 'rate platform', + 'rate vendors', + 'manage stock', + 'manage users', + 'inspect stock', + ]; + await queryInterface.bulkInsert( + 'Permissions', + defaultPermissions(permissions) + ); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('Permissions', null, {}); + }, +}; diff --git a/src/database/seeders/20240501163745-User.js b/src/database/seeders/20240501163745-User.js index 3f1ebd8d..6613e22b 100644 --- a/src/database/seeders/20240501163745-User.js +++ b/src/database/seeders/20240501163745-User.js @@ -1,34 +1,66 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ 'use strict'; const { v4: uuidv4 } = require('uuid'); - +const bcrypt = require('bcrypt'); +require('ts-node').register(); +const Role = require('../models/role').default; /** @type {import('sequelize-cli').Seed} */ + module.exports = { async up(queryInterface, Sequelize) { - return queryInterface.bulkInsert( - 'Users', - [ + try { + await Role.sync(); + const roles = await Role.findAll(); + if (roles.length === 0) { + throw new Error('No roles found'); + } + const role = roles[0].id; + const { + firstName, + lastName, + email, + password, + gender, + verified, + status, + phoneNumber, + } = process.env; + const users = [ { - id: uuidv4(), - firstName: 'admin', - lastName: '', - email: process.env.EMAIL, - password: - '$2b$10$ZCgzouXesg4Zqgj22u7ale5aAOJzmjfOchCpMlSgBMV8o2f.zdYUq', - gender: 'not specified', - phoneNumber: process.env.ADMIN_PHONE, - verified: true, - createdAt: new Date(), - updatedAt: new Date(), - status: 'active', - RoleId: '6ef1e121-304a-4f08-ad4e-cd07f9578b52', // Replace with the actual RoleId + firstName, + lastName, + email, + password: await hashPassword(password), + gender, + verified: toBoolean(verified), + status, + RoleId: role, + phoneNumber, }, - ], - {} - ); + ]; + + return queryInterface.bulkInsert('Users', users.map(addUuid)); + } catch (err) { + console.log(err); + throw err; + } }, async down(queryInterface, Sequelize) { return queryInterface.bulkDelete('Users', null, {}); }, }; + +async function hashPassword(password) { + return await bcrypt.hash(password, 10); +} +function toBoolean(value) { + return value === 'true'; +} +function addUuid(user) { + return { + ...user, + id: uuidv4(), + createdAt: new Date(), + updatedAt: new Date(), + }; +} diff --git a/src/docs/permissions.yaml b/src/docs/permissions.yaml new file mode 100644 index 00000000..6ef83ac5 --- /dev/null +++ b/src/docs/permissions.yaml @@ -0,0 +1,239 @@ +tags: + - name: Permissions + description: API for managing permissions. +paths: + /api/permissions: + post: + tags: + - Permissions + summary: Create a permission + security: + - bearerAuth: [] + description: Creates a new permission. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: The name of the permission. + required: + - name + responses: + '201': + description: Successfully created a permission. + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + description: Indicates if the operation was successful. + example: true + data: + type: object + description: The created permission object. + get: + summary: Get all permissions + tags: + - Permissions + description: Retrieves a list of all permissions. + security: + - bearerAuth: [] + responses: + '200': + description: Successfully retrieved permissions. + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + description: Indicates if the operation was successful. + example: true + data: + type: array + items: + type: object + properties: + id: + type: integer + description: The ID of the permission. + name: + type: string + description: The name of the permission. + /api/permissions/{id}: + get: + summary: Get a single permission + security: + - bearerAuth: [] + tags: + - Permissions + description: Retrieves a single permission by ID. + parameters: + - in: path + name: id + required: true + schema: + type: integer + description: The ID of the permission to retrieve. + responses: + '200': + description: Successfully retrieved the permission. + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + description: Indicates if the operation was successful. + example: true + data: + type: object + properties: + id: + type: integer + description: The ID of the permission. + name: + type: string + description: The name of the permission. + '404': + description: Permission not found. + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + example: false + errorMessage: + type: string + example: Permission not found. + put: + summary: Update a permission + tags: + - Permissions + description: Updates a permission by ID. + security: + - bearerAuth: [] + parameters: + - in: path + name: id + required: true + schema: + type: integer + description: The ID of the permission to update. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: The new name for the permission. + required: + - name + responses: + '200': + description: Successfully updated the permission. + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + description: Indicates if the operation was successful. + example: true + data: + type: object + properties: + id: + type: integer + description: The ID of the permission. + name: + type: string + description: The updated name of the permission. + '400': + description: Name is required. + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + example: false + errorMessage: + type: string + example: Name is required. + '404': + description: Permission not found. + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + example: false + errorMessage: + type: string + example: Permission not found. + delete: + summary: Delete a permission + security: + - bearerAuth: [] + tags: + - Permissions + description: Deletes a permission by ID. + parameters: + - in: path + name: id + required: true + schema: + type: integer + description: The ID of the permission to delete. + responses: + '200': + description: Successfully deleted the permission. + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + description: Indicates if the operation was successful. + example: true + message: + type: string + description: Confirmation message. + example: Permission deleted successfully! + '404': + description: Permission not found. + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + example: false + errorMessage: + type: string + example: Permission not found. +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer diff --git a/src/docs/vendor.yaml b/src/docs/vendor.yaml new file mode 100644 index 00000000..60014af1 --- /dev/null +++ b/src/docs/vendor.yaml @@ -0,0 +1,232 @@ +tags: + name: Vendor_Request + description: Vendor Request Management API + +paths: + /api/vendor-requests: + post: + summary: Create a new product + security: + - bearerAuth: [] + tags: + - Vendor_Request + description: Create a new Vendor Request + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + agreement *: + type: boolean + description: Agreement to terms and conditions example(true/false) + documents *: + type: array + items: + type: string + format: binary + description: Array of vendor documents (minimum 6 documents required) + responses: + 201: + description: Vendor request created successfully + content: + application/json: + schema: + type: object + properties: + status: + type: integer + example: 201 + ok: + type: boolean + example: true + data: + $ref: '#/components/schemas/VendorRequest' + message: + type: string + example: Thank you for accepting to work with us! + 400: + description: Bad request, invalid parameters provided + content: + application/json: + schema: + type: object + properties: + status: + type: integer + example: 400 + ok: + type: boolean + example: false + message: + type: string + example: Vendor documents should be at least 6 documents + 500: + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + status: + type: integer + example: 500 + ok: + type: boolean + example: false + message: + type: string + example: Something went wrong when creating the request + get: + summary: Get all vendor requests + security: + - bearerAuth: [] + tags: + - Vendor_Request + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/VendorRequest' + 500: + description: vendor Requests Not found! +/api/vendor-requests/{id}: + get: + summary: Get a vendor request by ID + security: + - bearerAuth: [] + tags: + - Vendor_Request + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/VendorRequest' + '404': + description: Vendor request not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + patch: + summary: Update a vendor request by ID + security: + - bearerAuth: [] + tags: + - Vendor_Request + parameters: + - name: id + in: path + required: true + schema: + type: string + - name: authorization + in: header + required: true + schema: + type: string + format: JWT + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/VendorRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/VendorRequest' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + '404': + description: Vendor request not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + delete: + summary: Delete a vendor request by ID + security: + - bearerAuth: [] + tags: + - Vendor_Request + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessMessage' + '404': + description: Vendor request not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + +security: + - bearerAuth: [] +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + schemas: + UpdateVendorRequestInput: + type: object + properties: + agreement: + type: string + required: + - agreement + VendorRequest: + type: object + properties: + agreement: + type: boolean + description: Agreement to our terms and conditions + documents: + type: array + items: + type: string + description: Array of URLs of vendor documents + SuccessMessage: + type: object + properties: + ok: + type: boolean + example: true + message: + type: string + ErrorMessage: + type: object + properties: + ok: + type: boolean + example: false + message: + type: string diff --git a/src/middlewares/authMiddlewares.ts b/src/middlewares/authMiddlewares.ts index bc5c76df..03634748 100644 --- a/src/middlewares/authMiddlewares.ts +++ b/src/middlewares/authMiddlewares.ts @@ -55,7 +55,8 @@ export const isAuthenticated = (req: Request, res: Response, next: NextFunction) } }; // Middleware to check user roles -export const checkUserRoles = (requiredRole: string) => { +type userRole = 'admin' | 'buyer' | 'seller'; +export const checkUserRoles = (requiredRole: userRole) => { return async (req: Request, res: Response, next: NextFunction) => { const user = (await req.user) as any; const userRole = user.Role.name; diff --git a/src/routes/index.ts b/src/routes/index.ts index a474205b..d307eaa6 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -8,6 +8,9 @@ import productRouter from './productRoutes'; import { categoryRouter } from './categoryRouter'; import wishlistRoute from './wishlistRoute'; import notificationRoutes from './notificationRoutes'; +import { permissionRoute } from './permissionRoute'; +import { sellerRequestRouter } from './sellerRequestRoute'; + const router = Router(); router.use('/users', userRoute); @@ -19,4 +22,7 @@ router.use('/products', productRouter); router.use('/category', categoryRouter); router.use('/wishlist', wishlistRoute); router.use('/notifications', notificationRoutes); +router.use('/permissions', permissionRoute); +router.use('/vendor-requests', sellerRequestRouter); + export default router; diff --git a/src/routes/permissionRoute.ts b/src/routes/permissionRoute.ts new file mode 100644 index 00000000..160c1e5b --- /dev/null +++ b/src/routes/permissionRoute.ts @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/no-misused-promises */ +import { Router } from 'express'; +import { isAuthenticated, checkUserRoles } from '../middlewares/authMiddlewares'; +import { + createPermission, + getAllPermissions, + getSinglePermission, + updatePermission, + deletePermission, +} from '../controllers/permissionController'; +export const permissionRoute = Router(); +permissionRoute + .route('/') + .post(isAuthenticated, checkUserRoles('admin'), createPermission) + .get(isAuthenticated, checkUserRoles('admin'), getAllPermissions); +permissionRoute + .route('/:id') + .get(isAuthenticated, checkUserRoles('admin'), getSinglePermission) + .put(isAuthenticated, checkUserRoles('admin'), updatePermission) + .delete(isAuthenticated, checkUserRoles('admin'), deletePermission); diff --git a/src/routes/roleRoute.ts b/src/routes/roleRoute.ts index 1b9a6a49..14b1ccae 100644 --- a/src/routes/roleRoute.ts +++ b/src/routes/roleRoute.ts @@ -5,9 +5,9 @@ import { isAuthenticated, checkUserRoles } from '../middlewares/authMiddlewares' const router = Router(); router.get('/', isAuthenticated, getAllRoles); -router.get('/:id', getSingleRole); +router.get('/:id', isAuthenticated, getSingleRole); router.post('/', isAuthenticated, checkUserRoles('admin'), createRole); -router.patch('/:id', updateRole); -router.delete('/:id', deleteRole); +router.patch('/:id', isAuthenticated, checkUserRoles('admin'), updateRole); +router.delete('/:id', isAuthenticated, checkUserRoles('admin'), deleteRole); export default router; diff --git a/src/routes/sellerRequestRoute.ts b/src/routes/sellerRequestRoute.ts new file mode 100644 index 00000000..98d803e8 --- /dev/null +++ b/src/routes/sellerRequestRoute.ts @@ -0,0 +1,23 @@ +/* eslint-disable @typescript-eslint/no-misused-promises */ +import { isAuthenticated } from '../middlewares/authMiddlewares'; +import { Router } from 'express'; +import multerUpload from '../helpers/multer'; +import { + createSellerRequest, + getAllSellerRequests, + getSellerRequest, + updateSellerRequest, + deleteSellerRequest, +} from '../controllers/sellerRequestController'; + +export const sellerRequestRouter = Router(); + +sellerRequestRouter + .route('/') + .post(isAuthenticated, multerUpload.array('documents', 6), createSellerRequest) + .get(isAuthenticated, getAllSellerRequests); +sellerRequestRouter + .route('/:id') + .get(isAuthenticated, getSellerRequest) + .patch(isAuthenticated, multerUpload.array('documents', 6), updateSellerRequest) + .delete(isAuthenticated, deleteSellerRequest); diff --git a/src/server.ts b/src/server.ts index e754fdfa..e9879753 100644 --- a/src/server.ts +++ b/src/server.ts @@ -14,7 +14,7 @@ import scheduledTasks from './config/cornJobs'; dotenv.config(); -const app: Application = express(); +export const app: Application = express(); app.use(cors()); app.use(passport.initialize());