@@ -1,2 +1,5 @@
|
||||
API_PORT=9000
|
||||
MONGODB_URI=
|
||||
MONGODB_URI=
|
||||
JWT_SECRET=
|
||||
JWT_REFRESH_EXPIRY_TIME=
|
||||
JWT_ACCESS_EXPIRY_TIME=
|
||||
53
src/config/auth.js
Normal file
53
src/config/auth.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const jwt = require("jsonwebtoken");
|
||||
const { JWT_SECRET } = require("./env");
|
||||
const User = require("../models/User");
|
||||
const constants = require("./constants");
|
||||
|
||||
const authenticate = async (token) => {
|
||||
const decodedToken = jwt.verify(token, JWT_SECRET);
|
||||
if (decodedToken) {
|
||||
return await User.findById(decodedToken.id)
|
||||
.populate({ path: "roles", populate: "permissions" })
|
||||
.populate("permissions");
|
||||
}
|
||||
};
|
||||
|
||||
const authorize = async (
|
||||
user,
|
||||
requiredRoles = [],
|
||||
requiredPermissions = []
|
||||
) => {
|
||||
const userRoles = user.roles.map((_) => _._id);
|
||||
const userPermissions = [
|
||||
...user.permissions.map((_) => _._id),
|
||||
...userRoles.map((_) => _.permissions).flat(),
|
||||
];
|
||||
|
||||
return (
|
||||
user != undefined &&
|
||||
requiredRoles.every((_) => userRoles.includes(_)) &&
|
||||
requiredPermissions.every((_) => userPermissions.includes(_))
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
AuthenticateMiddleware: async (req, res, next) => {
|
||||
try {
|
||||
const token = req.headers.authorization || "";
|
||||
if (token) {
|
||||
const user = authenticate(token);
|
||||
res.locals.user = user;
|
||||
next();
|
||||
}
|
||||
} catch (error) {
|
||||
res
|
||||
.status(401)
|
||||
.send({
|
||||
success: false,
|
||||
error: constants.AUTHENTICATION_FAILURE_ERROR_MESSAGE,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
AuthorizeUser: authorize,
|
||||
};
|
||||
@@ -12,6 +12,8 @@ const UserActions = [
|
||||
"Receive",
|
||||
];
|
||||
|
||||
const InventoryScopes = ["Inventory", "Material", "Item"];
|
||||
|
||||
const WarehouseScopes = [
|
||||
"Warehouse",
|
||||
"Zone",
|
||||
@@ -38,9 +40,22 @@ const CustomAttributeTypes = [
|
||||
"Enumerable",
|
||||
];
|
||||
|
||||
const AUTHENTICATION_FAILURE_ERROR_MESSAGE =
|
||||
"Authentication Failed!";
|
||||
const AUTHORIZATION_FAILURE_ERROR_MESSAGE =
|
||||
"User not permitted due to lack of access!";
|
||||
|
||||
module.exports = {
|
||||
UserActions,
|
||||
InventoryScopes,
|
||||
WarehouseScopes,
|
||||
InventoryTypes,
|
||||
CustomAttributeTypes,
|
||||
SUPER_ADMIN_ROLE: "super-admin",
|
||||
COMPANY_ADMIN_ROLE: "company-admin",
|
||||
WAREHOUSE_ADMIN_ROLE: "warehouse-admin",
|
||||
ZONE_ADMIN_ROLE: "zone-admin",
|
||||
AREA_ADMIN_ROLE: "area-admin",
|
||||
AUTHENTICATION_FAILURE_ERROR_MESSAGE,
|
||||
AUTHORIZATION_FAILURE_ERROR_MESSAGE,
|
||||
};
|
||||
|
||||
@@ -3,6 +3,10 @@ require("dotenv").config();
|
||||
const envVariables = {
|
||||
API_PORT: process.env.API_PORT || "3000",
|
||||
MONGODB_URI: process.env.MONGODB_URI || "mongodb://localhost:12017",
|
||||
JWT_SECRET: process.env.JWT_SECRET || "secret123",
|
||||
JWT_REFRESH_EXPIRY_TIME:
|
||||
parseInt(process.env.JWT_REFRESH_EXPIRY_TIME) || 3600,
|
||||
JWT_ACCESS_EXPIRY_TIME: parseInt(process.env.JWT_ACCESS_EXPIRY_TIME) || 86400,
|
||||
};
|
||||
|
||||
module.exports = envVariables;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
const router = require("express").Router();
|
||||
const userRouter = require("./user.router");
|
||||
const userRoleRouter = require("./userRole.router");
|
||||
const userPermissionRouter = require("./userPermission.router");
|
||||
const { AuthenticateMiddleware } = require("../config/auth");
|
||||
|
||||
const companyRouter = require("./company.router");
|
||||
const warehouseRouter = require("./warehouse.router");
|
||||
const zoneRouter = require("./zone.router");
|
||||
@@ -8,6 +12,8 @@ const bayRouter = require("./bay.router");
|
||||
const rowRouter = require("./row.router");
|
||||
const levelRouter = require("./level.router");
|
||||
|
||||
router.use("/user-role", AuthenticateMiddleware, userRoleRouter);
|
||||
router.use("/user-permission", AuthenticateMiddleware, userPermissionRouter);
|
||||
router.use("/user", userRouter);
|
||||
router.use("/company", companyRouter);
|
||||
router.use("/warehouse", warehouseRouter);
|
||||
@@ -17,8 +23,14 @@ router.use("/bay", bayRouter);
|
||||
router.use("/row", rowRouter);
|
||||
router.use("/level", levelRouter);
|
||||
|
||||
|
||||
router.get("/", (req, res) => {
|
||||
res.send("Hello world");
|
||||
res.send({ success: true, message: "Hello world" });
|
||||
});
|
||||
|
||||
router.use(function (err, req, res, next) {
|
||||
console.error(err.stack);
|
||||
res.status(500).send({ success: false, error: `Error: ${err.message}` });
|
||||
});
|
||||
|
||||
module.exports = { router };
|
||||
|
||||
@@ -1,5 +1,123 @@
|
||||
const bcrypt = require("bcrypt");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const User = require("./../models/User");
|
||||
const {
|
||||
JWT_SECRET,
|
||||
JWT_REFRESH_EXPIRY_TIME,
|
||||
JWT_ACCESS_EXPIRY_TIME,
|
||||
} = require("./../config/env");
|
||||
const UserRole = require("../models/UserRole");
|
||||
const UserPermission = require("../models/UserPermission");
|
||||
|
||||
const createAccessToken = (id) => {
|
||||
return jwt.sign({ id }, JWT_SECRET, {
|
||||
expiresIn: JWT_ACCESS_EXPIRY_TIME,
|
||||
});
|
||||
};
|
||||
|
||||
const createRefreshToken = (id) => {
|
||||
return jwt.sign({ id }, JWT_SECRET, {
|
||||
expiresIn: JWT_REFRESH_EXPIRY_TIME,
|
||||
});
|
||||
};
|
||||
|
||||
const getValidIds = async (ids, model) => {
|
||||
const verifiedIds = ids.filter((permission) =>
|
||||
mongoose.isValidObjectId(permission)
|
||||
);
|
||||
const verifiedObjects = await model
|
||||
.find({
|
||||
id: { $in: verifiedIds },
|
||||
})
|
||||
.select({ _id: 1 });
|
||||
return verifiedObjects.map((_) => _._id);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getUser: async (req, res) => {
|
||||
res.send("Not Found");
|
||||
registerUser: async (req, res, next) => {
|
||||
const { email, fullName, password } = req.body;
|
||||
try {
|
||||
const salt = await bcrypt.genSalt();
|
||||
const newUser = {
|
||||
email: email,
|
||||
fullName: fullName,
|
||||
password: await bcrypt.hash(password, salt),
|
||||
};
|
||||
|
||||
const user = await User.create(newUser);
|
||||
console.log({ msg: "new user created", user });
|
||||
|
||||
res.send({ success: true, message: "User successfully created!" });
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
|
||||
loginUser: async (req, res, next) => {
|
||||
const { email, password } = req.body;
|
||||
try {
|
||||
const user = await User.login(email, password);
|
||||
|
||||
const accessToken = createAccessToken(user._id);
|
||||
const refreshToken = createRefreshToken(user._id);
|
||||
|
||||
user.accessToken = accessToken;
|
||||
user.refreshToken = refreshToken;
|
||||
await user.save();
|
||||
|
||||
res.send({
|
||||
success: true,
|
||||
data: {
|
||||
email: user.email,
|
||||
fullName: user.fullName,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
|
||||
addUserAccessControl: async (req, res, next) => {
|
||||
const { user, roles, permissions } = req.body;
|
||||
if (!mongoose.isValidObjectId(user)) {
|
||||
throw new Error(`invalid format for user id field`);
|
||||
}
|
||||
const verifiedRoleIds = await getValidIds(roles, UserRole);
|
||||
const verifiedPermissionIds = await getValidIds(
|
||||
permissions,
|
||||
UserPermission
|
||||
);
|
||||
const response = await User.findByIdAndUpdate(user, {
|
||||
$push: {
|
||||
roles: { $each: verifiedRoleIds },
|
||||
permissions: { $each: verifiedPermissionIds },
|
||||
},
|
||||
});
|
||||
res.send({ success: true, data: response });
|
||||
},
|
||||
|
||||
removeUserAccessControl: async (req, res, next) => {
|
||||
const { user, roles, permissions } = req.body;
|
||||
if (!mongoose.isValidObjectId(user)) {
|
||||
throw new Error(`invalid format for user id field`);
|
||||
}
|
||||
const verifiedRoleIds = await getValidIds(roles, UserRole);
|
||||
const verifiedPermissionIds = await getValidIds(
|
||||
permissions,
|
||||
UserPermission
|
||||
);
|
||||
const response = await User.findByIdAndUpdate(user, {
|
||||
$pull: {
|
||||
roles: { $in: verifiedRoleIds },
|
||||
permissions: { $in: verifiedPermissionIds },
|
||||
},
|
||||
});
|
||||
res.send({ success: true, data: response });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
const router = require("express").Router();
|
||||
const controller = require("./user.controller");
|
||||
const { AuthenticateMiddleware } = require("../config/auth");
|
||||
const { SuperAdminCheck } = require("./utils/authorize");
|
||||
|
||||
router.get("/:id", controller.getUser);
|
||||
router.post("/register", controller.registerUser);
|
||||
router.post("/login", controller.loginUser);
|
||||
router.post(
|
||||
"/:id/addAccess",
|
||||
AuthenticateMiddleware,
|
||||
SuperAdminCheck,
|
||||
controller.addUserAccessControl
|
||||
);
|
||||
router.post(
|
||||
"/:id/removeAccess",
|
||||
AuthenticateMiddleware,
|
||||
SuperAdminCheck,
|
||||
controller.removeUserAccessControl
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
96
src/controller/userPermission.controller.js
Normal file
96
src/controller/userPermission.controller.js
Normal file
@@ -0,0 +1,96 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const UserPermission = require("./../models/UserPermission");
|
||||
const { InventoryScopes, WarehouseScopes } = require("./../config/constants");
|
||||
|
||||
const getScopes = async (scopes, searchSet) => {
|
||||
const verifiedScopes = [];
|
||||
if (scopes !== undefined && Array.isArray(scopes)) {
|
||||
for (const scope of scopes) {
|
||||
if (mongoose.isValidObjectId(scope.id)) {
|
||||
if (scope.type !== undefined && searchSet.contains(scope.type)) {
|
||||
const model = require(`../models/${scope.type}`);
|
||||
const inventoryObject = await model.findById(scope.id);
|
||||
if (inventoryObject == undefined) {
|
||||
continue;
|
||||
}
|
||||
verifiedScopes.push({
|
||||
id: inventoryObject._id,
|
||||
type: scope.type,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new Error(`invalid data format for object-id - ${scope.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return verifiedScopes;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getAllPermissions: async (req, res, next) => {
|
||||
let { page, perPage } = req.query;
|
||||
page = page || 0;
|
||||
perPage = perPage || 10;
|
||||
|
||||
const result = await UserPermission.find(
|
||||
{},
|
||||
{ id: 1, name: 1, inventoryScopes: 1, warehouseScopes: 1, actions: 1 },
|
||||
{ skip: page * perPage, limit: perPage }
|
||||
);
|
||||
res.send({ success: true, data: result });
|
||||
},
|
||||
getPermission: async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
if (mongoose.isValidObjectId(id)) {
|
||||
const permission = await UserPermission.findById(id);
|
||||
res.send({ success: true, data: permission });
|
||||
} else {
|
||||
throw new Error(`invalid data format for object-id - ${id}`);
|
||||
}
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
},
|
||||
createPermission: async (req, res, next) => {
|
||||
try {
|
||||
const { name, inventoryScopes, warehouseScopes, actions } = req.body;
|
||||
const verifiedInventoryScopes = await getScopes(
|
||||
inventoryScopes,
|
||||
InventoryScopes
|
||||
);
|
||||
const verifiedWarehouseScopes = await getScopes(
|
||||
warehouseScopes,
|
||||
WarehouseScopes
|
||||
);
|
||||
|
||||
const newUserPermission = await UserPermission.create({
|
||||
name,
|
||||
inventoryScopes: verifiedInventoryScopes,
|
||||
warehouseScopes: verifiedWarehouseScopes,
|
||||
actions: actions == undefined ? [] : actions,
|
||||
});
|
||||
res.send({ success: true, data: newUserPermission });
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
},
|
||||
updatePermission: async (req, res, next) => {
|
||||
// Need more clarity
|
||||
res.send({ success: false, error: "not implemented" });
|
||||
},
|
||||
deletePermission: async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
if (mongoose.isValidObjectId(id)) {
|
||||
const result = await UserPermission.deleteOne({ _id: id });
|
||||
res.send({ success: true, data: result });
|
||||
} else {
|
||||
throw new Error(`invalid data format for object-id - ${id}`);
|
||||
}
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
10
src/controller/userPermission.router.js
Normal file
10
src/controller/userPermission.router.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const router = require("express").Router();
|
||||
const controller = require("./userPermission.controller");
|
||||
|
||||
router.get("/all", controller.getAllPermissions);
|
||||
router.get("/:id", controller.getPermission);
|
||||
router.post("/create", controller.createPermission);
|
||||
router.post("/:id", controller.updatePermission);
|
||||
router.delete("/:id", controller.deletePermission);
|
||||
|
||||
module.exports = router;
|
||||
75
src/controller/userRole.controller.js
Normal file
75
src/controller/userRole.controller.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const mongoose = require("mongoose");
|
||||
const UserRole = require("../models/UserRole");
|
||||
const UserPermission = require("../models/UserPermission");
|
||||
|
||||
const getValidPermissions = async (permissions) => {
|
||||
const verifiedPermissions = permissions.filter((permission) =>
|
||||
mongoose.isValidObjectId(permission)
|
||||
);
|
||||
const permissionObjects = await UserPermission.find({
|
||||
id: { $in: verifiedPermissions },
|
||||
}).select({ _id: 1 });
|
||||
return permissionObjects.map((_) => _._id);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getAllRoles: async (req, res, next) => {
|
||||
let { page, perPage } = req.query;
|
||||
page = page || 0;
|
||||
perPage = perPage || 10;
|
||||
|
||||
const result = await UserRole.find(
|
||||
{},
|
||||
{
|
||||
id: 1,
|
||||
name: 1,
|
||||
permissions: 1,
|
||||
},
|
||||
{ skip: page * perPage, limit: perPage }
|
||||
);
|
||||
res.send({ success: true, data: result });
|
||||
},
|
||||
getRole: async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
if (mongoose.isValidObjectId(id)) {
|
||||
const role = await UserRole.findById(id);
|
||||
res.send({ success: true, data: role });
|
||||
} else {
|
||||
throw new Error(`invalid data format for object-id - ${id}`);
|
||||
}
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
},
|
||||
createRole: async (req, res, next) => {
|
||||
try {
|
||||
const { name, permissions } = req.body;
|
||||
const verifiedPermissions = await getValidPermissions(permissions);
|
||||
const newUserRole = await UserRole.create({
|
||||
name,
|
||||
permissions: verifiedPermissions,
|
||||
});
|
||||
res.send({ success: true, data: newUserRole });
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
},
|
||||
updateRole: async (req, res, next) => {
|
||||
// Need more clarity
|
||||
res.send({ success: false, error: "not implemented" });
|
||||
},
|
||||
deleteRole: async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
if (mongoose.isValidObjectId(id)) {
|
||||
const result = await UserRole.deleteOne({ _id: id });
|
||||
res.send({ success: true, data: result });
|
||||
} else {
|
||||
throw new Error(`invalid data format for object-id - ${id}`);
|
||||
}
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
10
src/controller/userRole.router.js
Normal file
10
src/controller/userRole.router.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const router = require("express").Router();
|
||||
const controller = require("./userRole.controller");
|
||||
|
||||
router.get("/all", controller.getAllRoles);
|
||||
router.get("/:id", controller.getRole);
|
||||
router.post("/create", controller.createRole);
|
||||
router.post("/:id", controller.updateRole);
|
||||
router.delete("/:id", controller.deleteRole);
|
||||
|
||||
module.exports = router;
|
||||
16
src/controller/utils/authorize.js
Normal file
16
src/controller/utils/authorize.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const UserRole = require("../../models/UserRole");
|
||||
const { AuthorizeUser } = require("../../config/auth");
|
||||
const { SUPER_ADMIN_ROLE, AUTHORIZATION_FAILURE_ERROR_MESSAGE } = require("../../config/constants");
|
||||
|
||||
module.exports = {
|
||||
SuperAdminCheck: async (req, res, next) => {
|
||||
const SuperAdmin = await UserRole.findOne({ name: SUPER_ADMIN_ROLE });
|
||||
if (AuthorizeUser(req.locals.user, [SuperAdmin.id])) {
|
||||
next();
|
||||
} else {
|
||||
res
|
||||
.status(403)
|
||||
.send({ success: false, error: AUTHORIZATION_FAILURE_ERROR_MESSAGE });
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
const mongoose = require("mongoose");
|
||||
const { isEmail } = require("validator");
|
||||
const { UserActions, WarehouseScopes } = require("./../config/constants");
|
||||
const bcrypt = require("bcrypt");
|
||||
|
||||
const schema = new mongoose.Schema(
|
||||
{
|
||||
@@ -36,28 +36,16 @@ const schema = new mongoose.Schema(
|
||||
passwordResetToken: {
|
||||
type: String,
|
||||
},
|
||||
authPolicies: [
|
||||
roles: [
|
||||
{
|
||||
inventory: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Inventory",
|
||||
},
|
||||
warehouseScope: {
|
||||
on: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
refPath: "onModel",
|
||||
},
|
||||
onModel: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: WarehouseScopes,
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: UserActions,
|
||||
},
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "UserRole",
|
||||
},
|
||||
],
|
||||
permissions: [
|
||||
{
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "UserPermission",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -66,6 +54,18 @@ const schema = new mongoose.Schema(
|
||||
}
|
||||
);
|
||||
|
||||
schema.statics.login = async function (email, password) {
|
||||
const user = await this.findOne({ email });
|
||||
if (user) {
|
||||
const auth = await bcrypt.compare(password, user.password);
|
||||
if (auth) {
|
||||
return user;
|
||||
}
|
||||
throw Error("incorrect password");
|
||||
}
|
||||
throw Error("incorrect email");
|
||||
};
|
||||
|
||||
const User = mongoose.model("User", schema);
|
||||
|
||||
module.exports = User;
|
||||
|
||||
55
src/models/UserPermission.js
Normal file
55
src/models/UserPermission.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const mongoose = require("mongoose");
|
||||
const {
|
||||
UserActions,
|
||||
WarehouseScopes,
|
||||
InventoryScopes,
|
||||
} = require("./../config/constants");
|
||||
|
||||
const schema = new mongoose.Schema(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
trim: true,
|
||||
},
|
||||
inventoryScopes: [
|
||||
{
|
||||
id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
refPath: "type",
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: InventoryScopes,
|
||||
},
|
||||
},
|
||||
],
|
||||
warehouseScopes: [
|
||||
{
|
||||
id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
refPath: "type",
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: WarehouseScopes,
|
||||
},
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: String,
|
||||
required: true,
|
||||
enum: UserActions,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
const UserPermission = mongoose.model("UserPermission", schema);
|
||||
|
||||
module.exports = UserPermission;
|
||||
24
src/models/UserRole.js
Normal file
24
src/models/UserRole.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const schema = new mongoose.Schema(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
permissions: [
|
||||
{
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "UserPermission",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
const UserRole = mongoose.model("UserRole", schema);
|
||||
|
||||
module.exports = UserRole;
|
||||
Reference in New Issue
Block a user