Merge pull request #7 from kfnawaz/feat/auth

Feat: Added Auth module
This commit is contained in:
bluestreamlds
2021-12-27 12:25:08 +05:30
committed by GitHub
15 changed files with 533 additions and 27 deletions

View File

@@ -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
View 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,
};

View File

@@ -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,
};

View File

@@ -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;

View File

@@ -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 };

View File

@@ -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 });
},
};

View File

@@ -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;

View 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);
}
},
};

View 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;

View 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);
}
},
};

View 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;

View 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 });
}
},
};

View File

@@ -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;

View 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
View 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;