fix
All checks were successful
Build And Deploy Main / build-and-deploy (push) Successful in 36s

This commit is contained in:
2026-03-16 14:52:20 -04:00
parent 2664177463
commit a500bef32d
11 changed files with 868 additions and 61 deletions

View File

@@ -1,10 +1,14 @@
const db = require("../models");
const Employee = db.employee;
const ExtUserPermission = db.ext_usr_perm;
const config = require("../config/auth.config");
const axios = require("axios");
var jwt = require("jsonwebtoken");
var bcrypt = require("bcryptjs");
const { splitSite } = require("../middlewares");
const HR_AUTH_LOGIN_ENDPOINT = "https://ws-hr.mayosolution.com/api/auth/login";
const SYSTEM_ACCESS_PERMISSION = "System Access";
const ALL_PERMISSIONS = [
'Dashboard',
@@ -91,62 +95,171 @@ const isEmployeeActive = (employeeDoc) => {
return `${rawStatus}`.trim().toLowerCase() === "active";
};
const parseExternalTokenId = (value = "") => {
const token = `${value || ""}`;
if (!token.startsWith("ext:")) return null;
const chunks = token.split(":");
if (chunks.length < 3) return null;
return {
externalUserId: chunks[1],
site: Number(chunks[2]) || null
};
};
const getExternalPermissionBySite = async (externalUserId, site) => {
if (!externalUserId || !site) return null;
return ExtUserPermission.findOne({
external_user_id: `${externalUserId}`,
allow_site: Number(site)
}).sort({ updatedAt: -1 });
};
const buildExternalAuthResponse = async (externalUser, site) => {
const permissionDoc = await getExternalPermissionBySite(externalUser?.id, site);
const permissions = Array.isArray(permissionDoc?.permissions) ? permissionDoc.permissions : [];
const tokenPayload = {
id: `ext:${externalUser?.id}:${site}`,
authSource: "external",
externalUserId: externalUser?.id,
site: Number(site),
username: externalUser?.username || "",
email: externalUser?.email || "",
name: externalUser?.name || "",
firstname: externalUser?.firstname || "",
lastname: externalUser?.lastname || "",
status: externalUser?.status || ""
};
const accessToken = jwt.sign(tokenPayload, config.secret, {
expiresIn: 86400
});
return {
accessToken,
username: externalUser?.username || "",
email: externalUser?.email || "",
roles: [],
permissions,
id: externalUser?.id,
name: externalUser?.name || "",
firstname: externalUser?.firstname || "",
lastname: externalUser?.lastname || "",
site: Number(site),
status: externalUser?.status || "",
auth_source: "external"
};
};
// Create and Save a new User
exports.login = (req, res) => {
var condition = {};
exports.login = async (req, res) => {
const emailUsername = req.body.emailUsername;
console.log('emailUsername', emailUsername);
if (emailUsername) {
condition = { $or: [
{ email: emailUsername },
{ username: emailUsername }
]};
condition = splitSite.splitSiteGet(req, condition);
Employee.find(condition)
.then(data => {
if (data && data.length > 0) {
const activeEmployee = isEmployeeActive(data[0]);
if (data.length === 1 && bcrypt.compareSync(
req.body.password,
data[0].password
) && activeEmployee) {
var token = jwt.sign({id: data[0].id}, config.secret, {
expiresIn: 86400 // 24 hours
});
res.send({
accessToken: token,
username: data[0].username,
email: data[0].email,
roles: data[0].roles,
permissions: getEffectivePermissions(data[0]),
id: data[0].id,
name: data[0].name,
name_cn: data[0].name_cn
} );
} else {
if (!activeEmployee) {
throw(Error('User is not activated'));
} else {
throw(Error('Email or Password Is Invalid'));
}
}
} else {
throw(Error('Email or Password Is Invalid'));
}
})
.catch(err => {
res.status(500).send({
message:
err.message || "Email Or Password Invalid"
});
});
} else {
throw(Error('email or username is required'));
const password = req.body.password;
const requestedSite = Number(req.body.site);
const site = Number.isInteger(requestedSite) && requestedSite > 0 ? requestedSite : splitSite.findSiteNumber(req);
if (!emailUsername) {
return res.status(400).send({ message: "email or username is required" });
}
}
try {
let condition = {
$or: [{ email: emailUsername }, { username: emailUsername }]
};
condition = splitSite.splitSiteGet(req, condition);
const localEmployees = await Employee.find(condition);
if (localEmployees && localEmployees.length > 0) {
const localEmployee = localEmployees[0];
const activeEmployee = isEmployeeActive(localEmployee);
const isPasswordCorrect = bcrypt.compareSync(password, localEmployee.password || "");
if (localEmployees.length === 1 && isPasswordCorrect && activeEmployee) {
const token = jwt.sign({ id: localEmployee.id }, config.secret, {
expiresIn: 86400 // 24 hours
});
return res.send({
accessToken: token,
username: localEmployee.username,
email: localEmployee.email,
roles: localEmployee.roles,
permissions: getEffectivePermissions(localEmployee),
id: localEmployee.id,
name: localEmployee.name,
name_cn: localEmployee.name_cn
});
}
if (!activeEmployee) {
throw Error("User is not activated");
}
// Local account exists but credential doesn't match. Continue to external auth fallback.
}
try {
const externalAuthResponse = await axios.post(
HR_AUTH_LOGIN_ENDPOINT,
{
emailUsername,
password,
site
},
{ timeout: 15000 }
);
const externalUser = externalAuthResponse?.data;
const isExternalUserValid =
externalUser &&
externalUser.id &&
`${externalUser?.status || ""}`.trim().toLowerCase() === "active";
if (isExternalUserValid) {
const loginPayload = await buildExternalAuthResponse(externalUser, site);
return res.send(loginPayload);
}
} catch (_externalError) {
// Fall through to existing invalid-login response.
}
throw Error("Email or Password Is Invalid");
} catch (err) {
return res.status(500).send({
message: err.message || "Email Or Password Invalid"
});
}
};
exports.me = (req, res) => {
const authPayload = req.authPayload || {};
const parsedExternalToken = parseExternalTokenId(req.userId);
const isExternalAuth =
authPayload?.authSource === "external" || !!parsedExternalToken?.externalUserId;
if (isExternalAuth) {
const site = Number(authPayload?.site) || parsedExternalToken?.site || splitSite.findSiteNumber(req);
const externalUserId = authPayload?.externalUserId || parsedExternalToken?.externalUserId;
return getExternalPermissionBySite(externalUserId, site)
.then((permissionDoc) => {
let permissions = Array.isArray(permissionDoc?.permissions) ? permissionDoc.permissions : [];
if (permissions.includes(SYSTEM_ACCESS_PERMISSION) === false) {
// Keep login valid; UI permission gate will lock down non-system users.
permissions = permissions.filter(Boolean);
}
return res.send({
username: authPayload?.username || "",
email: authPayload?.email || "",
roles: [],
permissions,
id: externalUserId,
name: authPayload?.name || "",
firstname: authPayload?.firstname || "",
lastname: authPayload?.lastname || "",
site,
status: authPayload?.status || "",
auth_source: "external"
});
})
.catch((err) => {
return res.status(500).send({
message: err.message || "Failed to fetch current user"
});
});
}
const condition = splitSite.splitSiteGet(req, { _id: req.userId });
Employee.findOne(condition)
.then((employee) => {

View File

@@ -1,8 +1,14 @@
const db = require("../models");
const Employee = db.employee;
const ExtUserPermission = db.ext_usr_perm;
const axios = require("axios");
var bcrypt = require("bcryptjs");
const { splitSite } = require("../middlewares");
const SYSTEM_ACCESS_PERMISSION = "System Access";
const HR_EMPLOYEE_LIST_ENDPOINT = "https://ws-hr.mayosolution.com/api/integration/employees/list";
const HR_INTEGRATION_USERNAME = "vibecodingking";
const HR_INTEGRATION_PASSWORD = "oAQC483f1jxdJdoJcd0kCAd7C";
const ALL_PERMISSIONS = [
'Dashboard',
@@ -312,3 +318,131 @@ exports.deleteEmployee = (req, res) => {
exports.getEmployeesWithUsernameOrEmail = (req, res) => {
};
exports.getExternalEmployeesList = async (req, res) => {
try {
const response = await axios.post(
HR_EMPLOYEE_LIST_ENDPOINT,
{
username: HR_INTEGRATION_USERNAME,
password: HR_INTEGRATION_PASSWORD,
status: "active"
},
{ timeout: 15000 }
);
const list = Array.isArray(response?.data) ? response.data : [];
res.send(list);
} catch (err) {
res.status(500).send({
message: err?.response?.data?.message || err.message || "Failed to fetch employees from HR system."
});
}
};
exports.getExternalUserPermission = async (req, res) => {
try {
const externalUserId = `${req.query.external_user_id || req.query.externalUserId || ""}`.trim();
const requestedSite = Number(req.query.site || req.query.allow_site);
const allowSite = Number.isInteger(requestedSite) && requestedSite > 0 ? requestedSite : splitSite.findSiteNumber(req);
if (!externalUserId) {
return res.status(400).send({ message: "external_user_id is required." });
}
const config = await ExtUserPermission.findOne({
external_user_id: externalUserId,
allow_site: allowSite
}).sort({ updatedAt: -1 });
return res.send(config || null);
} catch (err) {
return res.status(500).send({
message: err.message || "Failed to fetch external user permissions."
});
}
};
exports.getExternalUserPermissionsList = async (req, res) => {
try {
const requestedSite = Number(req.query.site || req.query.allow_site);
const allowSite = Number.isInteger(requestedSite) && requestedSite > 0 ? requestedSite : splitSite.findSiteNumber(req);
const records = await ExtUserPermission.find({ allow_site: allowSite }).sort({ username: 1, updatedAt: -1 });
return res.send(records);
} catch (err) {
return res.status(500).send({
message: err.message || "Failed to fetch external user permissions list."
});
}
};
exports.saveExternalUserPermission = async (req, res) => {
try {
const externalUserId = `${req.body.external_user_id || req.body.externalUserId || ""}`.trim();
const allowSite = Number(req.body.allow_site || req.body.allowSite);
const username = req.body.username || "";
const name = req.body.name || "";
const email = req.body.email || "";
if (!externalUserId) {
return res.status(400).send({ message: "external_user_id is required." });
}
if (!Number.isInteger(allowSite) || allowSite <= 0) {
return res.status(400).send({ message: "allow_site must be a positive number." });
}
const rawPermissions = Array.isArray(req.body.permissions) ? req.body.permissions : [];
const permissions = Array.from(new Set([SYSTEM_ACCESS_PERMISSION, ...rawPermissions.filter(Boolean)]));
const editorId = req.userId ? `${req.userId}` : "";
const existing = await ExtUserPermission.findOne({
external_user_id: externalUserId,
allow_site: allowSite
});
const payload = {
external_user_id: externalUserId,
allow_site: allowSite,
username,
name,
email,
permissions,
edit_by: editorId
};
if (!existing) {
payload.create_by = editorId;
}
const saved = await ExtUserPermission.findOneAndUpdate(
{
external_user_id: externalUserId,
allow_site: allowSite
},
payload,
{
upsert: true,
new: true,
runValidators: true,
setDefaultsOnInsert: true
}
);
return res.send(saved);
} catch (err) {
return res.status(500).send({
message: err.message || "Failed to save external user permissions."
});
}
};
exports.revokeExternalUserPermission = async (req, res) => {
try {
const externalUserId = `${req.query.external_user_id || req.query.externalUserId || req.body.external_user_id || ""}`.trim();
const requestedSite = Number(req.query.site || req.query.allow_site || req.body.allow_site);
const allowSite = Number.isInteger(requestedSite) && requestedSite > 0 ? requestedSite : splitSite.findSiteNumber(req);
if (!externalUserId) {
return res.status(400).send({ message: "external_user_id is required." });
}
await ExtUserPermission.deleteOne({
external_user_id: externalUserId,
allow_site: allowSite
});
return res.send({ success: true });
} catch (err) {
return res.status(500).send({
message: err.message || "Failed to revoke external user permissions."
});
}
};

View File

@@ -13,6 +13,7 @@ verifyToken = (req, res, next) => {
return res.status(401).send({ message: "Unauthorized!" });
}
req.userId = decoded.id;
req.authPayload = decoded;
next();
});
};

View File

@@ -0,0 +1,36 @@
module.exports = (mongoose) => {
const schema = mongoose.Schema(
{
external_user_id: {
type: String,
required: true
},
allow_site: {
type: Number,
required: true
},
username: String,
name: String,
email: String,
permissions: [
{
type: String
}
],
create_by: String,
edit_by: String
},
{ collection: "ext_usr_perm", timestamps: true }
);
schema.method("toJSON", function () {
const { __v, _id, ...object } = this.toObject();
object.id = _id;
return object;
});
schema.index({ external_user_id: 1, allow_site: 1 }, { unique: true });
const ExtUserPermission = mongoose.model("ext_usr_perm", schema);
return ExtUserPermission;
};

View File

@@ -36,4 +36,5 @@ db.carousel = require("./carousel.model")(mongoose);
db.fingerprint_attendance = require("./fingerprint-attendance.model")(mongoose);
db.dailyRoutesTemplate = require("./daily-routes-template.model")(mongoose);
db.msg_custom_template = require("./msg-custom-template.model")(mongoose);
db.ext_usr_perm = require("./ext-usr-perm.model")(mongoose);
module.exports = db;

View File

@@ -11,6 +11,16 @@ module.exports = app => {
var router = require("express").Router();
// Retrieve all employee
router.get("/", employees.getAllEmployees);
// Retrieve active employee list from HR integration service
router.post("/external/list", [authJwt.verifyToken], employees.getExternalEmployeesList);
// Get external user permission config for a site
router.get("/external/permissions", [authJwt.verifyToken], employees.getExternalUserPermission);
// List external user permission configs for a site
router.get("/external/permissions-list", [authJwt.verifyToken], employees.getExternalUserPermissionsList);
// Save external user permission config for a site
router.post("/external/permissions", [authJwt.verifyToken], employees.saveExternalUserPermission);
// Revoke external user permission config for a site
router.delete("/external/permissions", [authJwt.verifyToken], employees.revokeExternalUserPermission);
// Retrive employees across sites
router.get("/all-sites", employees.getAllEmployeesAcrossSites);
// Create a new employee