Compare commits

...

20 Commits

Author SHA1 Message Date
a6c2e7fc7e add: role db 2025-10-08 11:37:40 +07:00
45968832f0 add: device.db 2025-10-08 11:37:33 +07:00
cf37732ebe add: brand.db.js 2025-10-08 11:37:19 +07:00
20b70edaa6 add: user validation 2025-10-08 11:36:56 +07:00
b16e65463d add: is_approve null, validate is_approve login 2025-10-08 11:36:39 +07:00
58cb0c8425 update: user service 2025-10-08 11:35:50 +07:00
0ae39aa504 update: user controller 2025-10-08 11:35:38 +07:00
9ad16fcff7 fix: user route 2025-10-08 11:35:15 +07:00
453b5eb5af update: userdb 2025-10-08 11:34:27 +07:00
76c5eef2f7 fix: req.user, delete verifyrefreshtoken 2025-10-08 11:30:48 +07:00
f9d3bd913f fix: is_approve validations 2025-10-07 23:12:49 +07:00
4b60f922ee fix: auth 2025-10-07 15:45:14 +07:00
a4ef76e74e add: is approve validation 2025-10-07 15:44:48 +07:00
33e70721d9 update verifyacces 2025-10-07 15:15:00 +07:00
8fca2d3cd2 fix: update device 2025-10-07 15:14:44 +07:00
a632791a4d delete 2025-10-07 15:12:54 +07:00
2eec70b7e3 add verify role 2025-10-07 15:12:49 +07:00
ba7f746433 update: device db 2025-10-07 15:12:40 +07:00
ddf9784213 update: auth 2025-10-07 15:12:27 +07:00
48cb3af91d add verifyAccess 2025-10-07 13:48:31 +07:00
16 changed files with 637 additions and 498 deletions

View File

@@ -26,16 +26,23 @@ class AuthController {
const { user, tokens } = await AuthService.register(value); const { user, tokens } = await AuthService.register(value);
// Set refresh token in cookie // Set refresh token di cookie
res.cookie('refreshToken', tokens.refreshToken, { res.cookie('refreshToken', tokens.refreshToken, {
httpOnly: true, httpOnly: true,
secure: false, //masih dev secure: false, // masih dev
sameSite: 'lax', sameSite: 'lax',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 hari maxAge: 7 * 24 * 60 * 60 * 1000 // 7 hari
}); });
return res.status(201).json( return res.status(201).json(
setResponse({ user, accessToken: tokens.accessToken }, 'User registered successfully', 201) setResponse(
{
user: { ...user, approved: false },
accessToken: tokens.accessToken
},
'User registered successfully. Waiting for admin approval.',
201
)
); );
} catch (err) { } catch (err) {
return res.status(err.statusCode || 500).json( return res.status(err.statusCode || 500).json(
@@ -68,7 +75,7 @@ class AuthController {
const { user, tokens } = await AuthService.login({ email, password }); const { user, tokens } = await AuthService.login({ email, password });
// Set refresh token in cookie // Set refresh token di cookie
res.cookie('refreshToken', tokens.refreshToken, { res.cookie('refreshToken', tokens.refreshToken, {
httpOnly: true, httpOnly: true,
secure: false, // masih dev secure: false, // masih dev
@@ -77,7 +84,14 @@ class AuthController {
}); });
return res.status(200).json( return res.status(200).json(
setResponse({ user, accessToken: tokens.accessToken }, 'Login successful', 200) setResponse(
{
user: { ...user, approved: true },
accessToken: tokens.accessToken
},
'Login successful',
200
)
); );
} catch (err) { } catch (err) {
return res.status(err.statusCode || 500).json( return res.status(err.statusCode || 500).json(

View File

@@ -1,172 +1,157 @@
const userService = require("../services/user.service"); const userService = require("../services/user.service");
const { ErrorHandler } = require("../helpers/error"); const { ErrorHandler } = require("../helpers/error");
const { hashPassword } = require("../helpers/hashPassword"); const { setResponse } = require("../helpers/utils");
const { setResponse, setPaging, setResponsePaging } = require("../helpers/utils");
const Joi = require("joi"); const Joi = require("joi");
const { userSchema } = require("../helpers/validation");
// Definisikan skema validasi class UserController {
const validateTerm = Joi.object({ // Get all users
user_fullname: Joi.string().max(255).required(), static async getAllUsers(req, res) {
user_name: Joi.string().max(255).required(), try {
user_email: Joi.string().max(255).email().allow(null), const users = await userService.getAllUsers();
user_password: Joi.string().max(255).required(), return res.status(200).json(setResponse(users, "Users retrieved successfully", 200));
role_id: Joi.number().integer().allow(null), } catch (error) {
is_active: Joi.boolean().required() return res
}); .status(error.statusCode || 500)
.json(setResponse(null, error.message, error.statusCode || 500));
}
}
const getAllUsers = async (req, res) => { // Get user by ID
static async getUserById(req, res) {
try {
const { id } = req.params;
const user = await userService.getUserById(id);
return res.status(200).json(setResponse(user, "User retrieved successfully", 200));
} catch (error) {
return res
.status(error.statusCode || 500)
.json(setResponse(null, error.message, error.statusCode || 500));
}
}
// Create new user
static async createUser(req, res) {
try {
const { error, value } = userSchema.validate(req.body, { abortEarly: false });
if (error) {
const validationErrors = error.details.map((err) => err.message);
throw new ErrorHandler(400, validationErrors);
}
// Kirim approved_by dari user yang bikin
const result = await userService.createUser({
...value,
approved_by: req.user.user_id
});
return res.status(201).json(setResponse(result, "User created successfully", 201));
} catch (error) {
return res
.status(error.statusCode || 500)
.json(setResponse(null, error.message, error.statusCode || 500));
}
}
// Update user
static async updateUser(req, res) {
try {
const { id } = req.params;
const { const {
page = 1, fullname,
limit = 10, name,
fullname: userFullname, email,
username: userName, phone,
is_active: isActive, role_id,
criteria, is_sa,
tenantID, is_active,
} = req.query is_approve
} = req.body;
const offset = (page - 1) * limit; const result = await userService.updateUser({
user_id: parseInt(id, 10),
const filterQuery = { fullname,
fixed: { name,
limit, offset, tenantID email,
}, phone,
filterQuery: [ role_id,
{ is_sa,
type: 'string', is_active,
column: 'user_fullname', is_approve
param: userFullname
},
{
type: 'string',
column: 'user_name',
param: userName
},
{
type: 'number',
column: 'is_active',
param: isActive
}
],
filterCriteria:
{
criteria,
column: [
'user_fullname', 'user_name'
]
}
}
const results = await userService.getAllUsers(filterQuery)
const response = await setResponsePaging(results.data, results.total, parseInt(limit), parseInt(page))
res.status(response.statusCode).json(response)
};
const getAllStatusUsers = async (req, res) => {
const results = await userService.getAllStatusUsers();
const response = await setResponse(results)
res.status(response.statusCode).json(response);
};
const createUser = async (req, res) => {
// Lakukan validasi
const { error } = validateTerm.validate(req.body, { stripUnknown: true });
if (error) {
const response = await setResponse([], error.details[0].message, 400)
return res.status(response.statusCode).json(response);
}
const results = await userService.createUser({
userFullname: req.body.user_fullname,
userName: req.body.user_name,
userEmail: req.body.user_email,
userPassword: req.body.user_password,
roleId: req.body.role_id,
isActive: req.body.is_active, // default 1 jika tidak dikirim
userID: req.body.userID,
tenantID: req.body.tenantID
}); });
const response = await setResponse(results); console.log("PARAM ID:", req.params);
console.log("BODY:", req.body);
res.status(response.statusCode).json(response); return res.status(200).json(setResponse(result, "User updated successfully", 200));
}; } catch (error) {
return res
const getUserById = async (req, res) => { .status(error.statusCode || 500)
const { id } = req.params; .json(setResponse(null, error.message, error.statusCode || 500));
}
const results = await userService.getUserById(id);
const response = await setResponse(results)
res.status(response.statusCode).json(response);
};
const getUserProfile = async (req, res) => {
const { id } = req.user;
const results = await userService.getUserById(id);
const response = await setResponse(results)
res.status(response.statusCode).json(response);
};
const updateUser = async (req, res) => {
const { id } = req.params;
// Lakukan validasi
const { error } = validateTerm.validate(req.body, { stripUnknown: true });
if (error) {
const response = await setResponse([], error.details[0].message, 400)
return res.status(response.statusCode).json(response);
} }
const results = await userService.updateUser({ // Delete user
userFullname: req.body.user_fullname, static async deleteUser(req, res) {
userName: req.body.user_name, try {
userEmail: req.body.user_email,
userPassword: req.body.user_password,
roleId: req.body.role_id,
isActive: req.body.is_active, // default 1 jika tidak dikirim
userID: req.body.userID,
tenantID: req.body.tenantID,
id
});
const response = await setResponse(results);
res.status(response.statusCode).json(response);
};
const deleteUser = async (req, res) => {
const { id } = req.params; const { id } = req.params;
const userID = req.userID const deletedBy = req.user?.user_id;
const results = await userService.deleteUser(id, userID); const result = await userService.deleteUser(id, deletedBy);
const response = await setResponse(results) return res.status(200).json(setResponse(result, "User deleted successfully", 200));
} catch (error) {
return res
.status(error.statusCode || 500)
.json(setResponse(null, error.message, error.statusCode || 500));
}
}
res.status(response.statusCode).json(response); // Change user password
}; static async changePassword(req, res) {
try {
const { id } = req.params;
const { new_password } = req.body;
const getAllRoles = async (req, res) => { if (!id || !new_password) {
const results = await userService.getAllRoles(req.body.tenantID); throw new ErrorHandler(400, "user_id and new_password are required");
const response = await setResponse(results) }
res.status(response.statusCode).json(response); const result = await userService.changeUserPassword(user_id, new_password);
}; return res.status(200).json(setResponse(result, "Password changed successfully", 200));
} catch (error) {
return res
.status(error.statusCode || 500)
.json(setResponse(null, error.message, error.statusCode || 500));
}
}
module.exports = { // Get all status users
getAllUsers, static async getAllStatusUsers(req, res) {
createUser, try {
getUserById, const result = await userService.getAllStatusUsers();
updateUser, return res.status(200).json(setResponse(result, "Status list retrieved successfully", 200));
deleteUser, } catch (error) {
getUserProfile, return res
getAllRoles, .status(error.statusCode || 500)
getAllStatusUsers .json(setResponse(null, error.message, error.statusCode || 500));
}; }
}
// Approve user
static async approveUser(req, res) {
try {
const { id } = req.params;
const approverId = req.user?.user_id || null;
const result = await userService.approveUser(id, approverId);
return res.status(200).json(setResponse(result, "User approved successfully", 200));
} catch (error) {
return res
.status(error.statusCode || 500)
.json(setResponse(null, error.message, error.statusCode || 500));
}
}
}
module.exports = UserController;

View File

@@ -1,24 +1,46 @@
const pool = require("../config"); const pool = require("../config");
// Get all brands // Get all brands
const getAllBrandsDb = async () => { const getAllBrandsDb = async (filters = {}) => {
const { whereConditions, queryParams } = pool.buildFilterQuery([
{ column: "b.brand_name", param: filters.brand_name, type: "string" },
{ column: "b.created_by", param: filters.created_by, type: "number" },
]);
const whereClause = whereConditions.length ? `AND ${whereConditions.join(" AND ")}` : "";
const queryText = ` const queryText = `
SELECT * SELECT
FROM m_brands b.brand_id,
WHERE deleted_at IS NULL b.brand_name,
ORDER BY brand_id ASC b.created_at,
b.updated_at,
b.deleted_at,
b.created_by,
b.updated_by,
b.deleted_by
FROM m_brands b
WHERE b.deleted_at IS NULL ${whereClause}
ORDER BY b.brand_id ASC
`; `;
const result = await pool.query(queryText); const result = await pool.query(queryText, queryParams);
return result.recordset; return result.recordset;
}; };
// Get brand by ID // Get brand by ID
const getBrandByIdDb = async (id) => { const getBrandByIdDb = async (id) => {
const queryText = ` const queryText = `
SELECT * SELECT
brand_id,
brand_name,
created_at,
updated_at,
deleted_at,
created_by,
updated_by,
deleted_by
FROM m_brands FROM m_brands
WHERE brand_id = $1 WHERE brand_id = $1 AND deleted_at IS NULL
AND deleted_at IS NULL
`; `;
const result = await pool.query(queryText, [id]); const result = await pool.query(queryText, [id]);
return result.recordset[0]; return result.recordset[0];
@@ -27,10 +49,17 @@ const getBrandByIdDb = async (id) => {
// Get brand by name // Get brand by name
const getBrandByNameDb = async (name) => { const getBrandByNameDb = async (name) => {
const queryText = ` const queryText = `
SELECT * SELECT
brand_id,
brand_name,
created_at,
updated_at,
deleted_at,
created_by,
updated_by,
deleted_by
FROM m_brands FROM m_brands
WHERE brand_name = $1 WHERE brand_name = $1 AND deleted_at IS NULL
AND deleted_at IS NULL
`; `;
const result = await pool.query(queryText, [name]); const result = await pool.query(queryText, [name]);
return result.recordset[0]; return result.recordset[0];
@@ -38,18 +67,24 @@ const getBrandByNameDb = async (name) => {
// Create brand // Create brand
const createBrandDb = async (data) => { const createBrandDb = async (data) => {
const { query: queryText, values } = pool.buildDynamicInsert("m_brands", data); const { query, values } = pool.buildDynamicInsert("m_brands", {
const result = await pool.query(queryText, values); ...data,
created_at: new Date(),
});
const result = await pool.query(query, values);
const insertedId = result.recordset[0]?.inserted_id; const insertedId = result.recordset[0]?.inserted_id;
if (!insertedId) return null; if (!insertedId) return null;
return getBrandByIdDb(insertedId); return getBrandByIdDb(insertedId);
}; };
// Update brand // Update brand
const updateBrandDb = async (id, data) => { const updateBrandDb = async (id, data) => {
const { query: queryText, values } = pool.buildDynamicUpdate("m_brands", data, { brand_id: id }); const { query, values } = pool.buildDynamicUpdate(
await pool.query(queryText, values); "m_brands",
{ ...data, updated_at: new Date() },
{ brand_id: id }
);
await pool.query(query, values);
return getBrandByIdDb(id); return getBrandByIdDb(id);
}; };
@@ -59,8 +94,7 @@ const softDeleteBrandDb = async (id, deletedBy) => {
UPDATE m_brands UPDATE m_brands
SET deleted_at = GETDATE(), SET deleted_at = GETDATE(),
deleted_by = $1 deleted_by = $1
WHERE brand_id = $2 WHERE brand_id = $2 AND deleted_at IS NULL
AND deleted_at IS NULL
`; `;
await pool.query(queryText, [deletedBy, id]); await pool.query(queryText, [deletedBy, id]);
return true; return true;

View File

@@ -1,85 +1,69 @@
const pool = require("../config"); const pool = require("../config");
// Get all devices // Get all devices
const getAllDevicesDb = async () => { const getAllDevicesDb = async (searchParams = {}) => {
const { whereConditions, queryParams } = pool.buildFilterQuery([
{ column: "d.device_name", param: searchParams.name, type: "string" },
{ column: "d.device_code", param: searchParams.code, type: "string" },
{ column: "d.device_location", param: searchParams.location, type: "string" },
{ column: "b.brand_name", param: searchParams.brand, type: "string" },
]);
const whereClause = whereConditions.length
? `AND ${whereConditions.join(" AND ")}`
: "";
const queryText = ` const queryText = `
SELECT * SELECT d.*, b.brand_name
FROM m_device FROM m_device d
WHERE deleted_at IS NULL LEFT JOIN m_brands b ON d.brand_id = b.brand_id
ORDER BY device_id ASC WHERE d.deleted_at IS NULL ${whereClause}
ORDER BY d.device_id ASC
`; `;
const result = await pool.query(queryText); const result = await pool.query(queryText, queryParams);
return result.recordset; return result.recordset;
}; };
// Search devices by keyword
const searchDevicesDb = async (keyword) => {
const queryText = `
SELECT *
FROM m_device
WHERE deleted_at IS NULL
AND (
device_name LIKE '%' + $1 + '%'
OR device_code LIKE '%' + $1 + '%'
OR device_location LIKE '%' + $1 + '%'
OR ip_address LIKE '%' + $1 + '%'
OR device_description LIKE '%' + $1 + '%'
)
ORDER BY device_id ASC
`;
const result = await pool.query(queryText, [keyword]);
return result.recordset;
};
// Get device by ID
const getDeviceByIdDb = async (id) => { const getDeviceByIdDb = async (id) => {
const queryText = ` const queryText = `
SELECT * SELECT d.*, b.brand_name
FROM m_device FROM m_device d
WHERE device_id = $1 LEFT JOIN m_brands b ON d.brand_id = b.brand_id
AND deleted_at IS NULL WHERE d.device_id = $1 AND d.deleted_at IS NULL
`; `;
const result = await pool.query(queryText, [id]); const result = await pool.query(queryText, [id]);
return result.recordset[0]; return result.recordset[0];
}; };
// Get device by device_code
const getDeviceByCodeDb = async (code) => { const getDeviceByCodeDb = async (code) => {
const queryText = ` const queryText = `
SELECT * SELECT d.*, b.brand_name
FROM m_device FROM m_device d
WHERE device_code = $1 LEFT JOIN m_brands b ON d.brand_id = b.brand_id
AND deleted_at IS NULL WHERE d.device_code = $1 AND d.deleted_at IS NULL
`; `;
const result = await pool.query(queryText, [code]); const result = await pool.query(queryText, [code]);
return result.recordset[0]; return result.recordset[0];
}; };
// Create device
const createDeviceDb = async (data) => { const createDeviceDb = async (data) => {
const { query: queryText, values } = pool.buildDynamicInsert("m_device", data); const { query: queryText, values } = pool.buildDynamicInsert("m_device", data);
const result = await pool.query(queryText, values); const result = await pool.query(queryText, values);
const insertedId = result.recordset[0]?.inserted_id; const insertedId = result.recordset[0]?.inserted_id;
if (!insertedId) return null; return insertedId ? await getDeviceByIdDb(insertedId) : null;
return getDeviceByIdDb(insertedId);
}; };
// Update device
const updateDeviceDb = async (id, data) => { const updateDeviceDb = async (id, data) => {
const { query: queryText, values } = pool.buildDynamicUpdate("m_device", data, { device_id: id }); const { query: queryText, values } = pool.buildDynamicUpdate("m_device", data, { device_id: id });
await pool.query(queryText, values); await pool.query(`${queryText} AND deleted_at IS NULL`, values);
return getDeviceByIdDb(id); return getDeviceByIdDb(id);
}; };
// Soft delete device
const softDeleteDeviceDb = async (id, deletedBy) => { const softDeleteDeviceDb = async (id, deletedBy) => {
const queryText = ` const queryText = `
UPDATE m_device UPDATE m_device
SET deleted_at = GETDATE(), SET deleted_at = GETDATE(), deleted_by = $1
deleted_by = $1 WHERE device_id = $2 AND deleted_at IS NULL
WHERE device_id = $2
AND deleted_at IS NULL
`; `;
await pool.query(queryText, [deletedBy, id]); await pool.query(queryText, [deletedBy, id]);
return true; return true;
@@ -92,5 +76,4 @@ module.exports = {
createDeviceDb, createDeviceDb,
updateDeviceDb, updateDeviceDb,
softDeleteDeviceDb, softDeleteDeviceDb,
searchDevicesDb,
}; };

View File

@@ -1,55 +1,87 @@
const pool = require("../config"); const pool = require("../config");
// Get all roles // Get all roles
const getAllRolesDb = async () => { const getAllRolesDb = async (filters = {}) => {
const { whereConditions, queryParams } = pool.buildFilterQuery([
{ column: "r.role_name", param: filters.role_name, type: "string" },
{ column: "r.role_level", param: filters.role_level, type: "number" },
]);
const whereClause = whereConditions.length ? `AND ${whereConditions.join(" AND ")}` : "";
const queryText = ` const queryText = `
SELECT * SELECT
FROM m_roles r.role_id,
WHERE deleted_at IS NULL r.role_name,
ORDER BY role_id ASC r.role_description,
r.role_level,
r.created_at,
r.updated_at,
r.updated_by,
r.deleted_at,
r.deleted_by
FROM m_roles r
WHERE r.deleted_at IS NULL ${whereClause}
ORDER BY r.role_id ASC
`; `;
const result = await pool.query(queryText);
const result = await pool.query(queryText, queryParams);
return result.recordset; return result.recordset;
}; };
// Get role by ID // Get role by ID
const getRoleByIdDb = async (roleId) => { const getRoleByIdDb = async (id) => {
const queryText = ` const queryText = `
SELECT * SELECT
role_id,
role_name,
role_description,
role_level,
created_at,
updated_at,
updated_by,
deleted_at,
deleted_by
FROM m_roles FROM m_roles
WHERE role_id = $1 AND deleted_at IS NULL WHERE role_id = $1 AND deleted_at IS NULL
`; `;
const result = await pool.query(queryText, [roleId]); const result = await pool.query(queryText, [id]);
return result.recordset[0]; return result.recordset[0];
}; };
// Create role // Create role
const createRoleDb = async (data) => { const createRoleDb = async (data) => {
const { query: queryText, values } = pool.buildDynamicInsert("m_roles", data); const { query, values } = pool.buildDynamicInsert("m_roles", {
const result = await pool.query(queryText, values); ...data,
created_at: new Date(),
});
const result = await pool.query(query, values);
return result.recordset[0]?.inserted_id || null; return result.recordset[0]?.inserted_id || null;
}; };
// Update role // Update role
const updateRoleDb = async (roleId, data) => { const updateRoleDb = async (id, data) => {
const { query: queryText, values } = pool.buildDynamicUpdate("m_roles", data, { role_id: roleId }); const { query, values } = pool.buildDynamicUpdate(
await pool.query(queryText, values); "m_roles",
{ ...data, updated_at: new Date() },
{ role_id: id }
);
await pool.query(query, values);
return true; return true;
}; };
// Soft delete role // Soft delete role
const deleteRoleDb = async (roleId, deletedBy) => { const deleteRoleDb = async (id, deletedBy) => {
const queryText = ` const queryText = `
UPDATE m_roles UPDATE m_roles
SET deleted_at = GETDATE(), SET deleted_at = GETDATE(),
deleted_by = $1 deleted_by = $1
WHERE role_id = $2 WHERE role_id = $2
`; `;
await pool.query(queryText, [deletedBy, roleId]); await pool.query(queryText, [deletedBy, id]);
return true; return true;
}; };
module.exports = { module.exports = {
getAllRolesDb, getAllRolesDb,
getRoleByIdDb, getRoleByIdDb,

View File

@@ -1,36 +1,33 @@
const pool = require("../config"); const { query, buildFilterQuery, buildDynamicUpdate } = require("../config");
// Get all users
const getAllUsersDb = async (searchParams = {}) => {
const { whereConditions, queryParams } = buildFilterQuery([
{ column: "u.user_fullname", param: searchParams.fullname, type: "string" },
{ column: "u.user_name", param: searchParams.username, type: "string" },
{ column: "u.user_email", param: searchParams.email, type: "string" },
{ column: "r.role_name", param: searchParams.role, type: "string" },
]);
const whereClause = whereConditions.length
? `AND ${whereConditions.join(" AND ")}`
: "";
// Get all users s
const getAllUsersDb = async () => {
const queryText = ` const queryText = `
SELECT SELECT
u.user_id, u.user_id, u.user_fullname, u.user_name, u.user_email, u.user_phone,
u.user_fullname, u.is_active, u.is_sa, u.is_approve, u.approved_by,
u.user_name,
u.user_email,
u.user_phone,
u.is_active,
u.is_sa,
u.is_approve,
u.approved_by,
approver.user_fullname AS approved_by_name, approver.user_fullname AS approved_by_name,
u.approved_at, u.approved_at, u.created_at, u.updated_at, u.deleted_at,
u.created_at, u.updated_by, u.deleted_by,
u.updated_at, r.role_id, r.role_name, r.role_description, r.role_level
u.deleted_at,
u.updated_by,
u.deleted_by,
r.role_id,
r.role_name,
r.role_description,
r.role_level
FROM m_users u FROM m_users u
LEFT JOIN m_roles r ON u.role_id = r.role_id LEFT JOIN m_roles r ON u.role_id = r.role_id
LEFT JOIN m_users approver ON u.approved_by = approver.user_id LEFT JOIN m_users approver ON u.approved_by = approver.user_id
WHERE u.deleted_at IS NULL WHERE u.deleted_at IS NULL ${whereClause}
ORDER BY u.user_id ASC ORDER BY u.user_id ASC
`; `;
const result = await pool.query(queryText); const result = await query(queryText, queryParams);
return result.recordset; return result.recordset;
}; };
@@ -38,32 +35,18 @@ const getAllUsersDb = async () => {
const getUserByIdDb = async (id) => { const getUserByIdDb = async (id) => {
const queryText = ` const queryText = `
SELECT SELECT
u.user_id, u.user_id, u.user_fullname, u.user_name, u.user_email, u.user_phone,
u.user_fullname, u.is_active, u.is_sa, u.is_approve, u.approved_by,
u.user_name,
u.user_email,
u.user_phone,
u.is_active,
u.is_sa,
u.is_approve,
u.approved_by,
approver.user_fullname AS approved_by_name, approver.user_fullname AS approved_by_name,
u.approved_at, u.approved_at, u.created_at, u.updated_at, u.deleted_at,
u.created_at, u.updated_by, u.deleted_by,
u.updated_at, r.role_id, r.role_name, r.role_description, r.role_level
u.deleted_at,
u.updated_by,
u.deleted_by,
r.role_id,
r.role_name,
r.role_description,
r.role_level
FROM m_users u FROM m_users u
LEFT JOIN m_roles r ON u.role_id = r.role_id LEFT JOIN m_roles r ON u.role_id = r.role_id
LEFT JOIN m_users approver ON u.approved_by = approver.user_id LEFT JOIN m_users approver ON u.approved_by = approver.user_id
WHERE u.user_id = $1 AND u.deleted_at IS NULL WHERE u.user_id = $1 AND u.deleted_at IS NULL
`; `;
const result = await pool.query(queryText, [id]); const result = await query(queryText, [id]);
return result.recordset[0]; return result.recordset[0];
}; };
@@ -71,24 +54,14 @@ const getUserByIdDb = async (id) => {
const getUserByUserEmailDb = async (email) => { const getUserByUserEmailDb = async (email) => {
const queryText = ` const queryText = `
SELECT SELECT
u.user_id, u.user_id, u.user_fullname, u.user_name, u.user_email, u.user_phone,
u.user_fullname, u.user_password, u.is_active, u.is_sa, u.is_approve, u.role_id,
u.user_name, r.role_name, r.role_description, r.role_level
u.user_email,
u.user_phone,
u.user_password,
u.is_active,
u.is_sa,
u.is_approve,
u.role_id,
r.role_name,
r.role_description,
r.role_level
FROM m_users u FROM m_users u
LEFT JOIN m_roles r ON u.role_id = r.role_id LEFT JOIN m_roles r ON u.role_id = r.role_id
WHERE u.user_email = $1 AND u.deleted_at IS NULL WHERE u.user_email = $1 AND u.deleted_at IS NULL
`; `;
const result = await pool.query(queryText, [email]); const result = await query(queryText, [email]);
return result.recordset[0]; return result.recordset[0];
}; };
@@ -96,64 +69,61 @@ const getUserByUserEmailDb = async (email) => {
const getUserByUsernameDb = async (username) => { const getUserByUsernameDb = async (username) => {
const queryText = ` const queryText = `
SELECT SELECT
u.user_id, u.user_id, u.user_fullname, u.user_name, u.user_email, u.user_phone,
u.user_fullname, u.user_password, u.is_active, u.is_sa, u.is_approve, u.role_id,
u.user_name, r.role_name, r.role_description, r.role_level
u.user_email,
u.user_phone,
u.user_password,
u.is_active,
u.is_sa,
u.is_approve,
u.role_id,
r.role_name,
r.role_description,
r.role_level
FROM m_users u FROM m_users u
LEFT JOIN m_roles r ON u.role_id = r.role_id LEFT JOIN m_roles r ON u.role_id = r.role_id
WHERE u.user_name = $1 AND u.deleted_at IS NULL WHERE u.user_name = $1 AND u.deleted_at IS NULL
`; `;
const result = await pool.query(queryText, [username]); const result = await query(queryText, [username]);
return result.recordset[0]; return result.recordset[0];
}; };
// Create user // Create user
const createUserDb = async (data) => { const createUserDb = async (data) => {
const { query: queryText, values } = pool.buildDynamicInsert("m_users", data); const queryText = `
const result = await pool.query(queryText, values); INSERT INTO m_users
return result.recordset[0]?.inserted_id || null; (user_fullname, user_name, user_email, user_phone, user_password, role_id, is_sa, is_active, is_approve, approved_by, approved_at)
VALUES
($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11);
SELECT SCOPE_IDENTITY() as user_id;
`;
const values = [
data.user_fullname,
data.user_name,
data.user_email,
data.user_phone,
data.user_password,
data.role_id || null,
data.is_sa || 0,
data.is_active || 1,
data.is_approve || 0,
data.approved_by || null,
data.approved_at || null
];
const result = await query(queryText, values);
return result.recordset[0]?.user_id || null;
}; };
// Update user // Update user
const updateUserDb = async (userId, data) => { const updateUserDb = async (userId, data) => {
const { query: queryText, values } = pool.buildDynamicUpdate("m_users", data, { user_id: userId }); const { query: queryText, values } = buildDynamicUpdate("m_users", data, { user_id: userId });
await pool.query(queryText, values); const finalQuery = queryText.replace("WHERE", "WHERE deleted_at IS NULL AND");
await query(finalQuery, values);
return true; return true;
}; };
// Approve user // Change user password
const approveUserDb = async (userId, approverId) => {
const queryText = `
UPDATE m_users
SET is_approve = 1,
approved_by = $1,
approved_at = GETDATE(),
updated_at = GETDATE()
WHERE user_id = $2 AND deleted_at IS NULL
`;
await pool.query(queryText, [approverId, userId]);
return true;
};
// Change password
const changeUserPasswordDb = async (userId, newPassword) => { const changeUserPasswordDb = async (userId, newPassword) => {
const queryText = ` const queryText = `
UPDATE m_users UPDATE m_users
SET user_password = $1, SET user_password = $1, updated_at = GETDATE()
updated_at = GETDATE()
WHERE user_id = $2 AND deleted_at IS NULL WHERE user_id = $2 AND deleted_at IS NULL
`; `;
await pool.query(queryText, [newPassword, userId]); await query(queryText, [newPassword, userId]);
return true; return true;
}; };
@@ -161,11 +131,15 @@ const changeUserPasswordDb = async (userId, newPassword) => {
const deleteUserDb = async (userId, deletedBy) => { const deleteUserDb = async (userId, deletedBy) => {
const queryText = ` const queryText = `
UPDATE m_users UPDATE m_users
SET deleted_at = GETDATE(), SET
deleted_by = $1 deleted_at = GETDATE(),
deleted_by = $1,
is_active = 0
WHERE user_id = $2 WHERE user_id = $2
AND deleted_at IS NULL
`; `;
await pool.query(queryText, [deletedBy, userId]);
await query(queryText, [deletedBy, userId]);
return true; return true;
}; };
@@ -176,7 +150,6 @@ module.exports = {
getUserByUsernameDb, getUserByUsernameDb,
createUserDb, createUserDb,
updateUserDb, updateUserDb,
approveUserDb,
changeUserPasswordDb, changeUserPasswordDb,
deleteUserDb, deleteUserDb,
}; };

View File

@@ -63,9 +63,38 @@ const deviceUpdateSchema = Joi.object({
}) })
}).min(1); }).min(1);
// ========================
// Users Validation
// ========================
const userSchema = Joi.object({
fullname: Joi.string().min(3).max(100).required(),
name: Joi.string().alphanum().min(3).max(50).required(),
email: Joi.string().email().required(),
phone: Joi.string()
.pattern(/^(?:\+62|0)8\d{7,10}$/)
.required()
.messages({
'string.pattern.base':
'Phone number must be a valid Indonesian number in format +628XXXXXXXXX'
}),
password: Joi.string()
.min(8)
.pattern(/[A-Z]/, 'uppercase letter')
.pattern(/[a-z]/, 'lowercase letter')
.pattern(/\d/, 'number')
.pattern(/[!@#$%^&*(),.?":{}|<>]/, 'special character')
.required()
.messages({
'string.min': 'Password must be at least 8 characters long',
'string.pattern.name': 'Password must contain at least one {#name}'
}),
role_id : Joi.number().integer().min(1)
});
module.exports = { module.exports = {
registerSchema, registerSchema,
loginSchema, loginSchema,
deviceSchema, deviceSchema,
deviceUpdateSchema deviceUpdateSchema,
userSchema,
}; };

42
middleware/verifyAcces.js Normal file
View File

@@ -0,0 +1,42 @@
const { ErrorHandler } = require("../helpers/error");
const { getUserByIdDb } = require("../db/user.db");
const verifyAccess = (minLevel = 1, allowUnapprovedReadOnly = false) => {
return async (req, res, next) => {
try {
const user = req.user;
if (!user) throw new ErrorHandler(401, "Unauthorized: User not found");
// Super Admin bypass semua
if (user.is_sa) return next();
// Ambil user lengkap dari DB
const fullUser = await getUserByIdDb(user.user_id);
if (!fullUser) throw new ErrorHandler(403, "Forbidden: User not found");
// Jika belum di-approve
if (!fullUser.is_approve) {
// Hanya boleh GET (read-only)
if (req.method !== "GET") {
throw new ErrorHandler(403, "Account not approved — read-only access");
}
if (allowUnapprovedReadOnly) return next();
throw new ErrorHandler(403, "Account not approved");
}
// Cek role level
if (!fullUser.role_level || fullUser.role_level < minLevel) {
throw new ErrorHandler(403, "Forbidden: Insufficient role level");
}
next();
} catch (err) {
next(err);
}
};
};
module.exports = verifyAccess;

View File

@@ -0,0 +1,38 @@
const { ErrorHandler } = require("../helpers/error");
const { getUserByIdDb } = require("../db/user.db");
const verifyAccess = (minLevel = 1, allowUnapprovedReadOnly = false) => {
return async (req, res, next) => {
try {
const user = req.user;
if (!user) throw new ErrorHandler(401, "Unauthorized: User not found");
// Super Admin bypass semua
if (user.is_sa) return next();
const fullUser = await getUserByIdDb(user.user_id);
if (!fullUser) throw new ErrorHandler(403, "Forbidden: User not found");
if (!fullUser.is_approve) {
if (req.method !== "GET") {
throw new ErrorHandler(403, "Account not approved — read-only access");
}
if (allowUnapprovedReadOnly) return next();
throw new ErrorHandler(403, "Account not approved");
}
if (!fullUser.role_level || fullUser.role_level < minLevel) {
throw new ErrorHandler(403, "Forbidden: Insufficient role level");
}
next();
} catch (err) {
next(err);
}
};
};
module.exports = verifyAccess;

View File

@@ -1,28 +0,0 @@
const { ErrorHandler } = require("../helpers/error");
const verifyRole = (allowedRoles) => {
return (req, res, next) => {
try {
const user = req.user;
if (!user) {
throw new ErrorHandler(401, "Unauthorized: User not found");
}
// Super Admin bypass semua role
if (user.is_sa) {
return next();
}
if (!allowedRoles.includes(user.role_id)) {
throw new ErrorHandler(403, "Forbidden: Access denied");
}
next();
} catch (err) {
next(err);
}
};
};
module.exports = verifyRole;

View File

@@ -26,7 +26,9 @@ function verifyAccessToken(req, res, next) {
} }
const decoded = JWTService.verifyToken(token); const decoded = JWTService.verifyToken(token);
setUser(req, decoded);
req.user = decoded;
next(); next();
} catch (error) { } catch (error) {
if (error.name === 'TokenExpiredError') { if (error.name === 'TokenExpiredError') {
@@ -41,23 +43,6 @@ function verifyAccessToken(req, res, next) {
} }
} }
function verifyRefreshToken(req, res, next) {
try {
const refreshToken = req.cookies?.refreshToken;
if (!refreshToken) {
throw new ErrorHandler(401, 'Refresh Token is required');
}
const decoded = JWTService.verifyRefreshToken(refreshToken);
req.user = decoded;
next();
} catch (error) {
next(new ErrorHandler(401, 'Refresh token is invalid or expired'));
}
}
module.exports = { module.exports = {
verifyAccessToken, verifyAccessToken
verifyRefreshToken,
}; };

View File

@@ -1,14 +1,14 @@
const express = require('express'); const express = require('express');
const DeviceController = require('../controllers/device.controller'); const DeviceController = require('../controllers/device.controller');
const verifyToken = require("../middleware/verifyToken") const verifyToken = require("../middleware/verifyToken")
const verifyRole = require("../middleware/verifyRole") const verifyAccess = require("../middleware/verifyAcces")
const router = express.Router(); const router = express.Router();
router.get('/', verifyToken.verifyAccessToken, DeviceController.getAll); router.get('/', verifyToken.verifyAccessToken, DeviceController.getAll);
router.get('/:id', verifyToken.verifyAccessToken, DeviceController.getById); router.get('/:id', verifyToken.verifyAccessToken, DeviceController.getById);
router.post('/', verifyToken.verifyAccessToken, verifyRole([1]), DeviceController.create); router.post('/', verifyToken.verifyAccessToken, verifyAccess, DeviceController.create);
router.put('/:id', verifyToken.verifyAccessToken, verifyRole([1, 2]), DeviceController.update); router.put('/:id', verifyToken.verifyAccessToken, verifyAccess, DeviceController.update);
router.delete('/:id', verifyToken.verifyAccessToken, verifyRole([1]), DeviceController.delete); router.delete('/:id', verifyToken.verifyAccessToken, verifyAccess, DeviceController.delete);
module.exports = router; module.exports = router;

View File

@@ -1,32 +1,30 @@
const { const express = require('express');
getAllUsers, const UserController = require('../controllers/users.controller');
createUser, const verifyToken = require('../middleware/verifyToken');
deleteUser, const verifyAccess = require('../middleware/verifyAcces');
getUserById,
updateUser,
getUserProfile,
getAllRoles,
getAllStatusUsers
} = require("../controllers/users.controller");
const router = require("express").Router();
const verifyAdmin = require("../middleware/verifyRole");
const verifyToken = require("../middleware/verifyToken");
router.get("/roles", getAllRoles); const router = express.Router();
router.get('/profile', verifyToken.verifyAccessToken, getUserProfile); // Get all users
router.get('/', verifyToken.verifyAccessToken, UserController.getAllUsers);
router.route("/") // Get user by ID
.get(verifyToken.verifyAccessToken, getAllUsers) router.get('/:id', verifyToken.verifyAccessToken, UserController.getUserById);
.post(verifyToken.verifyAccessToken, createUser);
router // Create new user
.route("/status") router.post('/', verifyToken.verifyAccessToken, verifyAccess(), UserController.createUser);
.get(verifyToken.verifyAccessToken, getAllStatusUsers);
// Update user
router.put('/:id', verifyToken.verifyAccessToken, verifyAccess(), UserController.updateUser);
// Delete user
router.delete('/:id', verifyToken.verifyAccessToken, verifyAccess(), UserController.deleteUser);
// Change user password
router.put('/change-password/:id', verifyToken.verifyAccessToken, verifyAccess(), UserController.changePassword);
// Approve user
router.put('/:id/approve', verifyToken.verifyAccessToken, verifyAccess(), UserController.approveUser);
router.route("/:id")
.get(verifyToken.verifyAccessToken, getUserById)
.put(verifyToken.verifyAccessToken, updateUser)
.delete(verifyToken.verifyAccessToken, deleteUser);
module.exports = router; module.exports = router;

View File

@@ -23,9 +23,12 @@ class AuthService {
user_email: email, user_email: email,
user_phone: phone, user_phone: phone,
user_password: hashedPassword, user_password: hashedPassword,
role_id: 3, role_id: null,
is_sa: 0, is_sa: 0,
is_active: 1 is_active: 1,
is_approve: 0,
approved_by: null,
approved_at: null
}); });
const newUser = { const newUser = {
@@ -33,8 +36,7 @@ class AuthService {
user_fullname: fullname, user_fullname: fullname,
user_name: name, user_name: name,
user_email: email, user_email: email,
user_phone: phone, user_phone: phone
role_id: 3,
}; };
// generate token pair // generate token pair
@@ -59,6 +61,10 @@ class AuthService {
throw new ErrorHandler(403, 'User is inactive'); throw new ErrorHandler(403, 'User is inactive');
} }
if (!user.is_approve) {
throw new ErrorHandler(403, 'Your account has not been approved by admin yet.');
}
const payload = { const payload = {
user_id: user.user_id, user_id: user.user_id,
user_fullname: user.user_fullname, user_fullname: user.user_fullname,

View File

@@ -28,9 +28,7 @@ class DeviceService {
// Get device by code // Get device by code
static async getDeviceByCode(code) { static async getDeviceByCode(code) {
const device = await getDeviceByCodeDb(code); const device = await getDeviceByCodeDb(code);
if (!device) { if (!device) throw new ErrorHandler(404, 'Device not found');
throw new ErrorHandler(404, 'Device not found');
}
return device; return device;
} }
@@ -40,7 +38,7 @@ class DeviceService {
data.created_by = userId; data.created_by = userId;
// cek kode unik // Cek kode unik
const existingDevice = await getDeviceByCodeDb(data.device_code); const existingDevice = await getDeviceByCodeDb(data.device_code);
if (existingDevice) { if (existingDevice) {
throw new ErrorHandler(400, 'Device code already exists'); throw new ErrorHandler(400, 'Device code already exists');
@@ -63,8 +61,6 @@ class DeviceService {
const updatedDevice = await updateDeviceDb(id, data); const updatedDevice = await updateDeviceDb(id, data);
await updateDeviceDb(id, data);
return { return {
message: 'Device updated successfully', message: 'Device updated successfully',
data: updatedDevice, data: updatedDevice,

View File

@@ -1,122 +1,174 @@
const { const {
createUserDb, createUserDb,
changeUserPasswordDb,
getUserByIdDb, getUserByIdDb,
updateUserDb,
deleteUserDb,
getAllUsersDb, getAllUsersDb,
getUserByUsernameDb, getUserByUsernameDb,
getAllRoleDb updateUserDb,
} = require("../db/user.db"); deleteUserDb,
const { ErrorHandler } = require("../helpers/error"); changeUserPasswordDb
const { convertId } = require("../helpers/utils"); } = require('../db/user.db');
const { hashPassword } = require('../helpers/hashPassword');
const { ErrorHandler } = require('../helpers/error');
const statusName = [ const statusName = [
{ { status: true, status_name: "Aktif" },
status: true, { status: false, status_name: "NonAktif" }
status_name: "Aktif"
}, {
status: false,
status_name: "NonAktif"
}
]; ];
class UserService { class UserService {
// Get all status users
getAllStatusUsers = async () => { getAllStatusUsers = async () => {
try { try {
return statusName; return statusName;
} catch (error) { } catch (error) {
throw new ErrorHandler(error.statusCode, error.message); throw new ErrorHandler(error.statusCode || 500, error.message);
} }
}; };
getAllUsers = async (param) => { // Get all users
getAllUsers = async () => {
try { try {
const results = await getAllUsersDb(param); const results = await getAllUsersDb();
results.data.map(element => { results.forEach(user => {
element.is_active = element.is_active == 1 ? true : false user.is_active = user.is_active == 1;
element.is_active_name = convertId(statusName, element.is_active, 'status', 'status_name') user.is_active_name = statusName.find(s => s.status === user.is_active)?.status_name;
delete user.user_password; // remove password
}); });
return results return results;
} catch (error) { } catch (error) {
throw new ErrorHandler(error.statusCode, error.message); throw new ErrorHandler(error.statusCode || 500, error.message);
}
};
createUser = async (param) => {
try {
const userByUsername = await getUserByUsernameDb(param.userName, param.tenantID);
if (userByUsername) {
throw new ErrorHandler(401, "username taken already");
}
return await createUserDb(param);
} catch (error) {
throw new ErrorHandler(error.statusCode, error.message);
} }
}; };
// Get user by ID
getUserById = async (id) => { getUserById = async (id) => {
try { try {
const user = await getUserByIdDb(id); const user = await getUserByIdDb(id);
// user.password = undefined; if (!user) throw new ErrorHandler(404, "User not found");
user.is_active = user.is_active == 1 ? true : false
user.is_active = user.is_active == 1;
user.is_active_name = statusName.find(s => s.status === user.is_active)?.status_name;
delete user.user_password;
return user; return user;
} catch (error) { } catch (error) {
throw new ErrorHandler(error.statusCode, error.message); throw new ErrorHandler(error.statusCode || 500, error.message);
} }
}; };
changeUserPassword = async (password, email, tenantID) => { // Create users
createUser = async ({ fullname, name, email, phone, password, role_id = null, is_sa = 0, is_active = 1, approved_by }) => {
try { try {
return await changeUserPasswordDb(password, email, tenantID); const existingUser = await getUserByUsernameDb(name);
if (existingUser) throw new ErrorHandler(400, "Username already taken");
const hashedPassword = await hashPassword(password);
const userId = await createUserDb({
user_fullname: fullname,
user_name: name,
user_email: email,
user_phone: phone,
user_password: hashedPassword,
role_id,
is_sa,
is_active,
is_approve: 1,
approved_by,
approved_at: new Date()
});
return {
user_id: userId,
user_fullname: fullname,
user_name: name,
user_email: email,
user_phone: phone,
role_id,
is_sa,
is_active,
is_approve: 1,
approved_by
};
} catch (error) { } catch (error) {
throw new ErrorHandler(error.statusCode, error.message); throw new ErrorHandler(error.statusCode || 500, error.message);
} }
}; };
updateUser = async (param) => { // Update user
const { userName, id } = param; updateUser = async ({ user_id, fullname, name, email, phone, role_id, is_sa, is_active, is_approve }) => {
const errors = {};
try { try {
const user = await getUserByIdDb(user_id);
if (!user) throw new ErrorHandler(404, "User not found");
const user = await getUserByIdDb(id); // Cek username
if (name && user.user_name.toLowerCase() !== name.toLowerCase()) {
const findUserByUsername = await getUserByUsernameDb(userName, param.tenantID); const userByName = await getUserByUsernameDb(name);
if (userByName) throw new ErrorHandler(400, "Username already taken");
const usernameChanged = userName && user.user_name.toLowerCase() !== userName.toLowerCase();
if (usernameChanged && typeof findUserByUsername === "object") {
errors["username"] = "Username is already taken";
} }
if (Object.keys(errors).length > 0) { const updateData = {
throw new ErrorHandler(403, errors); ...(fullname && { user_fullname: fullname }),
} ...(name && { user_name: name }),
...(email && { user_email: email }),
...(phone && { user_phone: phone }),
...(role_id !== undefined && { role_id }),
...(is_sa !== undefined && { is_sa }),
...(is_active !== undefined && { is_active }),
...(is_approve !== undefined && { is_approve })
};
return await updateUserDb(param); await updateUserDb(user_id, updateData);
const updatedUser = await getUserByIdDb(user_id);
delete updatedUser.user_password;
updatedUser.is_active = updatedUser.is_active == 1;
updatedUser.is_active_name = statusName.find(s => s.status === updatedUser.is_active)?.status_name;
return updatedUser;
} catch (error) { } catch (error) {
throw new ErrorHandler(error.statusCode, error.message); throw new ErrorHandler(error.statusCode || 500, error.message);
} }
}; };
deleteUser = async (id, userID) => { // Approve user
approveUser = async (userId, approverId) => {
try { try {
return await deleteUserDb(id, userID); const updateData = {
is_approve: 1,
approved_by: approverId,
approved_at: new Date()
};
await updateUserDb(userId, updateData);
const updatedUser = await getUserByIdDb(userId);
delete updatedUser.user_password;
return updatedUser;
} catch (error) { } catch (error) {
throw new ErrorHandler(error.statusCode, error.message); throw new ErrorHandler(error.statusCode || 500, error.message);
} }
}; };
getAllRoles = async (tenantID) => { // Delete user (soft delete)
deleteUser = async (userId, deletedBy) => {
try { try {
return await getAllRoleDb(tenantID); await deleteUserDb(userId, deletedBy);
return { message: "User deleted successfully" };
} catch (error) { } catch (error) {
throw new ErrorHandler(error.statusCode, error.message); throw new ErrorHandler(error.statusCode || 500, error.message);
}
};
// Change password
changeUserPassword = async (userId, newPassword) => {
try {
const hashedPassword = await hashPassword(newPassword);
await changeUserPasswordDb(userId, hashedPassword);
return { message: "Password updated successfully" };
} catch (error) {
throw new ErrorHandler(error.statusCode || 500, error.message);
} }
}; };
} }