From ce646715513310ca69360a1e5846de2ba1280c8a Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 26 Sep 2025 10:58:56 +0700 Subject: [PATCH 001/126] hide package-lock --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ee0f965..51d619b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules .vscode request.http *.rest +package-lock.json \ No newline at end of file From 8142ceda30bcebaa576ddd64f324d1738a240301 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 26 Sep 2025 10:59:47 +0700 Subject: [PATCH 002/126] fix: auth controller --- controllers/auth.controller.js | 123 +++++++++++++++++++++++++++------ 1 file changed, 100 insertions(+), 23 deletions(-) diff --git a/controllers/auth.controller.js b/controllers/auth.controller.js index 464b3cd..e876dfd 100644 --- a/controllers/auth.controller.js +++ b/controllers/auth.controller.js @@ -1,26 +1,103 @@ -const authService = require("../services/auth.service"); +const AuthService = require('../services/auth.service'); +const { registerSchema, loginSchema } = require('../helpers/authValidation'); +const { setResponse } = require('../helpers/utils'); +const { createCaptcha } = require('../utils/captcha'); -const loginUser = async (req, res) => { - const { username, password, role, tenant } = req.body; - const { token, refreshToken, user } = await authService.login( - username, - password, - tenant - ); +class AuthController { - res.header("auth-token", token); - res.cookie("refreshToken", refreshToken, { - httpOnly: true, - sameSite: process.env.NODE_ENV === "development" ? true : "none", - secure: process.env.NODE_ENV === "development" ? false : true, - }); - res.status(200).json({ - token, - refreshToken, - user, - }); -}; + // Registration + static async register(req, res) { + try { + const { error, value } = registerSchema.validate(req.body, { abortEarly: false }); -module.exports = { - loginUser, -}; + if (error) { + // kumpulkan pesan error per field + const errors = error.details.reduce((acc, cur) => { + const field = Array.isArray(cur.path) ? cur.path.join('.') : String(cur.path); + if (!acc[field]) acc[field] = []; + acc[field].push(cur.message); + return acc; + }, {}); + + return res.status(400).json( + setResponse(errors, 'Validation failed', 400) + ); + } + + // Normalisasi phone menjadi +62 + if (value.phone && value.phone.startsWith('0')) { + value.phone = '+62' + value.phone.slice(1); + } + + const user = await AuthService.register(value); + return res.status(201).json( + setResponse(user, 'User registered successfully', 201) + ); + + } catch (err) { + return res.status(err.statusCode || 500).json( + setResponse([], err.message || 'Register failed', err.statusCode || 500) + ); + } + } + + + static async generateCaptcha(req, res) { + try { + const { svg, text } = createCaptcha(); + return res.status(200).json({ + data: { svg, text } + }); + } catch (err) { + return res.status(500).json(setResponse([], 'Captcha failed', 500)); + } + } + + static async login(req, res) { + try { + const { error, value } = loginSchema.validate(req.body, { abortEarly: false }); + if (error) return res.status(400).json(setResponse([], 'Validation failed', 400)); + + const { email, password, captcha, captchaText } = value; + + // verify captcha + if (!captcha || captcha.toLowerCase() !== captchaText.toLowerCase()) { + return res.status(400).json(setResponse([], 'Invalid captcha', 400)); + } + + const { user, tokens } = await AuthService.login({ email, password }); + + return res.status(200).json(setResponse({ user, tokens }, 'Login successful', 200)); + + } catch (err) { + return res.status(err.statusCode || 500).json( + setResponse([], err.message || 'Login failed', err.statusCode || 500) + ); + } + } + +// // Verify Captcha (secure) +// static async verifyCaptcha(req, res) { +// const { userInput } = req.body; + +// if (!userInput || !req.session.captcha) { +// return res.status(400).json( +// setResponse([], 'Missing data', 400) +// ); +// } + +// if (userInput.toLowerCase() === req.session.captcha.toLowerCase()) { +// req.session.captcha = null; // one-time use +// return res.json( +// setResponse([], 'Captcha is valid', 200) +// ); +// } else { +// return res.status(400).json( +// setResponse([], 'Invalid captcha', 400) +// ); +// } +// } + +} + +module.exports = AuthController; From a28c3c22d94945cf620e5e9d265fc585bf773f63 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 26 Sep 2025 11:03:35 +0700 Subject: [PATCH 003/126] update: user db --- db/user.db.js | 177 ++++++++++++++++++++++---------------------------- 1 file changed, 77 insertions(+), 100 deletions(-) diff --git a/db/user.db.js b/db/user.db.js index 46471bc..c098119 100644 --- a/db/user.db.js +++ b/db/user.db.js @@ -1,131 +1,108 @@ const pool = require("../config"); -const getAllUsersDb = async (param) => { - // limit & offset masuk fixed param - let fixedParams = [param.fixed.limit, param.fixed.offset, param.fixed.tenantID]; - - const { whereOrConditions, whereParam } = pool.buildStringOrIlike( - param.filterCriteria.column, - param.filterCriteria.criteria, - fixedParams - ); - const { whereConditions, queryParams } = pool.buildFilterQuery(param.filterQuery, whereParam);npm - - const query = ` - SELECT mut.*, mr.role_name, COUNT(*) OVER() AS total - FROM m_users mut - LEFT JOIN system.role_tenant mr ON mr.role_id = mut.role_id - WHERE mut.deleted_at IS NULL AND mut.is_sa != 1 AND mut.tenant_id = $3 - ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} - ${whereOrConditions ? whereOrConditions : ""} - ORDER BY mut.user_id - OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY +// Get all users +const getAllUsersDb = async () => { + const queryText = ` + SELECT u.user_id, u.user_fullname, u.user_name, u.user_email, u.user_phone, + u.is_active, u.created_at, u.updated_at, u.deleted_at, + u.updated_by, u.deleted_by, + r.role_id, r.role_name + FROM users u + LEFT JOIN roles r ON u.role_id = r.role_id + WHERE u.deleted_at IS NULL + ORDER BY u.user_id ASC `; - - const result = await pool.query(query, queryParams); - const rows = result.recordset; - - const total = rows.length > 0 ? parseInt(rows[0].total, 10) : 0; - return { data: rows, total }; + const result = await pool.query(queryText); + return result.recordset; }; -const createUserDb = async (param) => { - const insertData = { - tenant_id: param.tenantID, - user_fullname: param.userFullname, - user_name: param.userName, - user_email: param.userEmail ?? null, - user_password: param.userPassword, - role_id: param.roleId ?? null, - is_active: param.isActive ? 1 : 0, - created_by: param.userID, - updated_by: param.userID, - }; - - const { query, values } = pool.buildDynamicInsert("m_users", insertData); - - const result = await pool.query(query, values); +// Get user by ID +const getUserByIdDb = async (id) => { + const queryText = ` + SELECT u.user_id, u.user_fullname, u.user_name, u.user_email, u.user_phone, + u.is_active, u.created_at, u.updated_at, u.deleted_at, + u.updated_by, u.deleted_by, + r.role_id, r.role_name + FROM users u + LEFT JOIN roles r ON u.role_id = r.role_id + WHERE u.user_id = $1 AND u.deleted_at IS NULL + `; + const result = await pool.query(queryText, [id]); return result.recordset[0]; }; -const getUserByIdDb = async (id) => { - const query = ` - SELECT mut.* - FROM m_users mut - WHERE mut.user_id = $1 +// Get user by email (login) +const getUserByUserEmailDb = async (email) => { + const queryText = ` + SELECT u.user_id, u.user_fullname, u.user_name, u.user_email, u.user_phone, + u.user_password, u.is_active, u.is_sa, + r.role_id, r.role_name + FROM users u + LEFT JOIN roles r ON u.role_id = r.role_id + WHERE u.user_email = $1 AND u.deleted_at IS NULL `; - const result = await pool.query(query, [id]); + const result = await pool.query(queryText, [email]); return result.recordset[0]; }; const getUserByUsernameDb = async (username) => { - const query = ` - SELECT mut.* - FROM m_users mut - WHERE LOWER(mut.username) = LOWER($1) + const queryText = ` + SELECT u.user_id, u.user_fullname, u.user_name, u.user_email, u.user_phone, u.user_password, + u.is_active, u.role_id, + r.role_name + FROM users u + LEFT JOIN roles r ON u.role_id = r.role_id + WHERE u.user_name = $1 AND u.deleted_at IS NULL `; - const result = await pool.query(query, [username]); + const result = await pool.query(queryText, [username]); return result.recordset[0]; }; -const getUserByUserEmailDb = async (userEmail) => { - const query = ` - SELECT mut.* - FROM m_users mut - WHERE LOWER(mut.user_email) = LOWER($1) - `; - const result = await pool.query(query, [userEmail]); - return result.recordset[0]; +// Create user +const createUserDb = async (data) => { + const { query: queryText, values } = pool.buildDynamicInsert("users", data); + const result = await pool.query(queryText, values); + return result.recordset[0]?.inserted_id || null; }; -const updateUserDb = async (param) => { - const updateData = { - tenant_id: param.tenantID, - user_fullname: param.userFullname, - user_name: param.userName, - user_email: param.userEmail ?? null, - user_password: param.userPassword, - role_id: param.roleId ?? null, - is_active: param.isActive ? 1 : 0, - updated_by: param.userID, - }; - - const whereData = { user_id: param.id }; - - const { query, values } = pool.buildDynamicUpdate("m_users", updateData, whereData); - - const result = await pool.query(query, values); - return result.recordset[0]; +// Update user +const updateUserDb = async (userId, data) => { + const { query: queryText, values } = pool.buildDynamicUpdate("users", data, { user_id: userId }); + await pool.query(queryText, values); + return true; }; -const deleteUserDb = async (id, userID) => { - const query = ` - UPDATE m_users - SET deleted_at = GETDATE(), deleted_by = $1 - WHERE user_id = $2; - - SELECT * FROM m_users WHERE user_id = $2 +// Change user password +const changeUserPasswordDb = async (userId, newPassword) => { + const queryText = ` + UPDATE users + SET user_password = $1, updated_at = GETDATE() + WHERE user_id = $2 AND deleted_at IS NULL `; - const result = await pool.query(query, [userID, id]); - return result.recordset[0]; + await pool.query(queryText, [newPassword, userId]); + return true; }; -const changeUserPasswordDb = async (hashedPassword, userEmail, tenantId) => { - const query = ` - UPDATE m_users - SET user_password = $1 - WHERE user_email = $2 AND tenant_id = $3 +// Soft delete user +const deleteUserDb = async (userId, deletedBy) => { + const queryText = ` + UPDATE users + SET deleted_at = GETDATE(), + deleted_by = $1 + WHERE user_id = $2 `; - return pool.query(query, [hashedPassword, userEmail, tenantId]); + await pool.query(queryText, [deletedBy, userId]); + return true; }; -const getAllRoleDb = async (tenantId) => { - const query = ` - SELECT * - FROM system.role_tenant - WHERE deleted_at IS NULL AND tenant_id = $1 +// Get all roles +const getAllRoleDb = async () => { + const queryText = ` + SELECT role_id, role_name + FROM roles + ORDER BY role_id ASC `; - const result = await pool.query(query, [tenantId]); + const result = await pool.query(queryText); return result.recordset; }; From 6f66f0c2f3a6c383f8d9f45d48b59056712744c1 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 26 Sep 2025 11:03:47 +0700 Subject: [PATCH 004/126] add: auth validation --- helpers/authValidation.js | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 helpers/authValidation.js diff --git a/helpers/authValidation.js b/helpers/authValidation.js new file mode 100644 index 0000000..2bd6a72 --- /dev/null +++ b/helpers/authValidation.js @@ -0,0 +1,36 @@ +const Joi = require('joi'); + +const registerSchema = Joi.object({ + fullname: Joi.string().min(3).max(100).required(), + username: 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}' + }) +}); + +const loginSchema = Joi.object({ + email: Joi.string().email().required(), + password: Joi.string().required(), + captcha: Joi.string().required(), + captchaText: Joi.string().required() +}); + +module.exports = { + registerSchema, + loginSchema +}; From b022e86e027d0a719387bd27bfeb9d6e239d42b5 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 26 Sep 2025 11:04:04 +0700 Subject: [PATCH 005/126] fix: set response --- helpers/utils.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/helpers/utils.js b/helpers/utils.js index 41c60ed..716a5cc 100644 --- a/helpers/utils.js +++ b/helpers/utils.js @@ -1,12 +1,12 @@ -const setResponse = async (data = [], message = "success", statusCode = 200) => { - const response = { +const setResponse = (data = null, message = "success", statusCode = 200) => { + const total = Array.isArray(data) ? data.length : null; + + return { data, - total: data.length, + total, message, statusCode - } - - return response + }; }; const setResponsePaging = async (data = [], total, limit, page, message = "success", statusCode = 200) => { From 373b7079547a17388a1c371161f91cd7f895c726 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 26 Sep 2025 11:04:51 +0700 Subject: [PATCH 006/126] move to auth validation --- helpers/validateUser.js | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 helpers/validateUser.js diff --git a/helpers/validateUser.js b/helpers/validateUser.js deleted file mode 100644 index c904bb4..0000000 --- a/helpers/validateUser.js +++ /dev/null @@ -1,9 +0,0 @@ -const validateUser = (email, password) => { - const validEmail = typeof email === "string" && email.trim() !== ""; - const validPassword = - typeof password === "string" && password.trim().length >= 6; - - return validEmail && validPassword; -}; - -module.exports = validateUser; From d41c0421c4f3e52572ac3587cb7555ccc95d5877 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 26 Sep 2025 11:05:19 +0700 Subject: [PATCH 007/126] fix: verify token --- middleware/verifyToken.js | 88 ++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 39 deletions(-) diff --git a/middleware/verifyToken.js b/middleware/verifyToken.js index 58d4352..33e8dde 100644 --- a/middleware/verifyToken.js +++ b/middleware/verifyToken.js @@ -1,47 +1,57 @@ -const jwt = require("jsonwebtoken"); -const { ErrorHandler } = require("../helpers/error"); +const JWTService = require('../utils/jwt'); +const { ErrorHandler } = require('../helpers/error'); -const verifyToken = (req, res, next) => { - const authHeader = req.header("Authorization"); - // console.log("authHeader", authHeader) - - // Pastikan header Authorization ada dan berisi token - if (!authHeader || !authHeader.startsWith("Bearer ")) { - throw new ErrorHandler(401, "Token missing or invalid"); - } - - // Ambil token dari header Authorization - const token = authHeader.split(" ")[1]; +function setUser(req, decoded) { + req.user = { + userId: decoded.user_id, + fullname: decoded.user_fullname, + username: decoded.user_name, + email: decoded.user_email, + roleId: decoded.role_id, + roleName: decoded.role_name + }; +} +function verifyAccessToken(req, res, next) { try { - // const decoded = jwt.decode(token, { complete: true }); - // console.log("decoded", decoded) - // console.log("==============================") - // console.log("token", token) - // console.log("process.env.SECRET", process.env.SECRET) - // // console.log("==============================> ", jwt.verify(token, process.env.SECRET)) - // jwt.verify(token, process.env.SECRET, (err, decoded) => { - // if (err) { - // console.error('Error verifying token: ==============================>', err.message); - // } else { - // console.log('Decoded payload: ==============================>', decoded); - // } - // }); + let token = req.cookies?.accessToken; - const verified = jwt.verify(token, process.env.SECRET); - req.tokenExtract = verified; - // console.log(req.tokenExtract); - - req.userID = req.tokenExtract.user_id - req.tenantID = req.tokenExtract.tenant_id - req.roleID = req.tokenExtract.role_id - req.body.userID = req.tokenExtract.user_id - req.body.tenantID = req.tokenExtract.tenant_id - req.query.tenantID = req.tokenExtract.tenant_id + if (!token) { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer')) { + throw new ErrorHandler(401, 'Access Token is required'); + } + token = authHeader.split(' ')[1]; + } + + const decoded = JWTService.verifyToken(token); + setUser(req, decoded); next(); } catch (error) { - throw new ErrorHandler(401, error.message || "Invalid Token"); + if (error.name === 'TokenExpiredError' || error.name === 'JsonWebTokenError') { + return next(new ErrorHandler(401, error.message)); + } + next(new ErrorHandler(500, 'Authenticate verification failed')); } -}; +} -module.exports = verifyToken; +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); + setUser(req, decoded); + next(); + } catch (error) { + next(new ErrorHandler(500, 'Refresh token verification failed')); + } +} + +module.exports = { + verifyAccessToken, + verifyRefreshToken, +}; \ No newline at end of file From 050529cf78bf278a820a3730219a0c892ca1b310 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 26 Sep 2025 11:05:35 +0700 Subject: [PATCH 008/126] auth.route.js --- routes/auth.js | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 routes/auth.js diff --git a/routes/auth.js b/routes/auth.js deleted file mode 100644 index 1c738f0..0000000 --- a/routes/auth.js +++ /dev/null @@ -1,8 +0,0 @@ -const router = require("express").Router(); -const { - loginUser, -} = require("../controllers/auth.controller"); - -router.post("/login", loginUser); - -module.exports = router; From 7b2509ffa10e10eef3594fa6206c847c09b91e58 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 26 Sep 2025 11:05:49 +0700 Subject: [PATCH 009/126] update: auth routes --- routes/auth.route.js | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 routes/auth.route.js diff --git a/routes/auth.route.js b/routes/auth.route.js new file mode 100644 index 0000000..4d0e73f --- /dev/null +++ b/routes/auth.route.js @@ -0,0 +1,11 @@ +const express = require('express'); +const authController = require("../controllers/auth.controller"); + +const router = express.Router(); + +router.post('/login', authController.login); +router.post('/register', authController.register); +router.get('/generate-captcha', authController.generateCaptcha); +// router.post('/verify-captcha', authController.verifyCaptcha); + +module.exports = router; \ No newline at end of file From c0aca9ea87b3aa1931b373c376c0797fe9b86a9a Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 26 Sep 2025 11:06:04 +0700 Subject: [PATCH 010/126] update: routes --- routes/index.js | 6 +- routes/{users.js => users.route.js} | 15 ++- services/auth.service.js | 152 ++++++++++++++++------------ utils/captcha.js | 8 ++ utils/jwt.js | 81 +++++++++++++++ 5 files changed, 187 insertions(+), 75 deletions(-) rename routes/{users.js => users.route.js} (55%) create mode 100644 utils/captcha.js create mode 100644 utils/jwt.js diff --git a/routes/index.js b/routes/index.js index 92d5909..ff85afd 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,8 +1,8 @@ const router = require("express").Router(); -const auth = require("./auth"); -const users = require("./users"); +const auth = require("./auth.route"); +const users = require("./users.route"); router.use("/auth", auth); -router.use("/users", users); +router.use("/user", users); module.exports = router; diff --git a/routes/users.js b/routes/users.route.js similarity index 55% rename from routes/users.js rename to routes/users.route.js index 3cbf210..3f8a329 100644 --- a/routes/users.js +++ b/routes/users.route.js @@ -14,20 +14,19 @@ const verifyToken = require("../middleware/verifyToken"); router.get("/roles", getAllRoles); -router.route("/profile") - .get(getUserProfile); +router.get('/profile', verifyToken.verifyAccessToken, getUserProfile); router.route("/") - .get(verifyToken, getAllUsers) - .post(verifyToken, createUser); + .get(verifyToken.verifyAccessToken, getAllUsers) + .post(verifyToken.verifyAccessToken, createUser); router .route("/status") - .get(verifyToken, getAllStatusUsers); + .get(verifyToken.verifyAccessToken, getAllStatusUsers); router.route("/:id") - .get(verifyToken, getUserById) - .put(verifyToken, updateUser) - .delete(verifyToken, deleteUser); + .get(verifyToken.verifyAccessToken, getUserById) + .put(verifyToken.verifyAccessToken, updateUser) + .delete(verifyToken.verifyAccessToken, deleteUser); module.exports = router; diff --git a/services/auth.service.js b/services/auth.service.js index 583d300..fa25709 100644 --- a/services/auth.service.js +++ b/services/auth.service.js @@ -1,77 +1,101 @@ -const bcrypt = require("bcrypt"); -const jwt = require("jsonwebtoken"); -const validateUser = require("../helpers/validateUser"); -const { ErrorHandler } = require("../helpers/error"); -const { - getUserByUsernameDb -} = require("../db/user.db"); -const { logger } = require("../utils/logger"); +const { + getUserByUserEmailDb, + createUserDb +} = require('../db/user.db'); +const JWTService = require('../utils/jwt'); +const { hashPassword, comparePassword } = require('../helpers/hashPassword'); +const { ErrorHandler } = require('../helpers/error'); class AuthService { - async login(username, password, tenantId) { - try { - // if (!validateUser(username, password)) { - // throw new ErrorHandler(403, "Invalid login"); - // } - - const user = await getUserByUsernameDb(username, tenantId); - console.log(user); - - if (!user) { - throw new ErrorHandler(403, "Username not found."); - } - - const isCorrectPassword = password === user.password - if (!isCorrectPassword) { - throw new ErrorHandler(403, "Username or password incorrect."); - } - - const dataToken = { - tenant_id: tenantId, - user_id: user.user_id, - username, - fullname: user.full_name, - role_id: user.role_id - } - - const token = await this.signToken(dataToken); - const refreshToken = await this.signRefreshToken(dataToken); - - return { - token, - refreshToken, - role_id: dataToken.role_id, - tenant_id: tenantId, - user: { - user_id: dataToken.user_id, - fullname: dataToken.fullname, - username: dataToken.username, - }, - }; - } catch (error) { - throw new ErrorHandler(error.statusCode, error.message); + // Register + static async register({ fullname, username, email, phone, password }) { + const existingUser = await getUserByUserEmailDb(email); + if (existingUser) { + throw new ErrorHandler(400, 'Email already registered'); } + + const hashedPassword = await hashPassword(password); + + const userId = await createUserDb({ + user_fullname: fullname, + user_name: username, + user_email: email, + user_phone: phone, + user_password: hashedPassword, + role_id: 3, + is_sa: 0, + is_active: 1 + }); + + // ambil user baru + const newUser = { + user_id: userId, + user_fullname: fullname, + user_name: username, + user_email: email, + user_phone: phone, + role_id: 3, + }; + + // generate token pair + const tokens = JWTService.generateTokenPair(newUser); + + return { user: newUser, tokens }; } - async signToken(data) { - try { - // console.log("signToken process.env.SECRET", process.env.SECRET) - return jwt.sign(data, process.env.SECRET, { expiresIn: "23h" }); - } catch (error) { - logger.error(error); - throw new ErrorHandler(500, "An error occurred"); + // Login + static async login({ email, password }) { + const user = await getUserByUserEmailDb(email); + if (!user) { + throw new ErrorHandler(401, 'Invalid credentials'); } + + const passwordMatch = await comparePassword(password, user.user_password); + if (!passwordMatch) { + throw new ErrorHandler(401, 'Invalid credentials'); + } + + if (!user.is_active) { + throw new ErrorHandler(403, 'User is inactive'); + } + + const payload = { + user_id: user.user_id, + user_fullname: user.user_fullname, + user_name: user.user_name, + user_email: user.user_email, + phone: user.phone, + role_id: user.role_id, + role_name: user.role_name, + is_sa: user.is_sa + }; + + const tokens = JWTService.generateTokenPair(payload); + return { user: payload, tokens }; } - async signRefreshToken(data) { - try { - return jwt.sign(data, process.env.REFRESH_SECRET, { expiresIn: "23h" }); - } catch (error) { - logger.error(error); - throw new ErrorHandler(500, error.message); + // Refresh token + static async refreshToken(refreshToken) { + if (!refreshToken) { + throw new ErrorHandler(401, 'Refresh token is required'); } + + const decoded = JWTService.verifyRefreshToken(refreshToken); + + const payload = { + user_id: decoded.user_id, + user_fullname: decoded.user_fullname, + user_name: decoded.user_name, + user_email: decoded.user_email, + role_id: decoded.role_id, + role_name: decoded.role_name + }; + + const accessToken = JWTService.generateAccessToken(payload); + return { accessToken, tokenType: 'Bearer', expiresIn: 900 }; } + } -module.exports = new AuthService(); +module.exports = AuthService; diff --git a/utils/captcha.js b/utils/captcha.js new file mode 100644 index 0000000..50aee24 --- /dev/null +++ b/utils/captcha.js @@ -0,0 +1,8 @@ +const svgCaptcha = require('svg-captcha'); + +function createCaptcha() { + const captcha = svgCaptcha.create({ size: 5, noise: 2, color: true }); + return { svg: captcha.data, text: captcha.text }; +} + +module.exports = { createCaptcha }; \ No newline at end of file diff --git a/utils/jwt.js b/utils/jwt.js new file mode 100644 index 0000000..d131e6b --- /dev/null +++ b/utils/jwt.js @@ -0,0 +1,81 @@ +const jwt = require('jsonwebtoken'); +const crypto = require('crypto'); + +const tokenSettings = { + access: { + expiresIn: '15m', + type: 'access', + secret: process.env.SECRET + }, + refresh: { + expiresIn: '7d', + type: 'refresh', + secret: process.env.REFRESH_SECRET + } +}; + +function generateTokenId() { + return crypto.randomBytes(32).toString('hex'); +} + +function generateToken(payload, type) { + const settings = tokenSettings[type]; + if (!settings) throw new Error(`Invalid token type: ${type}`); + + const tokenPayload = { ...payload, type: settings.type }; + + return jwt.sign(tokenPayload, settings.secret, { + expiresIn: settings.expiresIn, + jwtid: generateTokenId() + }); +} + +function verifyTokenType(token, type) { + const settings = tokenSettings[type]; + try { + const decoded = jwt.verify(token, settings.secret); + if (decoded.type !== type) throw new Error('Invalid token type'); + return decoded; + } catch (error) { + if (error.name === 'TokenExpiredError') throw new Error(`${type} token has expired`); + if (error.name === 'JsonWebTokenError') throw new Error(`Invalid ${type} token`); + throw error; + } +} + +function generateAccessToken(payload) { + return generateToken(payload, 'access'); +} + +function generateRefreshToken(payload) { + return generateToken(payload, 'refresh'); +} + +function verifyToken(token) { + return verifyTokenType(token, 'access'); +} + +function verifyRefreshToken(token) { + return verifyTokenType(token, 'refresh'); +} + +function generateTokenPair(payload) { + const accessToken = generateAccessToken(payload); + const refreshToken = generateRefreshToken(payload); + + return { + accessToken, + refreshToken, + tokenType: 'Bearer', + expiresIn: 900, + refreshExpiresIn: 604800 + }; +} + +module.exports = { + generateAccessToken, + generateRefreshToken, + verifyToken, + verifyRefreshToken, + generateTokenPair, +}; From 4bd50c7a4cf6d374b31a0072bc0344c9e363627e Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 1 Oct 2025 10:16:38 +0700 Subject: [PATCH 011/126] add: captcha --- utils/captcha.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/captcha.js b/utils/captcha.js index 50aee24..d14d31c 100644 --- a/utils/captcha.js +++ b/utils/captcha.js @@ -1,7 +1,7 @@ const svgCaptcha = require('svg-captcha'); function createCaptcha() { - const captcha = svgCaptcha.create({ size: 5, noise: 2, color: true }); + const captcha = svgCaptcha.create({ size: 5, noise: 7, color: true }); return { svg: captcha.data, text: captcha.text }; } From 27d4541cfcddbaab3145ad965bd0fb7e50dac988 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 1 Oct 2025 10:17:05 +0700 Subject: [PATCH 012/126] fix: auth --- controllers/auth.controller.js | 91 ++++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 36 deletions(-) diff --git a/controllers/auth.controller.js b/controllers/auth.controller.js index e876dfd..8c3b405 100644 --- a/controllers/auth.controller.js +++ b/controllers/auth.controller.js @@ -1,5 +1,5 @@ const AuthService = require('../services/auth.service'); -const { registerSchema, loginSchema } = require('../helpers/authValidation'); +const { registerSchema, loginSchema } = require('../helpers/validation'); const { setResponse } = require('../helpers/utils'); const { createCaptcha } = require('../utils/captcha'); @@ -11,29 +11,32 @@ class AuthController { const { error, value } = registerSchema.validate(req.body, { abortEarly: false }); if (error) { - // kumpulkan pesan error per field const errors = error.details.reduce((acc, cur) => { const field = Array.isArray(cur.path) ? cur.path.join('.') : String(cur.path); if (!acc[field]) acc[field] = []; acc[field].push(cur.message); return acc; }, {}); - - return res.status(400).json( - setResponse(errors, 'Validation failed', 400) - ); + return res.status(400).json(setResponse(errors, 'Validation failed', 400)); } - // Normalisasi phone menjadi +62 if (value.phone && value.phone.startsWith('0')) { value.phone = '+62' + value.phone.slice(1); } - const user = await AuthService.register(value); - return res.status(201).json( - setResponse(user, 'User registered successfully', 201) - ); + const { user, tokens } = await AuthService.register(value); + // Set refresh token in cookie + res.cookie('refreshToken', tokens.refreshToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 7 * 24 * 60 * 60 * 1000 // 7 hari + }); + + return res.status(201).json( + setResponse({ user, accessToken: tokens.accessToken }, 'User registered successfully', 201) + ); } catch (err) { return res.status(err.statusCode || 500).json( setResponse([], err.message || 'Register failed', err.statusCode || 500) @@ -41,18 +44,17 @@ class AuthController { } } - + // Captcha static async generateCaptcha(req, res) { try { const { svg, text } = createCaptcha(); - return res.status(200).json({ - data: { svg, text } - }); + return res.status(200).json({ data: { svg, text } }); } catch (err) { return res.status(500).json(setResponse([], 'Captcha failed', 500)); } } + // Login static async login(req, res) { try { const { error, value } = loginSchema.validate(req.body, { abortEarly: false }); @@ -60,15 +62,23 @@ class AuthController { const { email, password, captcha, captchaText } = value; - // verify captcha if (!captcha || captcha.toLowerCase() !== captchaText.toLowerCase()) { return res.status(400).json(setResponse([], 'Invalid captcha', 400)); } const { user, tokens } = await AuthService.login({ email, password }); - return res.status(200).json(setResponse({ user, tokens }, 'Login successful', 200)); + // Set refresh token in cookie + res.cookie('refreshToken', tokens.refreshToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 7 * 24 * 60 * 60 * 1000 // 7 hari + }); + return res.status(200).json( + setResponse({ user, accessToken: tokens.accessToken }, 'Login successful', 200) + ); } catch (err) { return res.status(err.statusCode || 500).json( setResponse([], err.message || 'Login failed', err.statusCode || 500) @@ -76,28 +86,37 @@ class AuthController { } } -// // Verify Captcha (secure) -// static async verifyCaptcha(req, res) { -// const { userInput } = req.body; + // Refresh Token + static async refreshToken(req, res) { + try { + const refreshToken = req.cookies?.refreshToken; + if (!refreshToken) { + return res.status(401).json(setResponse(null, 'Refresh token is required', 401)); + } -// if (!userInput || !req.session.captcha) { -// return res.status(400).json( -// setResponse([], 'Missing data', 400) -// ); -// } + const result = await AuthService.refreshToken(refreshToken); -// if (userInput.toLowerCase() === req.session.captcha.toLowerCase()) { -// req.session.captcha = null; // one-time use -// return res.json( -// setResponse([], 'Captcha is valid', 200) -// ); -// } else { -// return res.status(400).json( -// setResponse([], 'Invalid captcha', 400) -// ); -// } -// } + return res.status(200).json(setResponse(result, 'Token refreshed successfully', 200)); + } catch (err) { + return res.status(err.statusCode || 500).json( + setResponse(null, err.message || 'Refresh token failed', err.statusCode || 500) + ); + } + } + // Logout + static async logout(req, res) { + try { + res.clearCookie('refreshToken', { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + }); + return res.status(200).json(setResponse(null, 'Logged out successfully', 200)); + } catch (err) { + return res.status(500).json(setResponse(null, 'Logout failed', 500)); + } + } } module.exports = AuthController; From 86cd21ca0bc33d6edd8eaeed8f0ff128f97b75f5 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 1 Oct 2025 10:17:22 +0700 Subject: [PATCH 013/126] add: device controller --- controllers/device.controller.js | 103 +++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 controllers/device.controller.js diff --git a/controllers/device.controller.js b/controllers/device.controller.js new file mode 100644 index 0000000..4de3e9d --- /dev/null +++ b/controllers/device.controller.js @@ -0,0 +1,103 @@ +const DeviceService = require('../services/device.service'); +const { deviceSchema } = require('../helpers/validation'); +const { setResponse } = require('../helpers/utils'); + +class DeviceController { + // Get all devices + static async getAll(req, res) { + try { + const devices = await DeviceService.getAllDevices(); + return res.status(200).json( + setResponse(devices, 'Devices retrieved successfully', 200) + ); + } catch (err) { + return res.status(err.statusCode || 500).json( + setResponse([], err.message || 'Failed to get devices', err.statusCode || 500) + ); + } + } + + // Get device by ID + static async getById(req, res) { + try { + const { id } = req.params; + const device = await DeviceService.getDeviceById(id); + return res.status(200).json( + setResponse(device, 'Device retrieved successfully', 200) + ); + } catch (err) { + return res.status(err.statusCode || 500).json( + setResponse([], err.message || 'Failed to get device', err.statusCode || 500) + ); + } + } + + // Create device + static async create(req, res) { + try { + const { error, value } = deviceSchema.validate(req.body || {}, { abortEarly: false }); + if (error) { + const errors = error.details.reduce((acc, cur) => { + const field = Array.isArray(cur.path) ? cur.path.join('.') : String(cur.path); + if (!acc[field]) acc[field] = []; + acc[field].push(cur.message); + return acc; + }, {}); + return res.status(400).json(setResponse(errors, 'Validation failed', 400)); + } + + const newDevice = await DeviceService.createDevice(value, req.user.userId); + return res.status(201).json( + setResponse(newDevice, 'Device created successfully', 201) + ); + } catch (err) { + return res.status(err.statusCode || 500).json( + setResponse([], err.message || 'Failed to create device', err.statusCode || 500) + ); + } + } + + // Update device + static async update(req, res) { + try { + const { id } = req.params; + const { error, value } = deviceSchema.validate(req.body || {}, { abortEarly: false }); + if (error) { + const errors = error.details.reduce((acc, cur) => { + const field = Array.isArray(cur.path) ? cur.path.join('.') : String(cur.path); + if (!acc[field]) acc[field] = []; + acc[field].push(cur.message); + return acc; + }, {}); + return res.status(400).json(setResponse(errors, 'Validation failed', 400)); + } + + await DeviceService.updateDevice(id, value, req.user.userId); + return res.status(200).json( + setResponse([], 'Device updated successfully', 200) + ); + } catch (err) { + return res.status(err.statusCode || 500).json( + setResponse([], err.message || 'Failed to update device', err.statusCode || 500) + ); + } + } + + // Soft delete device + static async delete(req, res) { + try { + const { id } = req.params; + + await DeviceService.deleteDevice(id, req.user.userId); + return res.status(200).json( + setResponse([], 'Device deleted successfully', 200) + ); + } catch (err) { + return res.status(err.statusCode || 500).json( + setResponse([], err.message || 'Failed to delete device', err.statusCode || 500) + ); + } + } +} + +module.exports = DeviceController; From 8e3c2df2765d9d572a136770b38c516360505931 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 1 Oct 2025 10:17:29 +0700 Subject: [PATCH 014/126] add: device db --- db/device.db.js | 76 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 db/device.db.js diff --git a/db/device.db.js b/db/device.db.js new file mode 100644 index 0000000..b92b754 --- /dev/null +++ b/db/device.db.js @@ -0,0 +1,76 @@ +const pool = require("../config"); + +// Get all devices +const getAllDevicesDb = async () => { + const queryText = ` + SELECT * + FROM m_device + WHERE deleted_at IS NULL + ORDER BY device_id ASC + `; + const result = await pool.query(queryText); + return result.recordset; +}; + +// Get device by ID +const getDeviceByIdDb = async (id) => { + const queryText = ` + SELECT * + FROM m_device + WHERE device_id = $1 + AND deleted_at IS NULL + `; + const result = await pool.query(queryText, [id]); + return result.recordset[0]; +}; + +// Get device by device_code +const getDeviceByCodeDb = async (code) => { + const queryText = ` + SELECT * + FROM m_device + WHERE device_code = $1 + AND deleted_at IS NULL + `; + const result = await pool.query(queryText, [code]); + return result.recordset[0]; +}; + +// Create device +const createDeviceDb = async (data) => { + const { query: queryText, values } = pool.buildDynamicInsert("m_device", data); + const result = await pool.query(queryText, values); + const insertedId = result.recordset[0]?.inserted_id; + if (!insertedId) return null; + + return getDeviceByIdDb(insertedId); +}; + +// Update device +const updateDeviceDb = async (id, data) => { + const { query: queryText, values } = pool.buildDynamicUpdate("m_device", data, { device_id: id }); + await pool.query(queryText, values); + return getDeviceByIdDb(id); +}; + +// Soft delete device +const softDeleteDeviceDb = async (id, deletedBy) => { + const queryText = ` + UPDATE m_device + SET deleted_at = GETDATE(), + deleted_by = $1 + WHERE device_id = $2 + AND deleted_at IS NULL + `; + await pool.query(queryText, [deletedBy, id]); + return true; +}; + +module.exports = { + getAllDevicesDb, + getDeviceByIdDb, + getDeviceByCodeDb, + createDeviceDb, + updateDeviceDb, + softDeleteDeviceDb, +}; From 15851e58530cf6e6f74b5af1375b754911450c82 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 1 Oct 2025 10:17:43 +0700 Subject: [PATCH 015/126] update: user db --- db/user.db.js | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/db/user.db.js b/db/user.db.js index c098119..f2096f9 100644 --- a/db/user.db.js +++ b/db/user.db.js @@ -7,8 +7,8 @@ const getAllUsersDb = async () => { u.is_active, u.created_at, u.updated_at, u.deleted_at, u.updated_by, u.deleted_by, r.role_id, r.role_name - FROM users u - LEFT JOIN roles r ON u.role_id = r.role_id + FROM m_users u + LEFT JOIN m_roles r ON u.role_id = r.role_id WHERE u.deleted_at IS NULL ORDER BY u.user_id ASC `; @@ -23,8 +23,8 @@ const getUserByIdDb = async (id) => { u.is_active, u.created_at, u.updated_at, u.deleted_at, u.updated_by, u.deleted_by, r.role_id, r.role_name - FROM users u - LEFT JOIN roles r ON u.role_id = r.role_id + FROM m_users u + LEFT JOIN m_roles r ON u.role_id = r.role_id WHERE u.user_id = $1 AND u.deleted_at IS NULL `; const result = await pool.query(queryText, [id]); @@ -37,21 +37,22 @@ const getUserByUserEmailDb = async (email) => { SELECT u.user_id, u.user_fullname, u.user_name, u.user_email, u.user_phone, u.user_password, u.is_active, u.is_sa, r.role_id, r.role_name - FROM users u - LEFT JOIN roles r ON u.role_id = r.role_id + FROM m_users u + LEFT JOIN m_roles r ON u.role_id = r.role_id WHERE u.user_email = $1 AND u.deleted_at IS NULL `; const result = await pool.query(queryText, [email]); return result.recordset[0]; }; +// Get user by username const getUserByUsernameDb = async (username) => { const queryText = ` SELECT u.user_id, u.user_fullname, u.user_name, u.user_email, u.user_phone, u.user_password, u.is_active, u.role_id, r.role_name - FROM users u - LEFT JOIN roles r ON u.role_id = r.role_id + FROM m_users u + LEFT JOIN m_roles r ON u.role_id = r.role_id WHERE u.user_name = $1 AND u.deleted_at IS NULL `; const result = await pool.query(queryText, [username]); @@ -60,14 +61,14 @@ const getUserByUsernameDb = async (username) => { // Create user const createUserDb = async (data) => { - const { query: queryText, values } = pool.buildDynamicInsert("users", data); + const { query: queryText, values } = pool.buildDynamicInsert("m_users", data); const result = await pool.query(queryText, values); return result.recordset[0]?.inserted_id || null; }; // Update user const updateUserDb = async (userId, data) => { - const { query: queryText, values } = pool.buildDynamicUpdate("users", data, { user_id: userId }); + const { query: queryText, values } = pool.buildDynamicUpdate("m_users", data, { user_id: userId }); await pool.query(queryText, values); return true; }; @@ -75,7 +76,7 @@ const updateUserDb = async (userId, data) => { // Change user password const changeUserPasswordDb = async (userId, newPassword) => { const queryText = ` - UPDATE users + UPDATE m_users SET user_password = $1, updated_at = GETDATE() WHERE user_id = $2 AND deleted_at IS NULL `; @@ -86,7 +87,7 @@ const changeUserPasswordDb = async (userId, newPassword) => { // Soft delete user const deleteUserDb = async (userId, deletedBy) => { const queryText = ` - UPDATE users + UPDATE m_users SET deleted_at = GETDATE(), deleted_by = $1 WHERE user_id = $2 @@ -99,7 +100,7 @@ const deleteUserDb = async (userId, deletedBy) => { const getAllRoleDb = async () => { const queryText = ` SELECT role_id, role_name - FROM roles + FROM m_roles ORDER BY role_id ASC `; const result = await pool.query(queryText); From 0590773d640d4a5a7859105c13f47b771403dca7 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 1 Oct 2025 10:18:06 +0700 Subject: [PATCH 016/126] update: validation --- helpers/authValidation.js | 36 ------------------------ helpers/validation.js | 58 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 36 deletions(-) delete mode 100644 helpers/authValidation.js create mode 100644 helpers/validation.js diff --git a/helpers/authValidation.js b/helpers/authValidation.js deleted file mode 100644 index 2bd6a72..0000000 --- a/helpers/authValidation.js +++ /dev/null @@ -1,36 +0,0 @@ -const Joi = require('joi'); - -const registerSchema = Joi.object({ - fullname: Joi.string().min(3).max(100).required(), - username: 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}' - }) -}); - -const loginSchema = Joi.object({ - email: Joi.string().email().required(), - password: Joi.string().required(), - captcha: Joi.string().required(), - captchaText: Joi.string().required() -}); - -module.exports = { - registerSchema, - loginSchema -}; diff --git a/helpers/validation.js b/helpers/validation.js new file mode 100644 index 0000000..ceb7584 --- /dev/null +++ b/helpers/validation.js @@ -0,0 +1,58 @@ +const Joi = require('joi'); + +// ======================== +// Auth Validation +// ======================== +const registerSchema = 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}' + }) +}); + +const loginSchema = Joi.object({ + email: Joi.string().email().required(), + password: Joi.string().required(), + captcha: Joi.string().required(), + captchaText: Joi.string().required() +}); + +// ======================== +// Device Validation +// ======================== +const deviceSchema = Joi.object({ + device_code: Joi.string().max(100).required(), + device_name: Joi.string().max(100).required(), + device_status: Joi.boolean().required(), + device_location: Joi.string().max(100).required(), + device_description: Joi.string().required(), + ip_address: Joi.string() + .ip({ version: ['ipv4', 'ipv6'] }) + .required() + .messages({ + 'string.ip': 'IP address must be a valid IPv4 or IPv6 address' + }) +}); + +module.exports = { + registerSchema, + loginSchema, + deviceSchema +}; From 1cadf8c69daff9c7fcdefce40fae06a95e72ad6f Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 1 Oct 2025 10:18:32 +0700 Subject: [PATCH 017/126] update: verifyRole --- middleware/verifyAdmin.js | 14 -------------- middleware/verifyRole.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 14 deletions(-) delete mode 100644 middleware/verifyAdmin.js create mode 100644 middleware/verifyRole.js diff --git a/middleware/verifyAdmin.js b/middleware/verifyAdmin.js deleted file mode 100644 index 4a04109..0000000 --- a/middleware/verifyAdmin.js +++ /dev/null @@ -1,14 +0,0 @@ -const { ErrorHandler } = require("../helpers/error"); - -module.exports = (req, res, next) => { - const { roles } = req.user; - if (roles && roles.includes("admin")) { - req.user = { - ...req.user, - roles, - }; - return next(); - } else { - throw new ErrorHandler(401, "require admin role"); - } -}; diff --git a/middleware/verifyRole.js b/middleware/verifyRole.js new file mode 100644 index 0000000..2bdfdb9 --- /dev/null +++ b/middleware/verifyRole.js @@ -0,0 +1,28 @@ +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; From 909c6d5fba9309bd52df2555795759d13a6570bd Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 1 Oct 2025 10:18:44 +0700 Subject: [PATCH 018/126] fix: token --- middleware/verifyToken.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/middleware/verifyToken.js b/middleware/verifyToken.js index 33e8dde..ab5eb21 100644 --- a/middleware/verifyToken.js +++ b/middleware/verifyToken.js @@ -8,7 +8,8 @@ function setUser(req, decoded) { username: decoded.user_name, email: decoded.user_email, roleId: decoded.role_id, - roleName: decoded.role_name + roleName: decoded.role_name, + is_sa: decoded.is_sa }; } @@ -44,10 +45,10 @@ function verifyRefreshToken(req, res, next) { } const decoded = JWTService.verifyRefreshToken(refreshToken); - setUser(req, decoded); + req.user = decoded; next(); } catch (error) { - next(new ErrorHandler(500, 'Refresh token verification failed')); + next(new ErrorHandler(401, 'Refresh token is invalid or expired')); } } From ed92e20033d5195d449f6315aca0bae5c54ed2f0 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 1 Oct 2025 10:19:09 +0700 Subject: [PATCH 019/126] add: refreshtoken route --- routes/auth.route.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/routes/auth.route.js b/routes/auth.route.js index 4d0e73f..166d68d 100644 --- a/routes/auth.route.js +++ b/routes/auth.route.js @@ -1,11 +1,11 @@ const express = require('express'); -const authController = require("../controllers/auth.controller"); +const AuthController = require("../controllers/auth.controller"); const router = express.Router(); -router.post('/login', authController.login); -router.post('/register', authController.register); -router.get('/generate-captcha', authController.generateCaptcha); -// router.post('/verify-captcha', authController.verifyCaptcha); +router.post('/login', AuthController.login); +router.post('/register', AuthController.register); +router.get('/generate-captcha', AuthController.generateCaptcha); +router.post('/refresh-token', AuthController.refreshToken); module.exports = router; \ No newline at end of file From c1ff968c19edf8b75d319a9797025666faa33bb6 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 1 Oct 2025 10:19:18 +0700 Subject: [PATCH 020/126] add: device --- routes/device.route.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 routes/device.route.js diff --git a/routes/device.route.js b/routes/device.route.js new file mode 100644 index 0000000..2885ad9 --- /dev/null +++ b/routes/device.route.js @@ -0,0 +1,14 @@ +const express = require('express'); +const DeviceController = require('../controllers/device.controller'); +const verifyToken = require("../middleware/verifyToken") +const verifyRole = require("../middleware/verifyRole") + +const router = express.Router(); + +router.get('/', verifyToken.verifyAccessToken, DeviceController.getAll); +router.get('/:id', verifyToken.verifyAccessToken, DeviceController.getById); +router.post('/', verifyToken.verifyAccessToken, verifyRole([1]), DeviceController.create); +router.put('/:id', verifyToken.verifyAccessToken, verifyRole([1, 2]), DeviceController.update); +router.delete('/:id', verifyToken.verifyAccessToken, verifyRole([1]), DeviceController.delete); + +module.exports = router; \ No newline at end of file From 18cf2dd73e8061d2b87db77e81d110bc81f1924f Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 1 Oct 2025 10:19:32 +0700 Subject: [PATCH 021/126] add: device route --- routes/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/routes/index.js b/routes/index.js index ff85afd..f78ea07 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,8 +1,10 @@ const router = require("express").Router(); const auth = require("./auth.route"); const users = require("./users.route"); +const device = require('./device.route'); router.use("/auth", auth); router.use("/user", users); +router.use("/device", device); module.exports = router; From a2124ce5ea4cb52cba264c5eb2db8bff4d715b81 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 1 Oct 2025 10:19:51 +0700 Subject: [PATCH 022/126] update --- routes/users.route.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/users.route.js b/routes/users.route.js index 3f8a329..bc7fd21 100644 --- a/routes/users.route.js +++ b/routes/users.route.js @@ -9,7 +9,7 @@ const { getAllStatusUsers } = require("../controllers/users.controller"); const router = require("express").Router(); -const verifyAdmin = require("../middleware/verifyAdmin"); +const verifyAdmin = require("../middleware/verifyRole"); const verifyToken = require("../middleware/verifyToken"); router.get("/roles", getAllRoles); From e57717935857d8f5bb3704404b3eb917dca7f45b Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 1 Oct 2025 10:20:01 +0700 Subject: [PATCH 023/126] update: auth --- services/auth.service.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/services/auth.service.js b/services/auth.service.js index fa25709..3e771f8 100644 --- a/services/auth.service.js +++ b/services/auth.service.js @@ -2,14 +2,14 @@ const { getUserByUserEmailDb, createUserDb } = require('../db/user.db'); -const JWTService = require('../utils/jwt'); const { hashPassword, comparePassword } = require('../helpers/hashPassword'); const { ErrorHandler } = require('../helpers/error'); +const JWTService = require('../utils/jwt'); class AuthService { // Register - static async register({ fullname, username, email, phone, password }) { + static async register({ fullname, name, email, phone, password }) { const existingUser = await getUserByUserEmailDb(email); if (existingUser) { throw new ErrorHandler(400, 'Email already registered'); @@ -19,7 +19,7 @@ class AuthService { const userId = await createUserDb({ user_fullname: fullname, - user_name: username, + user_name: name, user_email: email, user_phone: phone, user_password: hashedPassword, @@ -28,11 +28,10 @@ class AuthService { is_active: 1 }); - // ambil user baru const newUser = { user_id: userId, user_fullname: fullname, - user_name: username, + user_name: name, user_email: email, user_phone: phone, role_id: 3, @@ -65,7 +64,7 @@ class AuthService { user_fullname: user.user_fullname, user_name: user.user_name, user_email: user.user_email, - phone: user.phone, + user_phone: user.user_phone, role_id: user.role_id, role_name: user.role_name, is_sa: user.is_sa @@ -75,7 +74,7 @@ class AuthService { return { user: payload, tokens }; } - // Refresh token + // Refresh Token static async refreshToken(refreshToken) { if (!refreshToken) { throw new ErrorHandler(401, 'Refresh token is required'); @@ -89,13 +88,14 @@ class AuthService { user_name: decoded.user_name, user_email: decoded.user_email, role_id: decoded.role_id, - role_name: decoded.role_name + role_name: decoded.role_name, + is_sa: decoded.is_sa }; const accessToken = JWTService.generateAccessToken(payload); + return { accessToken, tokenType: 'Bearer', expiresIn: 900 }; } - } module.exports = AuthService; From 446e393ee872cfe12e813baa3a9cb074a138c5b4 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 1 Oct 2025 10:20:09 +0700 Subject: [PATCH 024/126] add: device service --- services/device.service.js | 81 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 services/device.service.js diff --git a/services/device.service.js b/services/device.service.js new file mode 100644 index 0000000..d4c8a44 --- /dev/null +++ b/services/device.service.js @@ -0,0 +1,81 @@ +const { + getAllDevicesDb, + getDeviceByIdDb, + getDeviceByCodeDb, + createDeviceDb, + updateDeviceDb, + softDeleteDeviceDb +} = require('../db/device.db'); +const { ErrorHandler } = require('../helpers/error'); + +class DeviceService { + // Get all devices + static async getAllDevices() { + const devices = await getAllDevicesDb(); + return devices; + } + + // Get device by ID + static async getDeviceById(id) { + const device = await getDeviceByIdDb(id); + if (!device) { + throw new ErrorHandler(404, 'Device not found'); + } + return device; + } + + // Get device by code + static async getDeviceByCode(code) { + const device = await getDeviceByCodeDb(code); + if (!device) { + throw new ErrorHandler(404, 'Device not found'); + } + return device; + } + + // Create device + static async createDevice(data, userId) { + if (!data || typeof data !== 'object') data = {}; + + data.created_by = userId; + data.is_active = 1; + + // cek kode unik + const existingDevice = await getDeviceByCodeDb(data.device_code); + if (existingDevice) { + throw new ErrorHandler(400, 'Device code already exists'); + } + + const newDevice = await createDeviceDb(data); + return newDevice; + } + + // Update device + static async updateDevice(id, data, userId) { + if (!data || typeof data !== 'object') data = {}; + + const existingDevice = await getDeviceByIdDb(id); + if (!existingDevice) { + throw new ErrorHandler(404, 'Device not found'); + } + + data.updated_by = userId; + data.updated_at = new Date(); + + await updateDeviceDb(id, data); + return { message: 'Device updated successfully' }; + } + + // Soft delete device + static async deleteDevice(id, userId) { + const existingDevice = await getDeviceByIdDb(id); + if (!existingDevice) { + throw new ErrorHandler(404, 'Device not found'); + } + + await softDeleteDeviceDb(id, userId); + return { message: 'Device deleted successfully' }; + } +} + +module.exports = DeviceService; From 8d2a8565ff847c9826bfb2baf482c0880588f726 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 1 Oct 2025 14:46:55 +0700 Subject: [PATCH 025/126] add: update device schema --- helpers/validation.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/helpers/validation.js b/helpers/validation.js index ceb7584..1b6738a 100644 --- a/helpers/validation.js +++ b/helpers/validation.js @@ -26,7 +26,6 @@ const registerSchema = Joi.object({ 'string.pattern.name': 'Password must contain at least one {#name}' }) }); - const loginSchema = Joi.object({ email: Joi.string().email().required(), password: Joi.string().required(), @@ -51,8 +50,22 @@ const deviceSchema = Joi.object({ }) }); +const deviceUpdateSchema = Joi.object({ + device_code: Joi.string().max(100), + device_name: Joi.string().max(100), + device_status: Joi.boolean(), + device_location: Joi.string().max(100), + device_description: Joi.string(), + ip_address: Joi.string() + .ip({ version: ['ipv4', 'ipv6'] }) + .messages({ + 'string.ip': 'IP address must be a valid IPv4 or IPv6 address' + }) +}).min(1); + module.exports = { registerSchema, loginSchema, - deviceSchema + deviceSchema, + deviceUpdateSchema }; From cdb9a7e0ef19a71c92d6be0e25d60554ab1d59a7 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 1 Oct 2025 14:47:58 +0700 Subject: [PATCH 026/126] fix: get all device --- controllers/device.controller.js | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/controllers/device.controller.js b/controllers/device.controller.js index 4de3e9d..eeb5ef4 100644 --- a/controllers/device.controller.js +++ b/controllers/device.controller.js @@ -1,18 +1,17 @@ const DeviceService = require('../services/device.service'); -const { deviceSchema } = require('../helpers/validation'); +const { deviceSchema, deviceUpdateSchema } = require('../helpers/validation'); const { setResponse } = require('../helpers/utils'); class DeviceController { // Get all devices - static async getAll(req, res) { + static async getAll(req, res) { try { - const devices = await DeviceService.getAllDevices(); - return res.status(200).json( - setResponse(devices, 'Devices retrieved successfully', 200) - ); + const { search } = req.query; + const devices = await DeviceService.getAllDevices(search || ''); + return res.status(200).json(setResponse(devices, 'Devices retrieved successfully', 200)); } catch (err) { return res.status(err.statusCode || 500).json( - setResponse([], err.message || 'Failed to get devices', err.statusCode || 500) + setResponse([], err.message || 'Failed to retrieve devices', err.statusCode || 500) ); } } @@ -22,12 +21,10 @@ class DeviceController { try { const { id } = req.params; const device = await DeviceService.getDeviceById(id); - return res.status(200).json( - setResponse(device, 'Device retrieved successfully', 200) - ); + return res.status(200).json(setResponse(device, 'Device retrieved successfully', 200)); } catch (err) { return res.status(err.statusCode || 500).json( - setResponse([], err.message || 'Failed to get device', err.statusCode || 500) + setResponse([], err.message || 'Device not found', err.statusCode || 500) ); } } @@ -61,7 +58,7 @@ class DeviceController { static async update(req, res) { try { const { id } = req.params; - const { error, value } = deviceSchema.validate(req.body || {}, { abortEarly: false }); + const { error, value } = deviceUpdateSchema.validate(req.body || {}, { abortEarly: false }); if (error) { const errors = error.details.reduce((acc, cur) => { const field = Array.isArray(cur.path) ? cur.path.join('.') : String(cur.path); @@ -72,9 +69,10 @@ class DeviceController { return res.status(400).json(setResponse(errors, 'Validation failed', 400)); } - await DeviceService.updateDevice(id, value, req.user.userId); + const updatedDevice = await DeviceService.updateDevice(id, value, req.user.userId); + return res.status(200).json( - setResponse([], 'Device updated successfully', 200) + setResponse(updatedDevice.data, 'Device updated successfully', 200) ); } catch (err) { return res.status(err.statusCode || 500).json( From 373caf307b44158cd3a05b2bc66ebf43a1745c49 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 1 Oct 2025 14:48:17 +0700 Subject: [PATCH 027/126] add: device search db --- db/device.db.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/db/device.db.js b/db/device.db.js index b92b754..1b2581c 100644 --- a/db/device.db.js +++ b/db/device.db.js @@ -12,6 +12,25 @@ const getAllDevicesDb = async () => { 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 queryText = ` @@ -73,4 +92,5 @@ module.exports = { createDeviceDb, updateDeviceDb, softDeleteDeviceDb, + searchDevicesDb, }; From ec81b4b31120e5114aab46636998c8654750bc3c Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 1 Oct 2025 14:48:43 +0700 Subject: [PATCH 028/126] fix: get all device --- services/device.service.js | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/services/device.service.js b/services/device.service.js index d4c8a44..31a9bb3 100644 --- a/services/device.service.js +++ b/services/device.service.js @@ -4,23 +4,24 @@ const { getDeviceByCodeDb, createDeviceDb, updateDeviceDb, - softDeleteDeviceDb + softDeleteDeviceDb, + searchDevicesDb } = require('../db/device.db'); const { ErrorHandler } = require('../helpers/error'); class DeviceService { // Get all devices - static async getAllDevices() { - const devices = await getAllDevicesDb(); - return devices; + static async getAllDevices(search) { + if (!search || search.trim() === '') { + return await getAllDevicesDb(); + } + return await searchDevicesDb(search); } // Get device by ID static async getDeviceById(id) { const device = await getDeviceByIdDb(id); - if (!device) { - throw new ErrorHandler(404, 'Device not found'); - } + if (!device) throw new ErrorHandler(404, 'Device not found'); return device; } @@ -38,7 +39,6 @@ class DeviceService { if (!data || typeof data !== 'object') data = {}; data.created_by = userId; - data.is_active = 1; // cek kode unik const existingDevice = await getDeviceByCodeDb(data.device_code); @@ -60,10 +60,15 @@ class DeviceService { } data.updated_by = userId; - data.updated_at = new Date(); + + const updatedDevice = await updateDeviceDb(id, data); + await updateDeviceDb(id, data); - return { message: 'Device updated successfully' }; + return { + message: 'Device updated successfully', + data: updatedDevice, + }; } // Soft delete device From f4580c42eea10b3478ee6e8ae2a55105de460ea3 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 2 Oct 2025 15:39:08 +0700 Subject: [PATCH 029/126] update: verifytoken --- utils/jwt.js | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/utils/jwt.js b/utils/jwt.js index d131e6b..4d5aaa4 100644 --- a/utils/jwt.js +++ b/utils/jwt.js @@ -32,15 +32,9 @@ function generateToken(payload, type) { function verifyTokenType(token, type) { const settings = tokenSettings[type]; - try { - const decoded = jwt.verify(token, settings.secret); - if (decoded.type !== type) throw new Error('Invalid token type'); - return decoded; - } catch (error) { - if (error.name === 'TokenExpiredError') throw new Error(`${type} token has expired`); - if (error.name === 'JsonWebTokenError') throw new Error(`Invalid ${type} token`); - throw error; - } + const decoded = jwt.verify(token, settings.secret); + if (decoded.type !== type) throw new Error('Invalid token type'); + return decoded; } function generateAccessToken(payload) { @@ -67,8 +61,6 @@ function generateTokenPair(payload) { accessToken, refreshToken, tokenType: 'Bearer', - expiresIn: 900, - refreshExpiresIn: 604800 }; } From 1987508887d3905862fa37558e932bea2e8db909 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 2 Oct 2025 15:39:26 +0700 Subject: [PATCH 030/126] fix: cookie dev --- controllers/auth.controller.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/controllers/auth.controller.js b/controllers/auth.controller.js index 8c3b405..87425c1 100644 --- a/controllers/auth.controller.js +++ b/controllers/auth.controller.js @@ -29,8 +29,8 @@ class AuthController { // Set refresh token in cookie res.cookie('refreshToken', tokens.refreshToken, { httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'strict', + secure: false, //masih dev + sameSite: 'lax', maxAge: 7 * 24 * 60 * 60 * 1000 // 7 hari }); @@ -71,8 +71,8 @@ class AuthController { // Set refresh token in cookie res.cookie('refreshToken', tokens.refreshToken, { httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'strict', + secure: false, // masih dev + sameSite: 'lax', maxAge: 7 * 24 * 60 * 60 * 1000 // 7 hari }); @@ -98,8 +98,9 @@ class AuthController { return res.status(200).json(setResponse(result, 'Token refreshed successfully', 200)); } catch (err) { - return res.status(err.statusCode || 500).json( - setResponse(null, err.message || 'Refresh token failed', err.statusCode || 500) + const status = err.statusCode && err.statusCode < 500 ? err.statusCode : 401; + return res.status(status).json( + setResponse(null, err.message || 'Refresh token invalid', status) ); } } @@ -109,8 +110,8 @@ class AuthController { try { res.clearCookie('refreshToken', { httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'strict', + sameSite: 'none', + secure: true }); return res.status(200).json(setResponse(null, 'Logged out successfully', 200)); } catch (err) { From 8375c0c2f24b6a355b578d5b5bd1e0c77abbb387 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 2 Oct 2025 15:39:56 +0700 Subject: [PATCH 031/126] update: response error --- middleware/verifyToken.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/middleware/verifyToken.js b/middleware/verifyToken.js index ab5eb21..1e38774 100644 --- a/middleware/verifyToken.js +++ b/middleware/verifyToken.js @@ -29,10 +29,15 @@ function verifyAccessToken(req, res, next) { setUser(req, decoded); next(); } catch (error) { - if (error.name === 'TokenExpiredError' || error.name === 'JsonWebTokenError') { - return next(new ErrorHandler(401, error.message)); + if (error.name === 'TokenExpiredError') { + return next(new ErrorHandler(401, 'Access token expired')); } - next(new ErrorHandler(500, 'Authenticate verification failed')); + + if (error.name === 'JsonWebTokenError') { + return next(new ErrorHandler(401, 'Invalid access token')); + } + + return next(new ErrorHandler(500, 'Internal authentication error')); } } From fe5241a1e1c65cec197faf66dc1b5f1ae84d9bbf Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 2 Oct 2025 15:40:09 +0700 Subject: [PATCH 032/126] fix: refresh token --- services/auth.service.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/services/auth.service.js b/services/auth.service.js index 3e771f8..68fbe9b 100644 --- a/services/auth.service.js +++ b/services/auth.service.js @@ -76,17 +76,22 @@ class AuthService { // Refresh Token static async refreshToken(refreshToken) { - if (!refreshToken) { - throw new ErrorHandler(401, 'Refresh token is required'); - } + if (!refreshToken) throw new ErrorHandler(401, 'Refresh token is required'); - const decoded = JWTService.verifyRefreshToken(refreshToken); + let decoded; + try { + decoded = JWTService.verifyRefreshToken(refreshToken); + } catch (err) { + if (err.message.includes('expired')) throw new ErrorHandler(401, 'Refresh token expired'); + throw new ErrorHandler(401, 'Invalid refresh token'); + } const payload = { user_id: decoded.user_id, user_fullname: decoded.user_fullname, user_name: decoded.user_name, user_email: decoded.user_email, + user_phone: decoded.user_phone, role_id: decoded.role_id, role_name: decoded.role_name, is_sa: decoded.is_sa @@ -94,7 +99,11 @@ class AuthService { const accessToken = JWTService.generateAccessToken(payload); - return { accessToken, tokenType: 'Bearer', expiresIn: 900 }; + return { + accessToken, + tokenType: 'Bearer', + expiresIn: 900 + }; } } From 315537fc6ececf75d25c452f5b13b6605a479490 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Tue, 7 Oct 2025 13:45:38 +0700 Subject: [PATCH 033/126] add: brand db --- db/brand.db.js | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 db/brand.db.js diff --git a/db/brand.db.js b/db/brand.db.js new file mode 100644 index 0000000..4a80cf0 --- /dev/null +++ b/db/brand.db.js @@ -0,0 +1,76 @@ +const pool = require("../config"); + +// Get all brands +const getAllBrandsDb = async () => { + const queryText = ` + SELECT * + FROM m_brands + WHERE deleted_at IS NULL + ORDER BY brand_id ASC + `; + const result = await pool.query(queryText); + return result.recordset; +}; + +// Get brand by ID +const getBrandByIdDb = async (id) => { + const queryText = ` + SELECT * + FROM m_brands + WHERE brand_id = $1 + AND deleted_at IS NULL + `; + const result = await pool.query(queryText, [id]); + return result.recordset[0]; +}; + +// Get brand by name +const getBrandByNameDb = async (name) => { + const queryText = ` + SELECT * + FROM m_brands + WHERE brand_name = $1 + AND deleted_at IS NULL + `; + const result = await pool.query(queryText, [name]); + return result.recordset[0]; +}; + +// Create brand +const createBrandDb = async (data) => { + const { query: queryText, values } = pool.buildDynamicInsert("m_brands", data); + const result = await pool.query(queryText, values); + const insertedId = result.recordset[0]?.inserted_id; + if (!insertedId) return null; + + return getBrandByIdDb(insertedId); +}; + +// Update brand +const updateBrandDb = async (id, data) => { + const { query: queryText, values } = pool.buildDynamicUpdate("m_brands", data, { brand_id: id }); + await pool.query(queryText, values); + return getBrandByIdDb(id); +}; + +// Soft delete brand +const softDeleteBrandDb = async (id, deletedBy) => { + const queryText = ` + UPDATE m_brands + SET deleted_at = GETDATE(), + deleted_by = $1 + WHERE brand_id = $2 + AND deleted_at IS NULL + `; + await pool.query(queryText, [deletedBy, id]); + return true; +}; + +module.exports = { + getAllBrandsDb, + getBrandByIdDb, + getBrandByNameDb, + createBrandDb, + updateBrandDb, + softDeleteBrandDb, +}; From 857e9ecf639fdad7377687988875456645f5361d Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Tue, 7 Oct 2025 13:45:48 +0700 Subject: [PATCH 034/126] add: role db --- db/role.db.js | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 db/role.db.js diff --git a/db/role.db.js b/db/role.db.js new file mode 100644 index 0000000..e4eac2e --- /dev/null +++ b/db/role.db.js @@ -0,0 +1,59 @@ +const pool = require("../config"); + +// Get all roles +const getAllRolesDb = async () => { + const queryText = ` + SELECT * + FROM m_roles + WHERE deleted_at IS NULL + ORDER BY role_id ASC + `; + const result = await pool.query(queryText); + return result.recordset; +}; + +// Get role by ID +const getRoleByIdDb = async (roleId) => { + const queryText = ` + SELECT * + FROM m_roles + WHERE role_id = $1 AND deleted_at IS NULL + `; + const result = await pool.query(queryText, [roleId]); + return result.recordset[0]; +}; + +// Create role +const createRoleDb = async (data) => { + const { query: queryText, values } = pool.buildDynamicInsert("m_roles", data); + const result = await pool.query(queryText, values); + return result.recordset[0]?.inserted_id || null; +}; + +// Update role +const updateRoleDb = async (roleId, data) => { + const { query: queryText, values } = pool.buildDynamicUpdate("m_roles", data, { role_id: roleId }); + await pool.query(queryText, values); + return true; +}; + +// Soft delete role +const deleteRoleDb = async (roleId, deletedBy) => { + const queryText = ` + UPDATE m_roles + SET deleted_at = GETDATE(), + deleted_by = $1 + WHERE role_id = $2 + `; + await pool.query(queryText, [deletedBy, roleId]); + return true; +}; + + +module.exports = { + getAllRolesDb, + getRoleByIdDb, + createRoleDb, + updateRoleDb, + deleteRoleDb, +}; \ No newline at end of file From 3e6877ee0742674532f4174aa6c4c3f7ed7fbff1 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Tue, 7 Oct 2025 13:46:55 +0700 Subject: [PATCH 035/126] update user db --- db/user.db.js | 132 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 97 insertions(+), 35 deletions(-) diff --git a/db/user.db.js b/db/user.db.js index f2096f9..92b78cd 100644 --- a/db/user.db.js +++ b/db/user.db.js @@ -1,14 +1,32 @@ const pool = require("../config"); -// Get all users +// Get all users s const getAllUsersDb = async () => { const queryText = ` - SELECT u.user_id, u.user_fullname, u.user_name, u.user_email, u.user_phone, - u.is_active, u.created_at, u.updated_at, u.deleted_at, - u.updated_by, u.deleted_by, - r.role_id, r.role_name + SELECT + u.user_id, + u.user_fullname, + 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, + u.approved_at, + u.created_at, + u.updated_at, + 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 LEFT JOIN m_roles r ON u.role_id = r.role_id + LEFT JOIN m_users approver ON u.approved_by = approver.user_id WHERE u.deleted_at IS NULL ORDER BY u.user_id ASC `; @@ -16,27 +34,56 @@ const getAllUsersDb = async () => { return result.recordset; }; -// Get user by ID +// Get user by ID const getUserByIdDb = async (id) => { const queryText = ` - SELECT u.user_id, u.user_fullname, u.user_name, u.user_email, u.user_phone, - u.is_active, u.created_at, u.updated_at, u.deleted_at, - u.updated_by, u.deleted_by, - r.role_id, r.role_name + SELECT + u.user_id, + u.user_fullname, + 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, + u.approved_at, + u.created_at, + u.updated_at, + 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 LEFT JOIN m_roles r ON u.role_id = r.role_id + LEFT JOIN m_users approver ON u.approved_by = approver.user_id WHERE u.user_id = $1 AND u.deleted_at IS NULL `; const result = await pool.query(queryText, [id]); return result.recordset[0]; }; -// Get user by email (login) +// Get user by email const getUserByUserEmailDb = async (email) => { const queryText = ` - SELECT u.user_id, u.user_fullname, u.user_name, u.user_email, u.user_phone, - u.user_password, u.is_active, u.is_sa, - r.role_id, r.role_name + SELECT + u.user_id, + u.user_fullname, + u.user_name, + 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 LEFT JOIN m_roles r ON u.role_id = r.role_id WHERE u.user_email = $1 AND u.deleted_at IS NULL @@ -45,12 +92,23 @@ const getUserByUserEmailDb = async (email) => { return result.recordset[0]; }; -// Get user by username +// Get user by username const getUserByUsernameDb = async (username) => { const queryText = ` - SELECT u.user_id, u.user_fullname, u.user_name, u.user_email, u.user_phone, u.user_password, - u.is_active, u.role_id, - r.role_name + SELECT + u.user_id, + u.user_fullname, + u.user_name, + 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 LEFT JOIN m_roles r ON u.role_id = r.role_id WHERE u.user_name = $1 AND u.deleted_at IS NULL @@ -73,11 +131,26 @@ const updateUserDb = async (userId, data) => { return true; }; -// Change user password +// Approve user +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 queryText = ` UPDATE m_users - SET user_password = $1, updated_at = GETDATE() + SET user_password = $1, + updated_at = GETDATE() WHERE user_id = $2 AND deleted_at IS NULL `; await pool.query(queryText, [newPassword, userId]); @@ -96,25 +169,14 @@ const deleteUserDb = async (userId, deletedBy) => { return true; }; -// Get all roles -const getAllRoleDb = async () => { - const queryText = ` - SELECT role_id, role_name - FROM m_roles - ORDER BY role_id ASC - `; - const result = await pool.query(queryText); - return result.recordset; -}; - module.exports = { getAllUsersDb, getUserByIdDb, getUserByUserEmailDb, - updateUserDb, - createUserDb, - deleteUserDb, getUserByUsernameDb, + createUserDb, + updateUserDb, + approveUserDb, changeUserPasswordDb, - getAllRoleDb, + deleteUserDb, }; From 48cb3af91d1fb14e251f493a5798a594a152c7ef Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Tue, 7 Oct 2025 13:48:31 +0700 Subject: [PATCH 036/126] add verifyAccess --- middleware/verifyAcces.js | 42 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 middleware/verifyAcces.js diff --git a/middleware/verifyAcces.js b/middleware/verifyAcces.js new file mode 100644 index 0000000..e982b89 --- /dev/null +++ b/middleware/verifyAcces.js @@ -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; From ddf97842133ef1e594f02b5c5adc683060baba60 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Tue, 7 Oct 2025 15:12:27 +0700 Subject: [PATCH 037/126] update: auth --- controllers/auth.controller.js | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/controllers/auth.controller.js b/controllers/auth.controller.js index 87425c1..daff027 100644 --- a/controllers/auth.controller.js +++ b/controllers/auth.controller.js @@ -20,22 +20,31 @@ class AuthController { return res.status(400).json(setResponse(errors, 'Validation failed', 400)); } + // Convert nomor HP ke format +62 if (value.phone && value.phone.startsWith('0')) { value.phone = '+62' + value.phone.slice(1); } + // Register user baru (is_approve default 0) const { user, tokens } = await AuthService.register(value); - // Set refresh token in cookie + // Set refresh token di cookie res.cookie('refreshToken', tokens.refreshToken, { httpOnly: true, - secure: false, //masih dev + secure: false, // masih dev sameSite: 'lax', maxAge: 7 * 24 * 60 * 60 * 1000 // 7 hari }); return res.status(201).json( - setResponse({ user, accessToken: tokens.accessToken }, 'User registered successfully', 201) + setResponse( + { + user: { ...user, approved: false }, // user belum disetujui + accessToken: tokens.accessToken + }, + 'User registered successfully. Waiting for admin approval.', + 201 + ) ); } catch (err) { return res.status(err.statusCode || 500).json( @@ -68,7 +77,7 @@ class AuthController { const { user, tokens } = await AuthService.login({ email, password }); - // Set refresh token in cookie + // Set refresh token di cookie res.cookie('refreshToken', tokens.refreshToken, { httpOnly: true, secure: false, // masih dev @@ -76,8 +85,20 @@ class AuthController { maxAge: 7 * 24 * 60 * 60 * 1000 // 7 hari }); + let message = 'Login successful'; + if (!user.is_approve) { + message = 'Login successful. Limited access until approved.'; + } + return res.status(200).json( - setResponse({ user, accessToken: tokens.accessToken }, 'Login successful', 200) + setResponse( + { + user: { ...user, approved: !!user.is_approve }, + accessToken: tokens.accessToken + }, + message, + 200 + ) ); } catch (err) { return res.status(err.statusCode || 500).json( @@ -120,4 +141,4 @@ class AuthController { } } -module.exports = AuthController; +module.exports = AuthController; \ No newline at end of file From ba7f746433382f3029a83301510a794fb96ef324 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Tue, 7 Oct 2025 15:12:40 +0700 Subject: [PATCH 038/126] update: device db --- db/device.db.js | 49 +++++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/db/device.db.js b/db/device.db.js index 1b2581c..0852aff 100644 --- a/db/device.db.js +++ b/db/device.db.js @@ -1,12 +1,13 @@ const pool = require("../config"); -// Get all devices +// Get all devices with brand info const getAllDevicesDb = async () => { const queryText = ` - SELECT * - FROM m_device - WHERE deleted_at IS NULL - ORDER BY device_id ASC + SELECT d.*, b.brand_name + FROM m_device d + LEFT JOIN m_brands b ON d.brand_id = b.brand_id + WHERE d.deleted_at IS NULL + ORDER BY d.device_id ASC `; const result = await pool.query(queryText); return result.recordset; @@ -15,17 +16,19 @@ const getAllDevicesDb = async () => { // Search devices by keyword const searchDevicesDb = async (keyword) => { const queryText = ` - SELECT * - FROM m_device - WHERE deleted_at IS NULL + SELECT d.*, b.brand_name + FROM m_device d + LEFT JOIN m_brands b ON d.brand_id = b.brand_id + WHERE d.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 + '%' + d.device_name LIKE '%' + $1 + '%' + OR d.device_code LIKE '%' + $1 + '%' + OR d.device_location LIKE '%' + $1 + '%' + OR d.ip_address LIKE '%' + $1 + '%' + OR d.device_description LIKE '%' + $1 + '%' + OR b.brand_name LIKE '%' + $1 + '%' ) - ORDER BY device_id ASC + ORDER BY d.device_id ASC `; const result = await pool.query(queryText, [keyword]); return result.recordset; @@ -34,10 +37,11 @@ const searchDevicesDb = async (keyword) => { // Get device by ID const getDeviceByIdDb = async (id) => { const queryText = ` - SELECT * - FROM m_device - WHERE device_id = $1 - AND deleted_at IS NULL + SELECT d.*, b.brand_name + FROM m_device d + LEFT JOIN m_brands b ON d.brand_id = b.brand_id + WHERE d.device_id = $1 + AND d.deleted_at IS NULL `; const result = await pool.query(queryText, [id]); return result.recordset[0]; @@ -46,10 +50,11 @@ const getDeviceByIdDb = async (id) => { // Get device by device_code const getDeviceByCodeDb = async (code) => { const queryText = ` - SELECT * - FROM m_device - WHERE device_code = $1 - AND deleted_at IS NULL + SELECT d.*, b.brand_name + FROM m_device d + LEFT JOIN m_brands b ON d.brand_id = b.brand_id + WHERE d.device_code = $1 + AND d.deleted_at IS NULL `; const result = await pool.query(queryText, [code]); return result.recordset[0]; From 2eec70b7e3d2bb6d2af8c68b8544524633338fb6 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Tue, 7 Oct 2025 15:12:49 +0700 Subject: [PATCH 039/126] add verify role --- middleware/verifyAccess.js | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 middleware/verifyAccess.js diff --git a/middleware/verifyAccess.js b/middleware/verifyAccess.js new file mode 100644 index 0000000..e62fee0 --- /dev/null +++ b/middleware/verifyAccess.js @@ -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; From a632791a4d968bd9d0ff33231d93aed431db2d16 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Tue, 7 Oct 2025 15:12:54 +0700 Subject: [PATCH 040/126] delete --- middleware/verifyRole.js | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 middleware/verifyRole.js diff --git a/middleware/verifyRole.js b/middleware/verifyRole.js deleted file mode 100644 index 2bdfdb9..0000000 --- a/middleware/verifyRole.js +++ /dev/null @@ -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; From 8fca2d3cd2942e91247ff948f1c23bf070d8be4b Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Tue, 7 Oct 2025 15:14:44 +0700 Subject: [PATCH 041/126] fix: update device --- services/device.service.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/services/device.service.js b/services/device.service.js index 31a9bb3..a103836 100644 --- a/services/device.service.js +++ b/services/device.service.js @@ -28,9 +28,7 @@ class DeviceService { // Get device by code static async getDeviceByCode(code) { const device = await getDeviceByCodeDb(code); - if (!device) { - throw new ErrorHandler(404, 'Device not found'); - } + if (!device) throw new ErrorHandler(404, 'Device not found'); return device; } @@ -40,7 +38,7 @@ class DeviceService { data.created_by = userId; - // cek kode unik + // Cek kode unik const existingDevice = await getDeviceByCodeDb(data.device_code); if (existingDevice) { throw new ErrorHandler(400, 'Device code already exists'); @@ -61,14 +59,12 @@ class DeviceService { data.updated_by = userId; - const updatedDevice = await updateDeviceDb(id, data); + const updatedDevice = await updateDeviceDb(id, data); - - await updateDeviceDb(id, data); - return { + return { message: 'Device updated successfully', - data: updatedDevice, - }; + data: updatedDevice, + }; } // Soft delete device From 33e70721d9f813ad093e77be63b5a6b45f9a373f Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Tue, 7 Oct 2025 15:15:00 +0700 Subject: [PATCH 042/126] update verifyacces --- routes/device.route.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/routes/device.route.js b/routes/device.route.js index 2885ad9..96cc382 100644 --- a/routes/device.route.js +++ b/routes/device.route.js @@ -1,14 +1,14 @@ const express = require('express'); const DeviceController = require('../controllers/device.controller'); const verifyToken = require("../middleware/verifyToken") -const verifyRole = require("../middleware/verifyRole") +const verifyAccess = require("../middleware/verifyAcces") const router = express.Router(); router.get('/', verifyToken.verifyAccessToken, DeviceController.getAll); router.get('/:id', verifyToken.verifyAccessToken, DeviceController.getById); -router.post('/', verifyToken.verifyAccessToken, verifyRole([1]), DeviceController.create); -router.put('/:id', verifyToken.verifyAccessToken, verifyRole([1, 2]), DeviceController.update); -router.delete('/:id', verifyToken.verifyAccessToken, verifyRole([1]), DeviceController.delete); +router.post('/', verifyToken.verifyAccessToken, verifyAccess, DeviceController.create); +router.put('/:id', verifyToken.verifyAccessToken, verifyAccess, DeviceController.update); +router.delete('/:id', verifyToken.verifyAccessToken, verifyAccess, DeviceController.delete); module.exports = router; \ No newline at end of file From a4ef76e74ef1f29e0bf51af0d6294ad8987f04dd Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Tue, 7 Oct 2025 15:44:48 +0700 Subject: [PATCH 043/126] add: is approve validation --- controllers/auth.controller.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/controllers/auth.controller.js b/controllers/auth.controller.js index daff027..db3a316 100644 --- a/controllers/auth.controller.js +++ b/controllers/auth.controller.js @@ -20,12 +20,10 @@ class AuthController { return res.status(400).json(setResponse(errors, 'Validation failed', 400)); } - // Convert nomor HP ke format +62 if (value.phone && value.phone.startsWith('0')) { value.phone = '+62' + value.phone.slice(1); } - // Register user baru (is_approve default 0) const { user, tokens } = await AuthService.register(value); // Set refresh token di cookie @@ -39,7 +37,7 @@ class AuthController { return res.status(201).json( setResponse( { - user: { ...user, approved: false }, // user belum disetujui + user: { ...user, approved: false }, accessToken: tokens.accessToken }, 'User registered successfully. Waiting for admin approval.', @@ -77,6 +75,12 @@ class AuthController { const { user, tokens } = await AuthService.login({ email, password }); + if (!user.is_approve) { + return res.status(403).json( + setResponse(null, 'Your account has not been approved by admin yet.', 403) + ); + } + // Set refresh token di cookie res.cookie('refreshToken', tokens.refreshToken, { httpOnly: true, @@ -85,18 +89,13 @@ class AuthController { maxAge: 7 * 24 * 60 * 60 * 1000 // 7 hari }); - let message = 'Login successful'; - if (!user.is_approve) { - message = 'Login successful. Limited access until approved.'; - } - return res.status(200).json( setResponse( { - user: { ...user, approved: !!user.is_approve }, + user: { ...user, approved: true }, accessToken: tokens.accessToken }, - message, + 'Login successful', 200 ) ); @@ -141,4 +140,4 @@ class AuthController { } } -module.exports = AuthController; \ No newline at end of file +module.exports = AuthController; From 4b60f922ee587ef0aecf0a5c356d6fa4e6710a07 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Tue, 7 Oct 2025 15:45:14 +0700 Subject: [PATCH 044/126] fix: auth --- services/auth.service.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/services/auth.service.js b/services/auth.service.js index 68fbe9b..97deb43 100644 --- a/services/auth.service.js +++ b/services/auth.service.js @@ -23,7 +23,7 @@ class AuthService { user_email: email, user_phone: phone, user_password: hashedPassword, - role_id: 3, + role_id: null, is_sa: 0, is_active: 1 }); @@ -33,8 +33,7 @@ class AuthService { user_fullname: fullname, user_name: name, user_email: email, - user_phone: phone, - role_id: 3, + user_phone: phone }; // generate token pair From f9d3bd913f7d1075201842f54fc3128f7900d220 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Tue, 7 Oct 2025 23:12:49 +0700 Subject: [PATCH 045/126] fix: is_approve validations --- controllers/auth.controller.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/controllers/auth.controller.js b/controllers/auth.controller.js index db3a316..6ea3349 100644 --- a/controllers/auth.controller.js +++ b/controllers/auth.controller.js @@ -75,12 +75,6 @@ class AuthController { const { user, tokens } = await AuthService.login({ email, password }); - if (!user.is_approve) { - return res.status(403).json( - setResponse(null, 'Your account has not been approved by admin yet.', 403) - ); - } - // Set refresh token di cookie res.cookie('refreshToken', tokens.refreshToken, { httpOnly: true, From 76c5eef2f7f560056aafc10131e7a0bdab51cf1c Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 8 Oct 2025 11:30:48 +0700 Subject: [PATCH 046/126] fix: req.user, delete verifyrefreshtoken --- middleware/verifyToken.js | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/middleware/verifyToken.js b/middleware/verifyToken.js index 1e38774..020e967 100644 --- a/middleware/verifyToken.js +++ b/middleware/verifyToken.js @@ -26,7 +26,9 @@ function verifyAccessToken(req, res, next) { } const decoded = JWTService.verifyToken(token); - setUser(req, decoded); + + req.user = decoded; + next(); } catch (error) { 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 = { - verifyAccessToken, - verifyRefreshToken, + verifyAccessToken }; \ No newline at end of file From 453b5eb5af8124b8608e479de91f0b8dea0eecf2 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 8 Oct 2025 11:34:27 +0700 Subject: [PATCH 047/126] update: userdb --- db/user.db.js | 179 +++++++++++++++++++++----------------------------- 1 file changed, 76 insertions(+), 103 deletions(-) diff --git a/db/user.db.js b/db/user.db.js index 92b78cd..3776a26 100644 --- a/db/user.db.js +++ b/db/user.db.js @@ -1,159 +1,129 @@ -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 = ` SELECT - u.user_id, - u.user_fullname, - u.user_name, - u.user_email, - u.user_phone, - u.is_active, - u.is_sa, - u.is_approve, - u.approved_by, + u.user_id, u.user_fullname, 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, - u.approved_at, - u.created_at, - u.updated_at, - u.deleted_at, - u.updated_by, - u.deleted_by, - r.role_id, - r.role_name, - r.role_description, - r.role_level + u.approved_at, u.created_at, u.updated_at, 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 LEFT JOIN m_roles r ON u.role_id = r.role_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 `; - const result = await pool.query(queryText); + const result = await query(queryText, queryParams); return result.recordset; }; -// Get user by ID +// Get user by ID const getUserByIdDb = async (id) => { const queryText = ` SELECT - u.user_id, - u.user_fullname, - u.user_name, - u.user_email, - u.user_phone, - u.is_active, - u.is_sa, - u.is_approve, - u.approved_by, + u.user_id, u.user_fullname, 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, - u.approved_at, - u.created_at, - u.updated_at, - u.deleted_at, - u.updated_by, - u.deleted_by, - r.role_id, - r.role_name, - r.role_description, - r.role_level + u.approved_at, u.created_at, u.updated_at, 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 LEFT JOIN m_roles r ON u.role_id = r.role_id LEFT JOIN m_users approver ON u.approved_by = approver.user_id 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]; }; -// Get user by email +// Get user by email const getUserByUserEmailDb = async (email) => { const queryText = ` SELECT - u.user_id, - u.user_fullname, - u.user_name, - 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 + u.user_id, u.user_fullname, u.user_name, 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 LEFT JOIN m_roles r ON u.role_id = r.role_id 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]; }; -// Get user by username +// Get user by username const getUserByUsernameDb = async (username) => { const queryText = ` SELECT - u.user_id, - u.user_fullname, - u.user_name, - 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 + u.user_id, u.user_fullname, u.user_name, 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 LEFT JOIN m_roles r ON u.role_id = r.role_id 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]; }; // Create user const createUserDb = async (data) => { - const { query: queryText, values } = pool.buildDynamicInsert("m_users", data); - const result = await pool.query(queryText, values); - return result.recordset[0]?.inserted_id || null; + const queryText = ` + INSERT INTO m_users + (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 const updateUserDb = async (userId, data) => { - const { query: queryText, values } = pool.buildDynamicUpdate("m_users", data, { user_id: userId }); - await pool.query(queryText, values); + const { query: queryText, values } = buildDynamicUpdate("m_users", data, { user_id: userId }); + const finalQuery = queryText.replace("WHERE", "WHERE deleted_at IS NULL AND"); + await query(finalQuery, values); return true; }; -// Approve user -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 +// Change user password const changeUserPasswordDb = async (userId, newPassword) => { const queryText = ` UPDATE m_users - SET user_password = $1, - updated_at = GETDATE() + SET user_password = $1, updated_at = GETDATE() WHERE user_id = $2 AND deleted_at IS NULL `; - await pool.query(queryText, [newPassword, userId]); + await query(queryText, [newPassword, userId]); return true; }; @@ -161,11 +131,15 @@ const changeUserPasswordDb = async (userId, newPassword) => { const deleteUserDb = async (userId, deletedBy) => { const queryText = ` UPDATE m_users - SET deleted_at = GETDATE(), - deleted_by = $1 - WHERE user_id = $2 + SET + deleted_at = GETDATE(), + deleted_by = $1, + is_active = 0 + WHERE user_id = $2 + AND deleted_at IS NULL `; - await pool.query(queryText, [deletedBy, userId]); + + await query(queryText, [deletedBy, userId]); return true; }; @@ -176,7 +150,6 @@ module.exports = { getUserByUsernameDb, createUserDb, updateUserDb, - approveUserDb, changeUserPasswordDb, deleteUserDb, }; From 9ad16fcff73595d7bb3bcce81423c7784c69e8a2 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 8 Oct 2025 11:35:15 +0700 Subject: [PATCH 048/126] fix: user route --- routes/users.route.js | 48 +++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/routes/users.route.js b/routes/users.route.js index bc7fd21..44fbfb4 100644 --- a/routes/users.route.js +++ b/routes/users.route.js @@ -1,32 +1,30 @@ -const { - getAllUsers, - createUser, - deleteUser, - getUserById, - updateUser, - getUserProfile, - getAllRoles, - getAllStatusUsers -} = require("../controllers/users.controller"); -const router = require("express").Router(); -const verifyAdmin = require("../middleware/verifyRole"); -const verifyToken = require("../middleware/verifyToken"); +const express = require('express'); +const UserController = require('../controllers/users.controller'); +const verifyToken = require('../middleware/verifyToken'); +const verifyAccess = require('../middleware/verifyAcces'); -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(verifyToken.verifyAccessToken, getAllUsers) - .post(verifyToken.verifyAccessToken, createUser); +// Get user by ID +router.get('/:id', verifyToken.verifyAccessToken, UserController.getUserById); -router - .route("/status") - .get(verifyToken.verifyAccessToken, getAllStatusUsers); +// Create new user +router.post('/', verifyToken.verifyAccessToken, verifyAccess(), UserController.createUser); + +// 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; From 0ae39aa504702a99bb4042c3c6cdb0a02d1e6204 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 8 Oct 2025 11:35:38 +0700 Subject: [PATCH 049/126] update: user controller --- controllers/users.controller.js | 285 +++++++++++++++----------------- 1 file changed, 135 insertions(+), 150 deletions(-) diff --git a/controllers/users.controller.js b/controllers/users.controller.js index ae9548e..5581127 100644 --- a/controllers/users.controller.js +++ b/controllers/users.controller.js @@ -1,172 +1,157 @@ const userService = require("../services/user.service"); const { ErrorHandler } = require("../helpers/error"); -const { hashPassword } = require("../helpers/hashPassword"); -const { setResponse, setPaging, setResponsePaging } = require("../helpers/utils"); +const { setResponse } = require("../helpers/utils"); const Joi = require("joi"); +const { userSchema } = require("../helpers/validation"); -// Definisikan skema validasi -const validateTerm = Joi.object({ - user_fullname: Joi.string().max(255).required(), - user_name: Joi.string().max(255).required(), - user_email: Joi.string().max(255).email().allow(null), - user_password: Joi.string().max(255).required(), - role_id: Joi.number().integer().allow(null), - is_active: Joi.boolean().required() -}); - -const getAllUsers = async (req, res) => { - - const { - page = 1, - limit = 10, - fullname: userFullname, - username: userName, - is_active: isActive, - criteria, - tenantID, - } = req.query - - const offset = (page - 1) * limit; - - const filterQuery = { - fixed: { - limit, offset, tenantID - }, - filterQuery: [ - { - type: 'string', - column: 'user_fullname', - param: userFullname - }, - { - type: 'string', - column: 'user_name', - param: userName - }, - { - type: 'number', - column: 'is_active', - param: isActive - } - ], - filterCriteria: - { - criteria, - column: [ - 'user_fullname', 'user_name' - ] +class UserController { + // Get all users + static async getAllUsers(req, res) { + try { + const users = await userService.getAllUsers(); + return res.status(200).json(setResponse(users, "Users retrieved successfully", 200)); + } catch (error) { + return res + .status(error.statusCode || 500) + .json(setResponse(null, error.message, error.statusCode || 500)); } } - 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); + // 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)); + } } - 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 - }); + // Create new user + static async createUser(req, res) { + try { + const { error, value } = userSchema.validate(req.body, { abortEarly: false }); - const response = await setResponse(results); + if (error) { + const validationErrors = error.details.map((err) => err.message); + throw new ErrorHandler(400, validationErrors); + } - res.status(response.statusCode).json(response); -}; + // Kirim approved_by dari user yang bikin + const result = await userService.createUser({ + ...value, + approved_by: req.user.user_id + }); -const getUserById = async (req, res) => { - const { id } = req.params; - - 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); + 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)); + } } - const results = await userService.updateUser({ - 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, - id - }); + // Update user + static async updateUser(req, res) { + try { + const { id } = req.params; + const { + fullname, + name, + email, + phone, + role_id, + is_sa, + is_active, + is_approve + } = req.body; - const response = await setResponse(results); + const result = await userService.updateUser({ + user_id: parseInt(id, 10), + fullname, + name, + email, + phone, + role_id, + is_sa, + is_active, + is_approve + }); - res.status(response.statusCode).json(response); -}; + console.log("PARAM ID:", req.params); + console.log("BODY:", req.body); -const deleteUser = async (req, res) => { - const { id } = req.params; - const userID = req.userID + return res.status(200).json(setResponse(result, "User updated successfully", 200)); + } catch (error) { + return res + .status(error.statusCode || 500) + .json(setResponse(null, error.message, error.statusCode || 500)); + } + } - const results = await userService.deleteUser(id, userID); - const response = await setResponse(results) + // Delete user + static async deleteUser(req, res) { + try { + const { id } = req.params; + const deletedBy = req.user?.user_id; - res.status(response.statusCode).json(response); -}; + const result = await userService.deleteUser(id, deletedBy); + 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)); + } + } -const getAllRoles = async (req, res) => { - const results = await userService.getAllRoles(req.body.tenantID); - const response = await setResponse(results) + // Change user password + static async changePassword(req, res) { + try { + const { id } = req.params; + const { new_password } = req.body; - res.status(response.statusCode).json(response); -}; + if (!id || !new_password) { + throw new ErrorHandler(400, "user_id and new_password are required"); + } -module.exports = { - getAllUsers, - createUser, - getUserById, - updateUser, - deleteUser, - getUserProfile, - getAllRoles, - getAllStatusUsers -}; + 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)); + } + } + + // Get all status users + static async getAllStatusUsers(req, res) { + try { + const result = await userService.getAllStatusUsers(); + return res.status(200).json(setResponse(result, "Status list retrieved successfully", 200)); + } catch (error) { + return res + .status(error.statusCode || 500) + .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; From 58cb0c8425d86a18ed68610aa0f93c03557416bc Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 8 Oct 2025 11:35:50 +0700 Subject: [PATCH 050/126] update: user service --- services/user.service.js | 182 +++++++++++++++++++++++++-------------- 1 file changed, 117 insertions(+), 65 deletions(-) diff --git a/services/user.service.js b/services/user.service.js index 2e95fda..91d0cac 100644 --- a/services/user.service.js +++ b/services/user.service.js @@ -1,122 +1,174 @@ -const { +const { createUserDb, - changeUserPasswordDb, getUserByIdDb, - updateUserDb, - deleteUserDb, getAllUsersDb, getUserByUsernameDb, - getAllRoleDb -} = require("../db/user.db"); -const { ErrorHandler } = require("../helpers/error"); -const { convertId } = require("../helpers/utils"); + updateUserDb, + deleteUserDb, + changeUserPasswordDb +} = require('../db/user.db'); +const { hashPassword } = require('../helpers/hashPassword'); +const { ErrorHandler } = require('../helpers/error'); const statusName = [ - { - status: true, - status_name: "Aktif" - }, { - status: false, - status_name: "NonAktif" - } + { status: true, status_name: "Aktif" }, + { status: false, status_name: "NonAktif" } ]; class UserService { + // Get all status users getAllStatusUsers = async () => { try { return statusName; } 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 { - const results = await getAllUsersDb(param); + const results = await getAllUsersDb(); - results.data.map(element => { - element.is_active = element.is_active == 1 ? true : false - element.is_active_name = convertId(statusName, element.is_active, 'status', 'status_name') + results.forEach(user => { + 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; // remove password }); - return results + return results; } catch (error) { - throw new ErrorHandler(error.statusCode, 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); + throw new ErrorHandler(error.statusCode || 500, error.message); } }; + // Get user by ID getUserById = async (id) => { try { const user = await getUserByIdDb(id); - // user.password = undefined; - user.is_active = user.is_active == 1 ? true : false + if (!user) throw new ErrorHandler(404, "User not found"); + + 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; } 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 { - 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) { - throw new ErrorHandler(error.statusCode, error.message); + throw new ErrorHandler(error.statusCode || 500, error.message); } }; - updateUser = async (param) => { - const { userName, id } = param; - const errors = {}; + // Update user + updateUser = async ({ user_id, fullname, name, email, phone, role_id, is_sa, is_active, is_approve }) => { try { + const user = await getUserByIdDb(user_id); + if (!user) throw new ErrorHandler(404, "User not found"); - const user = await getUserByIdDb(id); - - const findUserByUsername = await getUserByUsernameDb(userName, param.tenantID); - - const usernameChanged = userName && user.user_name.toLowerCase() !== userName.toLowerCase(); - - if (usernameChanged && typeof findUserByUsername === "object") { - errors["username"] = "Username is already taken"; + // Cek username + if (name && user.user_name.toLowerCase() !== name.toLowerCase()) { + const userByName = await getUserByUsernameDb(name); + if (userByName) throw new ErrorHandler(400, "Username already taken"); } - if (Object.keys(errors).length > 0) { - throw new ErrorHandler(403, errors); - } + const updateData = { + ...(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) { - 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 { - 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) { - 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 { - return await getAllRoleDb(tenantID); + await deleteUserDb(userId, deletedBy); + return { message: "User deleted successfully" }; } 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); } }; } From b16e65463d9074b42e87122c358a4436409d722e Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 8 Oct 2025 11:36:39 +0700 Subject: [PATCH 051/126] add: is_approve null, validate is_approve login --- services/auth.service.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/services/auth.service.js b/services/auth.service.js index 97deb43..dde32a8 100644 --- a/services/auth.service.js +++ b/services/auth.service.js @@ -25,7 +25,10 @@ class AuthService { user_password: hashedPassword, role_id: null, is_sa: 0, - is_active: 1 + is_active: 1, + is_approve: 0, + approved_by: null, + approved_at: null }); const newUser = { @@ -58,6 +61,10 @@ class AuthService { 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 = { user_id: user.user_id, user_fullname: user.user_fullname, From 20b70edaa61d7ad668f9fe7fdbc2e2a243cc76ed Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 8 Oct 2025 11:36:56 +0700 Subject: [PATCH 052/126] add: user validation --- helpers/validation.js | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/helpers/validation.js b/helpers/validation.js index 1b6738a..149d11d 100644 --- a/helpers/validation.js +++ b/helpers/validation.js @@ -63,9 +63,38 @@ const deviceUpdateSchema = Joi.object({ }) }).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 = { registerSchema, loginSchema, deviceSchema, - deviceUpdateSchema + deviceUpdateSchema, + userSchema, }; From cf37732ebee59fd2b23830cad9b22575c7d39790 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 8 Oct 2025 11:37:19 +0700 Subject: [PATCH 053/126] add: brand.db.js --- db/brand.db.js | 72 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 53 insertions(+), 19 deletions(-) diff --git a/db/brand.db.js b/db/brand.db.js index 4a80cf0..5693955 100644 --- a/db/brand.db.js +++ b/db/brand.db.js @@ -1,24 +1,46 @@ const pool = require("../config"); // 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 = ` - SELECT * - FROM m_brands - WHERE deleted_at IS NULL - ORDER BY brand_id ASC + SELECT + b.brand_id, + b.brand_name, + 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; }; // Get brand by ID const getBrandByIdDb = async (id) => { const queryText = ` - SELECT * + SELECT + brand_id, + brand_name, + created_at, + updated_at, + deleted_at, + created_by, + updated_by, + deleted_by FROM m_brands - WHERE brand_id = $1 - AND deleted_at IS NULL + WHERE brand_id = $1 AND deleted_at IS NULL `; const result = await pool.query(queryText, [id]); return result.recordset[0]; @@ -27,10 +49,17 @@ const getBrandByIdDb = async (id) => { // Get brand by name const getBrandByNameDb = async (name) => { const queryText = ` - SELECT * + SELECT + brand_id, + brand_name, + created_at, + updated_at, + deleted_at, + created_by, + updated_by, + deleted_by FROM m_brands - WHERE brand_name = $1 - AND deleted_at IS NULL + WHERE brand_name = $1 AND deleted_at IS NULL `; const result = await pool.query(queryText, [name]); return result.recordset[0]; @@ -38,18 +67,24 @@ const getBrandByNameDb = async (name) => { // Create brand const createBrandDb = async (data) => { - const { query: queryText, values } = pool.buildDynamicInsert("m_brands", data); - const result = await pool.query(queryText, values); + const { query, values } = pool.buildDynamicInsert("m_brands", { + ...data, + created_at: new Date(), + }); + const result = await pool.query(query, values); const insertedId = result.recordset[0]?.inserted_id; if (!insertedId) return null; - return getBrandByIdDb(insertedId); }; // Update brand const updateBrandDb = async (id, data) => { - const { query: queryText, values } = pool.buildDynamicUpdate("m_brands", data, { brand_id: id }); - await pool.query(queryText, values); + const { query, values } = pool.buildDynamicUpdate( + "m_brands", + { ...data, updated_at: new Date() }, + { brand_id: id } + ); + await pool.query(query, values); return getBrandByIdDb(id); }; @@ -59,8 +94,7 @@ const softDeleteBrandDb = async (id, deletedBy) => { UPDATE m_brands SET deleted_at = GETDATE(), deleted_by = $1 - WHERE brand_id = $2 - AND deleted_at IS NULL + WHERE brand_id = $2 AND deleted_at IS NULL `; await pool.query(queryText, [deletedBy, id]); return true; From 45968832f02a4d56ff0cc0053c16f0ff9f5bc9c8 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 8 Oct 2025 11:37:33 +0700 Subject: [PATCH 054/126] add: device.db --- db/device.db.js | 64 ++++++++++++++++--------------------------------- 1 file changed, 21 insertions(+), 43 deletions(-) diff --git a/db/device.db.js b/db/device.db.js index 0852aff..d21e4a8 100644 --- a/db/device.db.js +++ b/db/device.db.js @@ -1,90 +1,69 @@ const pool = require("../config"); -// Get all devices with brand info -const getAllDevicesDb = async () => { +// Get all devices +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 = ` SELECT d.*, b.brand_name FROM m_device d LEFT JOIN m_brands b ON d.brand_id = b.brand_id - WHERE d.deleted_at IS NULL + 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; }; -// Search devices by keyword -const searchDevicesDb = async (keyword) => { - const queryText = ` - SELECT d.*, b.brand_name - FROM m_device d - LEFT JOIN m_brands b ON d.brand_id = b.brand_id - WHERE d.deleted_at IS NULL - AND ( - d.device_name LIKE '%' + $1 + '%' - OR d.device_code LIKE '%' + $1 + '%' - OR d.device_location LIKE '%' + $1 + '%' - OR d.ip_address LIKE '%' + $1 + '%' - OR d.device_description LIKE '%' + $1 + '%' - OR b.brand_name LIKE '%' + $1 + '%' - ) - ORDER BY d.device_id ASC - `; - const result = await pool.query(queryText, [keyword]); - return result.recordset; -}; - -// Get device by ID const getDeviceByIdDb = async (id) => { const queryText = ` SELECT d.*, b.brand_name FROM m_device d LEFT JOIN m_brands b ON d.brand_id = b.brand_id - WHERE d.device_id = $1 - AND d.deleted_at IS NULL + WHERE d.device_id = $1 AND d.deleted_at IS NULL `; const result = await pool.query(queryText, [id]); return result.recordset[0]; }; -// Get device by device_code const getDeviceByCodeDb = async (code) => { const queryText = ` SELECT d.*, b.brand_name FROM m_device d LEFT JOIN m_brands b ON d.brand_id = b.brand_id - WHERE d.device_code = $1 - AND d.deleted_at IS NULL + WHERE d.device_code = $1 AND d.deleted_at IS NULL `; const result = await pool.query(queryText, [code]); return result.recordset[0]; }; -// Create device const createDeviceDb = async (data) => { const { query: queryText, values } = pool.buildDynamicInsert("m_device", data); const result = await pool.query(queryText, values); const insertedId = result.recordset[0]?.inserted_id; - if (!insertedId) return null; - - return getDeviceByIdDb(insertedId); + return insertedId ? await getDeviceByIdDb(insertedId) : null; }; -// Update device const updateDeviceDb = async (id, data) => { 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); }; -// Soft delete device const softDeleteDeviceDb = async (id, deletedBy) => { const queryText = ` UPDATE m_device - SET deleted_at = GETDATE(), - deleted_by = $1 - WHERE device_id = $2 - AND deleted_at IS NULL + SET deleted_at = GETDATE(), deleted_by = $1 + WHERE device_id = $2 AND deleted_at IS NULL `; await pool.query(queryText, [deletedBy, id]); return true; @@ -97,5 +76,4 @@ module.exports = { createDeviceDb, updateDeviceDb, softDeleteDeviceDb, - searchDevicesDb, }; From a6c2e7fc7e8d3aadc7a6802f7f635af1547a3548 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 8 Oct 2025 11:37:40 +0700 Subject: [PATCH 055/126] add: role db --- db/role.db.js | 68 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/db/role.db.js b/db/role.db.js index e4eac2e..2d72533 100644 --- a/db/role.db.js +++ b/db/role.db.js @@ -1,59 +1,91 @@ const pool = require("../config"); // 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 = ` - SELECT * - FROM m_roles - WHERE deleted_at IS NULL - ORDER BY role_id ASC + SELECT + r.role_id, + r.role_name, + 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; }; // Get role by ID -const getRoleByIdDb = async (roleId) => { +const getRoleByIdDb = async (id) => { 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 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]; }; // Create role const createRoleDb = async (data) => { - const { query: queryText, values } = pool.buildDynamicInsert("m_roles", data); - const result = await pool.query(queryText, values); + const { query, values } = pool.buildDynamicInsert("m_roles", { + ...data, + created_at: new Date(), + }); + const result = await pool.query(query, values); return result.recordset[0]?.inserted_id || null; }; // Update role -const updateRoleDb = async (roleId, data) => { - const { query: queryText, values } = pool.buildDynamicUpdate("m_roles", data, { role_id: roleId }); - await pool.query(queryText, values); +const updateRoleDb = async (id, data) => { + const { query, values } = pool.buildDynamicUpdate( + "m_roles", + { ...data, updated_at: new Date() }, + { role_id: id } + ); + await pool.query(query, values); return true; }; // Soft delete role -const deleteRoleDb = async (roleId, deletedBy) => { +const deleteRoleDb = async (id, deletedBy) => { const queryText = ` UPDATE m_roles SET deleted_at = GETDATE(), deleted_by = $1 WHERE role_id = $2 `; - await pool.query(queryText, [deletedBy, roleId]); + await pool.query(queryText, [deletedBy, id]); return true; }; - module.exports = { getAllRolesDb, getRoleByIdDb, createRoleDb, updateRoleDb, deleteRoleDb, -}; \ No newline at end of file +}; From 1b384a56b53d4080b6741e7cee18a8c350266ec0 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 9 Oct 2025 03:19:13 +0700 Subject: [PATCH 056/126] fix: veryfy access --- middleware/verifyAcces.js | 42 --------------------------------------- routes/device.route.js | 6 +++--- 2 files changed, 3 insertions(+), 45 deletions(-) delete mode 100644 middleware/verifyAcces.js diff --git a/middleware/verifyAcces.js b/middleware/verifyAcces.js deleted file mode 100644 index e982b89..0000000 --- a/middleware/verifyAcces.js +++ /dev/null @@ -1,42 +0,0 @@ -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; diff --git a/routes/device.route.js b/routes/device.route.js index 96cc382..a3f1f19 100644 --- a/routes/device.route.js +++ b/routes/device.route.js @@ -7,8 +7,8 @@ const router = express.Router(); router.get('/', verifyToken.verifyAccessToken, DeviceController.getAll); router.get('/:id', verifyToken.verifyAccessToken, DeviceController.getById); -router.post('/', verifyToken.verifyAccessToken, verifyAccess, DeviceController.create); -router.put('/:id', verifyToken.verifyAccessToken, verifyAccess, DeviceController.update); -router.delete('/:id', verifyToken.verifyAccessToken, verifyAccess, DeviceController.delete); +router.post('/', verifyToken.verifyAccessToken, verifyAccess(), DeviceController.create); +router.put('/:id', verifyToken.verifyAccessToken, verifyAccess(), DeviceController.update); +router.delete('/:id', verifyToken.verifyAccessToken, verifyAccess(), DeviceController.delete); module.exports = router; \ No newline at end of file From 7d7891f6cae7ac169f854c31ab1733e3f9affb25 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 9 Oct 2025 03:19:49 +0700 Subject: [PATCH 057/126] update: captcha header --- controllers/auth.controller.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/controllers/auth.controller.js b/controllers/auth.controller.js index 6ea3349..bf9b553 100644 --- a/controllers/auth.controller.js +++ b/controllers/auth.controller.js @@ -55,6 +55,9 @@ class AuthController { static async generateCaptcha(req, res) { try { const { svg, text } = createCaptcha(); + + res.setHeader('X-Captcha-Text', text); + return res.status(200).json({ data: { svg, text } }); } catch (err) { return res.status(500).json(setResponse([], 'Captcha failed', 500)); From fdeb8eb26dc5e94d661adb1f628070e34815b4ae Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 9 Oct 2025 03:23:18 +0700 Subject: [PATCH 058/126] fix verify access --- routes/device.route.js | 2 +- routes/users.route.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/routes/device.route.js b/routes/device.route.js index a3f1f19..5411b23 100644 --- a/routes/device.route.js +++ b/routes/device.route.js @@ -1,7 +1,7 @@ const express = require('express'); const DeviceController = require('../controllers/device.controller'); const verifyToken = require("../middleware/verifyToken") -const verifyAccess = require("../middleware/verifyAcces") +const verifyAccess = require("../middleware/verifyAccess") const router = express.Router(); diff --git a/routes/users.route.js b/routes/users.route.js index 44fbfb4..5e6ab74 100644 --- a/routes/users.route.js +++ b/routes/users.route.js @@ -1,7 +1,7 @@ const express = require('express'); const UserController = require('../controllers/users.controller'); const verifyToken = require('../middleware/verifyToken'); -const verifyAccess = require('../middleware/verifyAcces'); +const verifyAccess = require('../middleware/verifyAccess'); const router = express.Router(); From 0ae29030356e88ab5fb51309ec55c5606ab17c04 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 9 Oct 2025 03:36:17 +0700 Subject: [PATCH 059/126] update: auth login with username or email --- controllers/auth.controller.js | 4 ++-- helpers/validation.js | 2 +- services/auth.service.js | 16 ++++++++++++---- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/controllers/auth.controller.js b/controllers/auth.controller.js index bf9b553..2493355 100644 --- a/controllers/auth.controller.js +++ b/controllers/auth.controller.js @@ -70,13 +70,13 @@ class AuthController { const { error, value } = loginSchema.validate(req.body, { abortEarly: false }); if (error) return res.status(400).json(setResponse([], 'Validation failed', 400)); - const { email, password, captcha, captchaText } = value; + const { identifier, password, captcha, captchaText } = value; if (!captcha || captcha.toLowerCase() !== captchaText.toLowerCase()) { return res.status(400).json(setResponse([], 'Invalid captcha', 400)); } - const { user, tokens } = await AuthService.login({ email, password }); + const { user, tokens } = await AuthService.login({ identifier, password }); // Set refresh token di cookie res.cookie('refreshToken', tokens.refreshToken, { diff --git a/helpers/validation.js b/helpers/validation.js index 149d11d..a98bb40 100644 --- a/helpers/validation.js +++ b/helpers/validation.js @@ -27,7 +27,7 @@ const registerSchema = Joi.object({ }) }); const loginSchema = Joi.object({ - email: Joi.string().email().required(), + identifier: Joi.string().required(), password: Joi.string().required(), captcha: Joi.string().required(), captchaText: Joi.string().required() diff --git a/services/auth.service.js b/services/auth.service.js index dde32a8..6f084a9 100644 --- a/services/auth.service.js +++ b/services/auth.service.js @@ -1,6 +1,7 @@ const { getUserByUserEmailDb, - createUserDb + createUserDb, + getUserByUsernameDb } = require('../db/user.db'); const { hashPassword, comparePassword } = require('../helpers/hashPassword'); const { ErrorHandler } = require('../helpers/error'); @@ -46,10 +47,17 @@ class AuthService { } // Login - static async login({ email, password }) { - const user = await getUserByUserEmailDb(email); + static async login({ identifier, password }) { + let user; + + if (identifier.includes('@')) { + user = await getUserByUserEmailDb(identifier); + } else { + user = await getUserByUsernameDb(identifier); + } + if (!user) { - throw new ErrorHandler(401, 'Invalid credentials'); + throw new ErrorHandler(401, 'Invalid credentials') } const passwordMatch = await comparePassword(password, user.user_password); From 5d27056906e423c956692f158ec0ff412422cb11 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 9 Oct 2025 03:56:48 +0700 Subject: [PATCH 060/126] update device --- controllers/device.controller.js | 5 +++-- services/device.service.js | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/controllers/device.controller.js b/controllers/device.controller.js index eeb5ef4..2e45334 100644 --- a/controllers/device.controller.js +++ b/controllers/device.controller.js @@ -43,7 +43,8 @@ class DeviceController { return res.status(400).json(setResponse(errors, 'Validation failed', 400)); } - const newDevice = await DeviceService.createDevice(value, req.user.userId); + const newDevice = await DeviceService.createDevice(value, req.user.user_id); + return res.status(201).json( setResponse(newDevice, 'Device created successfully', 201) ); @@ -69,7 +70,7 @@ class DeviceController { return res.status(400).json(setResponse(errors, 'Validation failed', 400)); } - const updatedDevice = await DeviceService.updateDevice(id, value, req.user.userId); + const updatedDevice = await DeviceService.updateDevice(id, value, req.user.user_Id); return res.status(200).json( setResponse(updatedDevice.data, 'Device updated successfully', 200) diff --git a/services/device.service.js b/services/device.service.js index a103836..b90c4ff 100644 --- a/services/device.service.js +++ b/services/device.service.js @@ -49,7 +49,7 @@ class DeviceService { } // Update device - static async updateDevice(id, data, userId) { + static async updateDevice(id, data, user_Id) { if (!data || typeof data !== 'object') data = {}; const existingDevice = await getDeviceByIdDb(id); @@ -57,7 +57,7 @@ class DeviceService { throw new ErrorHandler(404, 'Device not found'); } - data.updated_by = userId; + data.updated_by = user_Id; const updatedDevice = await updateDeviceDb(id, data); From c81e8ef22d1d772e04ed40b3e77ef85dc697183d Mon Sep 17 00:00:00 2001 From: Muhammad Afif Date: Thu, 9 Oct 2025 13:22:41 +0700 Subject: [PATCH 061/126] add : CRUD roles --- controllers/roles.controllers.js | 173 +++++++++++++++++++++++++++++++ db/role.db.js | 10 +- routes/index.js | 2 + routes/roles.route.js | 13 +++ 4 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 controllers/roles.controllers.js create mode 100644 routes/roles.route.js diff --git a/controllers/roles.controllers.js b/controllers/roles.controllers.js new file mode 100644 index 0000000..3f59d1b --- /dev/null +++ b/controllers/roles.controllers.js @@ -0,0 +1,173 @@ +const roleDb = require("../db/role.db"); +const { setResponse } = require("../helpers/utils"); + +module.exports = { + getAllRoles: async (req, res) => { + try { + const { search } = req.query; + const roles = await roleDb.getAllRolesDb(search || ''); + + return res.status(200).json(setResponse(roles, 'Roles retrieved successfully', 200)); + } catch (err) { + return res.status(err.statusCode || 500).json( + setResponse([], err.message || 'Failed to retrieve roles', err.statusCode || 500) + ); + } + }, + + getRolesById: async (req, res, next) => { + try { + const { id } = req.params; + + if (!id) { + return res.status(400).json(setResponse(null, "Role ID is required", 400)); + } + + const role = await roleDb.getRoleByIdDb(id); + + if (!role) { + return res.status(404).json(setResponse(null, "Role not found", 404)); + } + + return res.status(200).json(setResponse(role, "Role retrieved successfully", 200)); + } catch (err) { + next(err); + } + }, + + createRoles: async (req, res, next) => { + try { + let { role_name, role_description, role_level,} = req.body; + + if (!role_name || role_level === undefined || role_level === null) { + return res.status(400).json( + setResponse( + null, + "Please provide role_name and role_level", + 400 + ) + ); + } + + const level = parseInt(role_level); + if (isNaN(level)) { + return res.status(400).json( + setResponse( + null, + "role_level must be a number", + 400 + ) + ); + } + + const dataToCreate = { + role_name, + role_description, + role_level: level, + }; + + Object.keys(dataToCreate).forEach( + (key) => dataToCreate[key] === undefined && delete dataToCreate[key] + ); + + const insertedId = await roleDb.createRoleDb(dataToCreate); + + const newRole = insertedId + ? await roleDb.getRoleByIdDb(insertedId) + : { role_id: null, ...dataToCreate }; + + return res.status(201).json(setResponse(newRole, "Role has been created!", 201)); + } catch (err) { + next(err); + } + }, + + updateRoles: async (req, res, next) => { + try { + const { id } = req.params; + const { role_name, role_description, role_level } = req.body; + + if (!id) { + return res.status(400).json(setResponse(null, "Role ID is required", 400)); + } + + const dataToUpdate = {}; + if (role_name) dataToUpdate.role_name = role_name; + + if (Object.prototype.hasOwnProperty.call(req.body, "role_description")) { + dataToUpdate.role_description = role_description; + } + + if (role_level !== undefined && role_level !== null) { + const level = parseInt(role_level); + if (isNaN(level)) { + return res.status(400).json( + setResponse( + null, + "role_level must be a number", + 400 + ) + ); + } + dataToUpdate.role_level = level; + } + + if (Object.keys(dataToUpdate).length === 0) { + return res.status(400).json( + setResponse( + null, + "No valid data provided for update", + 400 + ) + ); + } + + const existingRole = await roleDb.getRoleByIdDb(id); + if (!existingRole) { + return res.status(404).json(setResponse(null, "Role not found", 404)); + } + + await roleDb.updateRoleDb(id, dataToUpdate); + + const updatedRole = await roleDb.getRoleByIdDb(id); + + return res.status(200).json( + setResponse( + updatedRole, + "Role has been updated successfully", + 200 + ) + ); + } catch (err) { + next(err); + } + }, + + deleteRoles: async (req, res, next) => { + try { + const { id } = req.params; + const deletedBy = req.user?.id || 1; + + if (!id) { + return res.status(400).json(setResponse(null, "Role ID is required", 400)); + } + + const existingRole = await roleDb.getRoleByIdDb(id); + if (!existingRole) { + return res.status(404).json(setResponse(null, "Role not found", 404)); + } + + await roleDb.deleteRoleDb(id, deletedBy); + + return res.status(200).json( + setResponse( + null, + "Role has been soft deleted successfully", + 200 + ) + ); + } catch (err) { + next(err); + } + }, +}; \ No newline at end of file diff --git a/db/role.db.js b/db/role.db.js index 2d72533..ccdfa2a 100644 --- a/db/role.db.js +++ b/db/role.db.js @@ -51,19 +51,23 @@ const getRoleByIdDb = async (id) => { // Create role const createRoleDb = async (data) => { + const roles = { ...data }; + const { query, values } = pool.buildDynamicInsert("m_roles", { - ...data, - created_at: new Date(), + ...roles, }); + const result = await pool.query(query, values); return result.recordset[0]?.inserted_id || null; }; + + // Update role const updateRoleDb = async (id, data) => { const { query, values } = pool.buildDynamicUpdate( "m_roles", - { ...data, updated_at: new Date() }, + { ...data }, { role_id: id } ); await pool.query(query, values); diff --git a/routes/index.js b/routes/index.js index f78ea07..5765b89 100644 --- a/routes/index.js +++ b/routes/index.js @@ -2,9 +2,11 @@ const router = require("express").Router(); const auth = require("./auth.route"); const users = require("./users.route"); const device = require('./device.route'); +const roles = require('./roles.route') router.use("/auth", auth); router.use("/user", users); router.use("/device", device); +router.use("/roles", roles); module.exports = router; diff --git a/routes/roles.route.js b/routes/roles.route.js new file mode 100644 index 0000000..743760a --- /dev/null +++ b/routes/roles.route.js @@ -0,0 +1,13 @@ +const express = require('express'); +const router = express.Router(); + +const { createRoles, getAllRoles, deleteRoles, updateRoles, getRolesById } = require("../controllers/roles.controllers"); + +router.post("/roles", createRoles); +router.get("/roles", getAllRoles); +router.get("/roles/:id", getRolesById) +router.delete("/roles/:id", deleteRoles); +router.put("/roles/:id", updateRoles); + + +module.exports = router; \ No newline at end of file From 9c23dbe97b87ec38db476f3d4c2f93e5b3e2c2e5 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 9 Oct 2025 08:10:20 +0700 Subject: [PATCH 062/126] add: updatedById --- controllers/users.controller.js | 4 +++- services/user.service.js | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/controllers/users.controller.js b/controllers/users.controller.js index 5581127..b8ed7ff 100644 --- a/controllers/users.controller.js +++ b/controllers/users.controller.js @@ -68,6 +68,7 @@ class UserController { is_active, is_approve } = req.body; + const updatedById = req.user?.user_id; const result = await userService.updateUser({ user_id: parseInt(id, 10), @@ -78,7 +79,8 @@ class UserController { role_id, is_sa, is_active, - is_approve + is_approve, + updatedById }); console.log("PARAM ID:", req.params); diff --git a/services/user.service.js b/services/user.service.js index 91d0cac..1017a54 100644 --- a/services/user.service.js +++ b/services/user.service.js @@ -98,7 +98,7 @@ class UserService { }; // Update user - updateUser = async ({ user_id, fullname, name, email, phone, role_id, is_sa, is_active, is_approve }) => { + updateUser = async ({ user_id, fullname, name, email, phone, role_id, is_sa, is_active, is_approve, updatedById }) => { try { const user = await getUserByIdDb(user_id); if (!user) throw new ErrorHandler(404, "User not found"); @@ -117,7 +117,8 @@ class UserService { ...(role_id !== undefined && { role_id }), ...(is_sa !== undefined && { is_sa }), ...(is_active !== undefined && { is_active }), - ...(is_approve !== undefined && { is_approve }) + ...(is_approve !== undefined && { is_approve }), + ...(updatedById !== undefined && { updated_by: updatedById }) }; await updateUserDb(user_id, updateData); From ad0f44669b0c1faf98843a68d5489cfeda7c33e0 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 9 Oct 2025 09:00:51 +0700 Subject: [PATCH 063/126] add validation new_password --- helpers/validation.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/helpers/validation.js b/helpers/validation.js index a98bb40..02a314e 100644 --- a/helpers/validation.js +++ b/helpers/validation.js @@ -32,6 +32,19 @@ const loginSchema = Joi.object({ captcha: Joi.string().required(), captchaText: Joi.string().required() }); +const newPasswordSchema = Joi.object({ + new_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}' + }) +}) // ======================== // Device Validation @@ -94,6 +107,7 @@ const userSchema = Joi.object({ module.exports = { registerSchema, loginSchema, + newPasswordSchema, deviceSchema, deviceUpdateSchema, userSchema, From 3fd4a4c1b7364cb0b6ab033b4fc6a8c7ae8aa47d Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 9 Oct 2025 09:01:08 +0700 Subject: [PATCH 064/126] fix: change password --- controllers/users.controller.js | 24 ++++++++++++++++++------ services/user.service.js | 9 +++------ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/controllers/users.controller.js b/controllers/users.controller.js index b8ed7ff..cc24712 100644 --- a/controllers/users.controller.js +++ b/controllers/users.controller.js @@ -2,7 +2,7 @@ const userService = require("../services/user.service"); const { ErrorHandler } = require("../helpers/error"); const { setResponse } = require("../helpers/utils"); const Joi = require("joi"); -const { userSchema } = require("../helpers/validation"); +const { userSchema, newPasswordSchema } = require("../helpers/validation"); class UserController { // Get all users @@ -112,14 +112,26 @@ class UserController { // Change user password static async changePassword(req, res) { try { - const { id } = req.params; - const { new_password } = req.body; + const { new_password } = req.body; + const { id } = req.params; - if (!id || !new_password) { - throw new ErrorHandler(400, "user_id and new_password are required"); + if (!id || !new_password) { + throw new ErrorHandler(400, "user_id and new_password are required"); + } + + const { error } = newPasswordSchema.validate({ new_password }); + + if (error) { + const errors = error.details.reduce((acc, cur) => { + const field = Array.isArray(cur.path) ? cur.path.join('.') : String(cur.path); + if (!acc[field]) acc[field] = []; + acc[field].push(cur.message); + return acc; + }, {}); + return res.status(400).json(setResponse(errors, 'Validation failed', 400)); } - const result = await userService.changeUserPassword(user_id, new_password); + const result = await userService.changeUserPassword(id, new_password); return res.status(200).json(setResponse(result, "Password changed successfully", 200)); } catch (error) { return res diff --git a/services/user.service.js b/services/user.service.js index 1017a54..559b4c8 100644 --- a/services/user.service.js +++ b/services/user.service.js @@ -115,9 +115,6 @@ class UserService { ...(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 }), ...(updatedById !== undefined && { updated_by: updatedById }) }; @@ -163,10 +160,10 @@ class UserService { }; // Change password - changeUserPassword = async (userId, newPassword) => { + changeUserPassword = async (user_Id, new_Password) => { try { - const hashedPassword = await hashPassword(newPassword); - await changeUserPasswordDb(userId, hashedPassword); + const hashedPassword = await hashPassword(new_Password); + await changeUserPasswordDb(user_Id, hashedPassword); return { message: "Password updated successfully" }; } catch (error) { throw new ErrorHandler(error.statusCode || 500, error.message); From 6b419495f7ac1aafa988e36c9be279fce64793cb Mon Sep 17 00:00:00 2001 From: Muhammad Afif Date: Fri, 10 Oct 2025 09:00:23 +0700 Subject: [PATCH 065/126] repair: roles controllers --- controllers/roles.controllers.js | 50 ++++++++++++-------------------- db/role.db.js | 2 +- 2 files changed, 19 insertions(+), 33 deletions(-) diff --git a/controllers/roles.controllers.js b/controllers/roles.controllers.js index 3f59d1b..aca1ff3 100644 --- a/controllers/roles.controllers.js +++ b/controllers/roles.controllers.js @@ -85,63 +85,49 @@ module.exports = { updateRoles: async (req, res, next) => { try { const { id } = req.params; - const { role_name, role_description, role_level } = req.body; - + const { role_name, role_description, role_level, updated_by } = req.body; + if (!id) { return res.status(400).json(setResponse(null, "Role ID is required", 400)); } - + const dataToUpdate = {}; + if (role_name) dataToUpdate.role_name = role_name; - + if (Object.prototype.hasOwnProperty.call(req.body, "role_description")) { dataToUpdate.role_description = role_description; } - + if (role_level !== undefined && role_level !== null) { const level = parseInt(role_level); if (isNaN(level)) { - return res.status(400).json( - setResponse( - null, - "role_level must be a number", - 400 - ) - ); + return res.status(400).json(setResponse(null, "role_level must be a number", 400)); } dataToUpdate.role_level = level; } - + + if (updated_by) dataToUpdate.updated_by = updated_by; + if (Object.keys(dataToUpdate).length === 0) { - return res.status(400).json( - setResponse( - null, - "No valid data provided for update", - 400 - ) - ); + return res.status(400).json(setResponse(null, "No valid data provided for update", 400)); } - + const existingRole = await roleDb.getRoleByIdDb(id); - if (!existingRole) { + if (!existingRole || existingRole.length === 0) { return res.status(404).json(setResponse(null, "Role not found", 404)); } - + await roleDb.updateRoleDb(id, dataToUpdate); - + const updatedRole = await roleDb.getRoleByIdDb(id); - - return res.status(200).json( - setResponse( - updatedRole, - "Role has been updated successfully", - 200 - ) - ); + + return res.status(200).json(setResponse(updatedRole, "Role has been updated successfully", 200)); } catch (err) { next(err); } }, + deleteRoles: async (req, res, next) => { try { diff --git a/db/role.db.js b/db/role.db.js index ccdfa2a..e49b110 100644 --- a/db/role.db.js +++ b/db/role.db.js @@ -46,7 +46,7 @@ const getRoleByIdDb = async (id) => { WHERE role_id = $1 AND deleted_at IS NULL `; const result = await pool.query(queryText, [id]); - return result.recordset[0]; + return result.recordset; }; // Create role From fa91ce124a4c1a91c394223212ab2f46a4735317 Mon Sep 17 00:00:00 2001 From: Muhammad Afif Date: Fri, 10 Oct 2025 09:21:20 +0700 Subject: [PATCH 066/126] add : tag db --- controllers/roles.controllers.js | 3 +- db/tag.db.js | 105 +++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 db/tag.db.js diff --git a/controllers/roles.controllers.js b/controllers/roles.controllers.js index aca1ff3..cdd8864 100644 --- a/controllers/roles.controllers.js +++ b/controllers/roles.controllers.js @@ -37,7 +37,7 @@ module.exports = { createRoles: async (req, res, next) => { try { - let { role_name, role_description, role_level,} = req.body; + let { role_name, role_description, role_level, updated_by} = req.body; if (!role_name || role_level === undefined || role_level === null) { return res.status(400).json( @@ -63,6 +63,7 @@ module.exports = { const dataToCreate = { role_name, role_description, + updated_by, role_level: level, }; diff --git a/db/tag.db.js b/db/tag.db.js new file mode 100644 index 0000000..f0d9d00 --- /dev/null +++ b/db/tag.db.js @@ -0,0 +1,105 @@ +const { query, buildFilterQuery, buildDynamicUpdate } = require("../config"); + +// Get all tags +const getAllTagsDb = async (searchParams = {}) => { + const { whereConditions, queryParams } = buildFilterQuery([ + { column: "mt.tag_name", param: searchParams.name, type: "string" }, + { column: "mt.tag_code", param: searchParams.code, type: "string" }, + { column: "md.device_name", param: searchParams.deviceName, type: "string" }, + { column: "pss.sub_section_name", param: searchParams.subSectionName, type: "string" }, + ]); + + const whereClause = whereConditions.length + ? `AND ${whereConditions.join(" AND ")}` + : ""; + + const queryText = ` + SELECT + mt.tag_id, mt.device_id, mt.tag_code, mt.tag_name, mt.tag_number, + mt.data_type, mt.unit, mt.is_active, mt.sub_section_id, + mt.created_at, mt.updated_at, mt.deleted_at, + md.device_name, + pss.sub_section_code, pss.sub_section_name + FROM m_tags mt + LEFT JOIN m_device md ON mt.device_id = md.device_id + LEFT JOIN plant_sub_section pss ON mt.sub_section_id = pss.sub_section_id + WHERE mt.deleted_at IS NULL ${whereClause} + ORDER BY mt.tag_id ASC + `; + const result = await query(queryText, queryParams); + return result.recordset; +}; + +// Get tag by ID +const getTagByIdDb = async (id) => { + const queryText = ` + SELECT + mt.tag_id, mt.device_id, mt.tag_code, mt.tag_name, mt.tag_number, + mt.data_type, mt.unit, mt.is_active, mt.sub_section_id, + mt.created_at, mt.updated_at, mt.deleted_at, + md.device_name, + pss.sub_section_code, pss.sub_section_name + FROM m_tags mt + LEFT JOIN m_device md ON mt.device_id = md.device_id + LEFT JOIN plant_sub_section pss ON mt.sub_section_id = pss.sub_section_id + WHERE mt.tag_id = $1 AND mt.deleted_at IS NULL + `; + const result = await query(queryText, [id]); + return result.recordset[0]; +}; + +// Create tag +const createTagDb = async (data) => { + const queryText = ` + INSERT INTO m_tags + (device_id, tag_code, tag_name, tag_number, data_type, unit, is_active, sub_section_id, created_by) + VALUES + ($1,$2,$3,$4,$5,$6,$7,$8,$9); + SELECT SCOPE_IDENTITY() as tag_id; + `; + + const values = [ + data.device_id, + data.tag_code, + data.tag_name, + data.tag_number || null, + data.data_type || 'A2', + data.unit || null, + data.is_active || 1, //default aktif + data.sub_section_id || null, + data.created_by || null, + ]; + + const result = await query(queryText, values); + return result.recordset[0]?.tag_id || null; +}; + +const updateTagDb = async (tagId, data) => { + const { query: queryText, values } = buildDynamicUpdate("m_tags", data, { tag_id: tagId, updated_at: 'GETDATE()' }); + const finalQuery = queryText.replace("WHERE", "WHERE deleted_at IS NULL AND"); + await query(finalQuery, values); + return true; +}; + +const deleteTagDb = async (tagId, deletedBy) => { + const queryText = ` + UPDATE m_tags + SET + deleted_at = GETDATE(), + deleted_by = $1, + is_active = 0 + WHERE tag_id = $2 + AND deleted_at IS NULL + `; + + await query(queryText, [deletedBy, tagId]); + return true; +}; + +module.exports = { + getAllTagsDb, + getTagByIdDb, + createTagDb, + updateTagDb, + deleteTagDb, +}; \ No newline at end of file From 8add2618cee7d9c7741d1d1ddbb60deb675b48de Mon Sep 17 00:00:00 2001 From: Muhammad Afif Date: Fri, 10 Oct 2025 09:23:18 +0700 Subject: [PATCH 067/126] add: tags db --- db/tag.db.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/db/tag.db.js b/db/tag.db.js index f0d9d00..0d726e0 100644 --- a/db/tag.db.js +++ b/db/tag.db.js @@ -62,16 +62,16 @@ const createTagDb = async (data) => { data.device_id, data.tag_code, data.tag_name, - data.tag_number || null, - data.data_type || 'A2', - data.unit || null, + data.tag_number, + data.data_type, + data.unit, data.is_active || 1, //default aktif - data.sub_section_id || null, - data.created_by || null, + data.sub_section_id, + data.created_by, ]; const result = await query(queryText, values); - return result.recordset[0]?.tag_id || null; + return result.recordset[0]?.tag_id; }; const updateTagDb = async (tagId, data) => { From 6eed13bc4f007397fb24948bd322bbf21a789ed9 Mon Sep 17 00:00:00 2001 From: Muhammad Afif Date: Fri, 10 Oct 2025 11:09:32 +0700 Subject: [PATCH 068/126] repair: tags db --- db/tag.db.js | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/db/tag.db.js b/db/tag.db.js index 0d726e0..ed4e825 100644 --- a/db/tag.db.js +++ b/db/tag.db.js @@ -5,8 +5,16 @@ const getAllTagsDb = async (searchParams = {}) => { const { whereConditions, queryParams } = buildFilterQuery([ { column: "mt.tag_name", param: searchParams.name, type: "string" }, { column: "mt.tag_code", param: searchParams.code, type: "string" }, - { column: "md.device_name", param: searchParams.deviceName, type: "string" }, - { column: "pss.sub_section_name", param: searchParams.subSectionName, type: "string" }, + { + column: "md.device_name", + param: searchParams.deviceName, + type: "string", + }, + { + column: "pss.sub_section_name", + param: searchParams.subSectionName, + type: "string", + }, ]); const whereClause = whereConditions.length @@ -18,11 +26,11 @@ const getAllTagsDb = async (searchParams = {}) => { mt.tag_id, mt.device_id, mt.tag_code, mt.tag_name, mt.tag_number, mt.data_type, mt.unit, mt.is_active, mt.sub_section_id, mt.created_at, mt.updated_at, mt.deleted_at, - md.device_name, + md.device_name,md.ip_address, pss.sub_section_code, pss.sub_section_name FROM m_tags mt - LEFT JOIN m_device md ON mt.device_id = md.device_id - LEFT JOIN plant_sub_section pss ON mt.sub_section_id = pss.sub_section_id + INNER JOIN m_device md ON mt.device_id = md.device_id + INNER JOIN plant_sub_section pss ON mt.sub_section_id = pss.sub_section_id WHERE mt.deleted_at IS NULL ${whereClause} ORDER BY mt.tag_id ASC `; @@ -57,7 +65,7 @@ const createTagDb = async (data) => { ($1,$2,$3,$4,$5,$6,$7,$8,$9); SELECT SCOPE_IDENTITY() as tag_id; `; - + const values = [ data.device_id, data.tag_code, @@ -67,7 +75,7 @@ const createTagDb = async (data) => { data.unit, data.is_active || 1, //default aktif data.sub_section_id, - data.created_by, + data.created_by, ]; const result = await query(queryText, values); @@ -75,7 +83,10 @@ const createTagDb = async (data) => { }; const updateTagDb = async (tagId, data) => { - const { query: queryText, values } = buildDynamicUpdate("m_tags", data, { tag_id: tagId, updated_at: 'GETDATE()' }); + const { query: queryText, values } = buildDynamicUpdate("m_tags", data, { + tag_id: tagId, + updated_at: "GETDATE()", + }); const finalQuery = queryText.replace("WHERE", "WHERE deleted_at IS NULL AND"); await query(finalQuery, values); return true; @@ -91,7 +102,7 @@ const deleteTagDb = async (tagId, deletedBy) => { WHERE tag_id = $2 AND deleted_at IS NULL `; - + await query(queryText, [deletedBy, tagId]); return true; }; @@ -102,4 +113,4 @@ module.exports = { createTagDb, updateTagDb, deleteTagDb, -}; \ No newline at end of file +}; From ab3b38eb49deb51608321ad1a6c9950e27b9ce42 Mon Sep 17 00:00:00 2001 From: Fachba Date: Fri, 10 Oct 2025 14:53:43 +0700 Subject: [PATCH 069/126] example for template master device --- config/index.js | 38 +++++++++-- controllers/device.controller.js | 113 +++++++++++-------------------- db/device.db.js | 103 ++++++++++++++++++---------- helpers/utils.js | 55 +++++++-------- helpers/validation.js | 34 +--------- index.js | 2 +- routes/device.route.js | 13 ++-- services/device.service.js | 98 ++++++++++++++------------- validate/device.schema.js | 36 ++++++++++ 9 files changed, 266 insertions(+), 226 deletions(-) create mode 100644 validate/device.schema.js diff --git a/config/index.js b/config/index.js index 8ed5673..2ee4f79 100644 --- a/config/index.js +++ b/config/index.js @@ -46,6 +46,11 @@ async function query(text, params = []) { return request.query(sqlText); } +function isValidDate(dateStr) { + const d = new Date(dateStr); + return !isNaN(d.getTime()); // true kalau valid +} + /** * Build filter query */ @@ -71,10 +76,24 @@ function buildFilterQuery(filterQuery = [], fixedParams = []) { queryParams.push(f.param ? 1 : 0); whereConditions.push(`${f.column} = $${queryParams.length}`); break; + + case 'between': + if (Array.isArray(f.param) && f.param.length === 2) { + const from = f.param[0]; + const to = f.param[1]; + if (isValidDate(from) && isValidDate(to)) { + queryParams.push(from); + queryParams.push(to); + whereConditions.push( + `${f.column} BETWEEN $${queryParams.length - 1} AND $${queryParams.length}` + ); + } + } + break; } }); - return { whereConditions, queryParams }; + return { whereConditions, whereParamAnd: queryParams }; } /** @@ -96,13 +115,17 @@ function buildStringOrIlike(columnParam, criteria, fixedParams = []) { ? `AND (${orStringConditions.join(" OR ")})` : ""; - return { whereOrConditions: whereClause, whereParam: queryParams }; + return { whereOrConditions: whereClause, whereParamOr: queryParams }; } /** * Build dynamic UPDATE */ function buildDynamicUpdate(table, data, where) { + + data.updated_by = data.userId + delete data.userId; + const setParts = []; const values = []; let index = 1; @@ -118,8 +141,8 @@ function buildDynamicUpdate(table, data, where) { throw new Error("Tidak ada kolom untuk diupdate"); } - // updated_at otomatis pakai GETDATE() - setParts.push(`updated_at = GETDATE()`); + // updated_at otomatis pakai CURRENT_TIMESTAMP + setParts.push(`updated_at = CURRENT_TIMESTAMP`); const whereParts = []; for (const [key, value] of Object.entries(where)) { @@ -140,6 +163,11 @@ function buildDynamicUpdate(table, data, where) { * Build dynamic INSERT */ function buildDynamicInsert(table, data) { + + data.created_by = data.userId + data.updated_by = data.userId + delete data.userId; + const columns = []; const placeholders = []; const values = []; @@ -159,7 +187,7 @@ function buildDynamicInsert(table, data) { // created_at & updated_at otomatis columns.push("created_at", "updated_at"); - placeholders.push("GETDATE()", "GETDATE()"); + placeholders.push("CURRENT_TIMESTAMP", "CURRENT_TIMESTAMP"); const query = ` INSERT INTO ${table} (${columns.join(", ")}) diff --git a/controllers/device.controller.js b/controllers/device.controller.js index 2e45334..7b2481c 100644 --- a/controllers/device.controller.js +++ b/controllers/device.controller.js @@ -1,101 +1,70 @@ const DeviceService = require('../services/device.service'); -const { deviceSchema, deviceUpdateSchema } = require('../helpers/validation'); -const { setResponse } = require('../helpers/utils'); +const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); +const { insertDeviceSchema, updateDeviceSchema } = require('../validate/device.schema'); class DeviceController { // Get all devices - static async getAll(req, res) { - try { - const { search } = req.query; - const devices = await DeviceService.getAllDevices(search || ''); - return res.status(200).json(setResponse(devices, 'Devices retrieved successfully', 200)); - } catch (err) { - return res.status(err.statusCode || 500).json( - setResponse([], err.message || 'Failed to retrieve devices', err.statusCode || 500) - ); - } + static async getAll(req, res) { + const queryParams = req.query; + + const results = await DeviceService.getAllDevices(queryParams); + const response = await setResponsePaging(queryParams, results, 'Device found') + + res.status(response.statusCode).json(response); } // Get device by ID static async getById(req, res) { - try { - const { id } = req.params; - const device = await DeviceService.getDeviceById(id); - return res.status(200).json(setResponse(device, 'Device retrieved successfully', 200)); - } catch (err) { - return res.status(err.statusCode || 500).json( - setResponse([], err.message || 'Device not found', err.statusCode || 500) - ); - } + const { id } = req.params; + + const results = await DeviceService.getDeviceById(id); + const response = await setResponse(results, 'Device found') + + res.status(response.statusCode).json(response); } // Create device static async create(req, res) { - try { - const { error, value } = deviceSchema.validate(req.body || {}, { abortEarly: false }); - if (error) { - const errors = error.details.reduce((acc, cur) => { - const field = Array.isArray(cur.path) ? cur.path.join('.') : String(cur.path); - if (!acc[field]) acc[field] = []; - acc[field].push(cur.message); - return acc; - }, {}); - return res.status(400).json(setResponse(errors, 'Validation failed', 400)); - } + const { error, value } = await checkValidate(insertDeviceSchema, req) - const newDevice = await DeviceService.createDevice(value, req.user.user_id); - - return res.status(201).json( - setResponse(newDevice, 'Device created successfully', 201) - ); - } catch (err) { - return res.status(err.statusCode || 500).json( - setResponse([], err.message || 'Failed to create device', err.statusCode || 500) - ); + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); } + + value.userId = req.user.user_id + + const results = await DeviceService.createDevice(value); + const response = await setResponse(results, 'Device created successfully') + + return res.status(response.statusCode).json(response); } // Update device static async update(req, res) { - try { - const { id } = req.params; - const { error, value } = deviceUpdateSchema.validate(req.body || {}, { abortEarly: false }); - if (error) { - const errors = error.details.reduce((acc, cur) => { - const field = Array.isArray(cur.path) ? cur.path.join('.') : String(cur.path); - if (!acc[field]) acc[field] = []; - acc[field].push(cur.message); - return acc; - }, {}); - return res.status(400).json(setResponse(errors, 'Validation failed', 400)); - } + const { id } = req.params; - const updatedDevice = await DeviceService.updateDevice(id, value, req.user.user_Id); + const { error, value } = checkValidate(updateDeviceSchema, req) - return res.status(200).json( - setResponse(updatedDevice.data, 'Device updated successfully', 200) - ); - } catch (err) { - return res.status(err.statusCode || 500).json( - setResponse([], err.message || 'Failed to update device', err.statusCode || 500) - ); + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); } + + value.userId = req.user.user_id + + const results = await DeviceService.updateDevice(id, value); + const response = await setResponse(results, 'Device updated successfully') + + res.status(response.statusCode).json(response); } // Soft delete device static async delete(req, res) { - try { - const { id } = req.params; + const { id } = req.params; - await DeviceService.deleteDevice(id, req.user.userId); - return res.status(200).json( - setResponse([], 'Device deleted successfully', 200) - ); - } catch (err) { - return res.status(err.statusCode || 500).json( - setResponse([], err.message || 'Failed to delete device', err.statusCode || 500) - ); - } + const results = await DeviceService.deleteDevice(id, req.user.user_id); + const response = await setResponse(results, 'Device deleted successfully') + + res.status(response.statusCode).json(response); } } diff --git a/db/device.db.js b/db/device.db.js index d21e4a8..3d770ec 100644 --- a/db/device.db.js +++ b/db/device.db.js @@ -2,67 +2,97 @@ const pool = require("../config"); // Get all devices 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 ")}` - : ""; + queryParams = [] + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1 + queryParams = [Number(searchParams.limit ?? 10), page]; + } + + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike([ + "a.device_name", + "a.device_code", + "a.device_location", + "a.ip_address", + "b.brand_name" + ], searchParams.criteria, queryParams); + + queryParams = whereParamOr ? whereParamOr : queryParams + + const { whereConditions, whereParamAnd } = pool.buildFilterQuery([ + { column: "a.device_name", param: searchParams.name, type: "string" }, + { column: "a.device_code", param: searchParams.code, type: "string" }, + { column: "a.device_location", param: searchParams.location, type: "string" }, + { column: "b.brand_name", param: searchParams.brand, type: "string" }, + ], queryParams); + + queryParams = whereParamAnd ? whereParamAnd : queryParams const queryText = ` - SELECT d.*, b.brand_name - FROM m_device d - LEFT JOIN m_brands b ON d.brand_id = b.brand_id - WHERE d.deleted_at IS NULL ${whereClause} - ORDER BY d.device_id ASC + SELECT COUNT(*) OVER() AS total_data, a.*, b.brand_name + FROM m_device a + LEFT JOIN m_brands b ON a.brand_id = b.brand_id + WHERE a.deleted_at IS NULL + ${whereConditions.length > 0 ? ` AND ${whereConditions.join(' AND ')}` : ''} + ${whereOrConditions ? whereOrConditions : ''} + ORDER BY a.device_id ASC + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ''}; `; const result = await pool.query(queryText, queryParams); - return result.recordset; + + // Menghitung total data. + const total = result?.recordset.length > 0 ? parseInt(result.recordset[0].total_data, 10) : 0; + + // Mengembalikan data dan total. + return { data: result.recordset, total }; }; const getDeviceByIdDb = async (id) => { const queryText = ` - SELECT d.*, b.brand_name - FROM m_device d - LEFT JOIN m_brands b ON d.brand_id = b.brand_id - WHERE d.device_id = $1 AND d.deleted_at IS NULL + SELECT a.*, b.brand_name + FROM m_device a + LEFT JOIN m_brands b ON a.brand_id = b.brand_id + WHERE a.device_id = $1 AND a.deleted_at IS NULL `; const result = await pool.query(queryText, [id]); - return result.recordset[0]; -}; - -const getDeviceByCodeDb = async (code) => { - const queryText = ` - SELECT d.*, b.brand_name - FROM m_device d - LEFT JOIN m_brands b ON d.brand_id = b.brand_id - WHERE d.device_code = $1 AND d.deleted_at IS NULL - `; - const result = await pool.query(queryText, [code]); - return result.recordset[0]; + return result.recordset; }; const createDeviceDb = async (data) => { - const { query: queryText, values } = pool.buildDynamicInsert("m_device", data); + + const newCode = await pool.generateKode("DVC", "m_device", "device_code") + + const store = { + ...data, + device_code: newCode, + } + + const { query: queryText, values } = pool.buildDynamicInsert("m_device", store); const result = await pool.query(queryText, values); const insertedId = result.recordset[0]?.inserted_id; return insertedId ? await getDeviceByIdDb(insertedId) : null; }; const updateDeviceDb = async (id, data) => { - const { query: queryText, values } = pool.buildDynamicUpdate("m_device", data, { device_id: id }); + + const store = { + ...data + } + + // Kondisi WHERE + const whereData = { + device_id: id + }; + + const { query: queryText, values } = pool.buildDynamicUpdate("m_device", store, whereData); await pool.query(`${queryText} AND deleted_at IS NULL`, values); return getDeviceByIdDb(id); }; -const softDeleteDeviceDb = async (id, deletedBy) => { +const deleteDeviceDb = async (id, deletedBy) => { const queryText = ` UPDATE m_device - SET deleted_at = GETDATE(), deleted_by = $1 + SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 WHERE device_id = $2 AND deleted_at IS NULL `; await pool.query(queryText, [deletedBy, id]); @@ -72,8 +102,7 @@ const softDeleteDeviceDb = async (id, deletedBy) => { module.exports = { getAllDevicesDb, getDeviceByIdDb, - getDeviceByCodeDb, createDeviceDb, updateDeviceDb, - softDeleteDeviceDb, + deleteDeviceDb, }; diff --git a/helpers/utils.js b/helpers/utils.js index 716a5cc..7ed4a3e 100644 --- a/helpers/utils.js +++ b/helpers/utils.js @@ -2,42 +2,28 @@ const setResponse = (data = null, message = "success", statusCode = 200) => { const total = Array.isArray(data) ? data.length : null; return { - data, - total, message, - statusCode + statusCode, + rows: total, + data, }; }; -const setResponsePaging = async (data = [], total, limit, page, message = "success", statusCode = 200) => { +const setResponsePaging = async (queryParam, data = [], message = "success", statusCode = 200) => { - const totalPages = Math.ceil(total / limit); + const totalPages = Math.ceil(data?.total / Number(queryParam.limit ?? 0)); const response = { message, statusCode, - data, - total: data.length, + rows: data?.data?.length, paging: { - total, - limit, - page, - page_total: totalPages - } - } - - return response -}; - -const setPaging = async (total, limit, page) => { - - const totalPages = Math.ceil(total / limit); - - const response = { - total, - limit, - page, - page_total: totalPages + current_limit: Number(queryParam.limit ?? 0), + current_page: Number(queryParam.page ?? 0), + total_limit: data?.total, + total_page: totalPages + }, + data: data?.data ?? [] } return response @@ -86,4 +72,19 @@ function orderByClauseQuery(orderParams) { return orderByClause } -module.exports = { setResponse, setResponsePaging, setPaging, convertId, formatToYYYYMMDD, orderByClauseQuery }; +const checkValidate = (validateSchema, req) => { + const { error, value } = validateSchema.validate(req.body || {}, { abortEarly: false }); + if (error) { + const errors = error.details.reduce((acc, cur) => { + const field = Array.isArray(cur.path) ? cur.path.join('.') : String(cur.path); + if (!acc[field]) acc[field] = []; + acc[field].push(cur.message); + return acc; + }, {}); + return { error: errors, value } + } + + return { error, value } +} + +module.exports = { setResponse, setResponsePaging, convertId, formatToYYYYMMDD, orderByClauseQuery, checkValidate }; diff --git a/helpers/validation.js b/helpers/validation.js index 02a314e..a1c2436 100644 --- a/helpers/validation.js +++ b/helpers/validation.js @@ -46,36 +46,6 @@ const newPasswordSchema = Joi.object({ }) }) -// ======================== -// Device Validation -// ======================== -const deviceSchema = Joi.object({ - device_code: Joi.string().max(100).required(), - device_name: Joi.string().max(100).required(), - device_status: Joi.boolean().required(), - device_location: Joi.string().max(100).required(), - device_description: Joi.string().required(), - ip_address: Joi.string() - .ip({ version: ['ipv4', 'ipv6'] }) - .required() - .messages({ - 'string.ip': 'IP address must be a valid IPv4 or IPv6 address' - }) -}); - -const deviceUpdateSchema = Joi.object({ - device_code: Joi.string().max(100), - device_name: Joi.string().max(100), - device_status: Joi.boolean(), - device_location: Joi.string().max(100), - device_description: Joi.string(), - ip_address: Joi.string() - .ip({ version: ['ipv4', 'ipv6'] }) - .messages({ - 'string.ip': 'IP address must be a valid IPv4 or IPv6 address' - }) -}).min(1); - // ======================== // Users Validation // ======================== @@ -101,14 +71,12 @@ const userSchema = Joi.object({ '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) + role_id: Joi.number().integer().min(1) }); module.exports = { registerSchema, loginSchema, newPasswordSchema, - deviceSchema, - deviceUpdateSchema, userSchema, }; diff --git a/index.js b/index.js index 552b3a5..45fc2ac 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,6 @@ const { logger } = require("./utils/logger"); const server = http.createServer(app); -const PORT = process.env.PORT || 9524; +const PORT = process.env.PORT || 9530; server.listen(PORT, () => logger.info(`Magic happening on port: ${PORT}`)); diff --git a/routes/device.route.js b/routes/device.route.js index 5411b23..5c74061 100644 --- a/routes/device.route.js +++ b/routes/device.route.js @@ -5,10 +5,13 @@ const verifyAccess = require("../middleware/verifyAccess") const router = express.Router(); -router.get('/', verifyToken.verifyAccessToken, DeviceController.getAll); -router.get('/:id', verifyToken.verifyAccessToken, DeviceController.getById); -router.post('/', verifyToken.verifyAccessToken, verifyAccess(), DeviceController.create); -router.put('/:id', verifyToken.verifyAccessToken, verifyAccess(), DeviceController.update); -router.delete('/:id', verifyToken.verifyAccessToken, verifyAccess(), DeviceController.delete); +router.route("/") + .get(verifyToken.verifyAccessToken, DeviceController.getAll) + .post(verifyToken.verifyAccessToken, verifyAccess(), DeviceController.create); + +router.route("/:id") + .get(verifyToken.verifyAccessToken, DeviceController.getById) + .put(verifyToken.verifyAccessToken, verifyAccess(), DeviceController.update) + .delete(verifyToken.verifyAccessToken, verifyAccess(), DeviceController.delete); module.exports = router; \ No newline at end of file diff --git a/services/device.service.js b/services/device.service.js index b90c4ff..978b336 100644 --- a/services/device.service.js +++ b/services/device.service.js @@ -1,81 +1,87 @@ const { getAllDevicesDb, getDeviceByIdDb, - getDeviceByCodeDb, createDeviceDb, updateDeviceDb, - softDeleteDeviceDb, - searchDevicesDb + deleteDeviceDb } = require('../db/device.db'); const { ErrorHandler } = require('../helpers/error'); class DeviceService { // Get all devices - static async getAllDevices(search) { - if (!search || search.trim() === '') { - return await getAllDevicesDb(); + static async getAllDevices(param) { + try { + const results = await getAllDevicesDb(param); + + results.data.map(element => { + }); + + return results + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); } - return await searchDevicesDb(search); } // Get device by ID static async getDeviceById(id) { - const device = await getDeviceByIdDb(id); - if (!device) throw new ErrorHandler(404, 'Device not found'); - return device; - } + try { + const result = await getDeviceByIdDb(id); - // Get device by code - static async getDeviceByCode(code) { - const device = await getDeviceByCodeDb(code); - if (!device) throw new ErrorHandler(404, 'Device not found'); - return device; + if (result.length < 1) throw new ErrorHandler(404, 'Device not found'); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } } // Create device - static async createDevice(data, userId) { - if (!data || typeof data !== 'object') data = {}; + static async createDevice(data) { + try { + if (!data || typeof data !== 'object') data = {}; - data.created_by = userId; + const result = await createDeviceDb(data); - // Cek kode unik - const existingDevice = await getDeviceByCodeDb(data.device_code); - if (existingDevice) { - throw new ErrorHandler(400, 'Device code already exists'); + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); } - - const newDevice = await createDeviceDb(data); - return newDevice; } // Update device - static async updateDevice(id, data, user_Id) { - if (!data || typeof data !== 'object') data = {}; + static async updateDevice(id, data) { + try { + if (!data || typeof data !== 'object') data = {}; - const existingDevice = await getDeviceByIdDb(id); - if (!existingDevice) { - throw new ErrorHandler(404, 'Device not found'); + const dataExist = await getDeviceByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Device not found'); + } + + const result = await updateDeviceDb(id, data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); } - - data.updated_by = user_Id; - - const updatedDevice = await updateDeviceDb(id, data); - - return { - message: 'Device updated successfully', - data: updatedDevice, - }; } // Soft delete device static async deleteDevice(id, userId) { - const existingDevice = await getDeviceByIdDb(id); - if (!existingDevice) { - throw new ErrorHandler(404, 'Device not found'); - } + try { + const dataExist = await getDeviceByIdDb(id); - await softDeleteDeviceDb(id, userId); - return { message: 'Device deleted successfully' }; + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Device not found'); + } + + const result = await deleteDeviceDb(id, userId); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } } } diff --git a/validate/device.schema.js b/validate/device.schema.js new file mode 100644 index 0000000..a8e74e5 --- /dev/null +++ b/validate/device.schema.js @@ -0,0 +1,36 @@ +// ======================== +// Device Validation + +const Joi = require("joi"); + +// ======================== +const insertDeviceSchema = Joi.object({ + device_name: Joi.string().max(100).required(), + device_status: Joi.boolean().required(), + device_location: Joi.string().max(100).required(), + device_description: Joi.string().required(), + ip_address: Joi.string() + .ip({ version: ['ipv4', 'ipv6'] }) + .required() + .messages({ + 'string.ip': 'IP address must be a valid IPv4 or IPv6 address' + }) +}); + +const updateDeviceSchema = Joi.object({ + device_name: Joi.string().max(100), + device_status: Joi.boolean(), + device_location: Joi.string().max(100), + device_description: Joi.string(), + ip_address: Joi.string() + .ip({ version: ['ipv4', 'ipv6'] }) + .messages({ + 'string.ip': 'IP address must be a valid IPv4 or IPv6 address' + }) +}).min(1); + + +// ✅ Export dengan CommonJS +module.exports = { + insertDeviceSchema, updateDeviceSchema +}; \ No newline at end of file From bdd8ec02ddb4040da6943e12ec83d7982d8bad68 Mon Sep 17 00:00:00 2001 From: Fachba Date: Fri, 10 Oct 2025 14:55:07 +0700 Subject: [PATCH 070/126] push --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 45fc2ac..0f5b6e1 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,6 @@ const { logger } = require("./utils/logger"); const server = http.createServer(app); -const PORT = process.env.PORT || 9530; +const PORT = process.env.PORT || 9533; server.listen(PORT, () => logger.info(`Magic happening on port: ${PORT}`)); From e13cb845bc654acda9303059bcab54a2e31796d2 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 10 Oct 2025 16:30:14 +0700 Subject: [PATCH 071/126] add: auth validate --- validate/auth.schema.js | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 validate/auth.schema.js diff --git a/validate/auth.schema.js b/validate/auth.schema.js new file mode 100644 index 0000000..11d834d --- /dev/null +++ b/validate/auth.schema.js @@ -0,0 +1,40 @@ +const Joi = require("joi"); + +// ======================== +// Auth Validation +// ======================== +const registerSchema = 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}' + }) +}); + +const loginSchema = Joi.object({ + identifier: Joi.string().required(), + password: Joi.string().required(), + captcha: Joi.string().required(), + captchaText: Joi.string().required() +}); + +module.exports = { + registerSchema, + loginSchema, +}; \ No newline at end of file From 82563469bd48fe361cff7e0fa46006ad40b6b95d Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 10 Oct 2025 16:30:37 +0700 Subject: [PATCH 072/126] update validate route --- controllers/auth.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controllers/auth.controller.js b/controllers/auth.controller.js index 2493355..386f1f3 100644 --- a/controllers/auth.controller.js +++ b/controllers/auth.controller.js @@ -1,5 +1,5 @@ const AuthService = require('../services/auth.service'); -const { registerSchema, loginSchema } = require('../helpers/validation'); +const { registerSchema, loginSchema } = require('../validate/auth.schema') const { setResponse } = require('../helpers/utils'); const { createCaptcha } = require('../utils/captcha'); From c5b5f2ba109667ca542c48cc33daf3265ca60214 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 10 Oct 2025 16:30:52 +0700 Subject: [PATCH 073/126] add: user validate --- validate/user.schema.js | 48 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 validate/user.schema.js diff --git a/validate/user.schema.js b/validate/user.schema.js new file mode 100644 index 0000000..98dc385 --- /dev/null +++ b/validate/user.schema.js @@ -0,0 +1,48 @@ +const Joi = require("joi"); + +// ======================== +// 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) +}); + +const newPasswordSchema = Joi.object({ + new_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}' + }) +}); + +module.exports = { + userSchema, + newPasswordSchema, +}; \ No newline at end of file From 05668b4a39cf6414bb78919b6cbe38bfbb5ff920 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 10 Oct 2025 16:31:05 +0700 Subject: [PATCH 074/126] move to folder validate --- helpers/validation.js | 82 ------------------------------------------- 1 file changed, 82 deletions(-) delete mode 100644 helpers/validation.js diff --git a/helpers/validation.js b/helpers/validation.js deleted file mode 100644 index a1c2436..0000000 --- a/helpers/validation.js +++ /dev/null @@ -1,82 +0,0 @@ -const Joi = require('joi'); - -// ======================== -// Auth Validation -// ======================== -const registerSchema = 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}' - }) -}); -const loginSchema = Joi.object({ - identifier: Joi.string().required(), - password: Joi.string().required(), - captcha: Joi.string().required(), - captchaText: Joi.string().required() -}); -const newPasswordSchema = Joi.object({ - new_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}' - }) -}) - -// ======================== -// 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 = { - registerSchema, - loginSchema, - newPasswordSchema, - userSchema, -}; From 6c7d92deaea20c75868c5f63a33afeaa4d8cad8f Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 10 Oct 2025 16:31:19 +0700 Subject: [PATCH 075/126] update: validate route --- controllers/users.controller.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/controllers/users.controller.js b/controllers/users.controller.js index cc24712..47c6d2d 100644 --- a/controllers/users.controller.js +++ b/controllers/users.controller.js @@ -1,8 +1,7 @@ const userService = require("../services/user.service"); const { ErrorHandler } = require("../helpers/error"); const { setResponse } = require("../helpers/utils"); -const Joi = require("joi"); -const { userSchema, newPasswordSchema } = require("../helpers/validation"); +const { userSchema, newPasswordSchema } = require("../validate/user.schema"); class UserController { // Get all users From c51c686ccee8dcf91caca9e6492030fd57e02c4b Mon Sep 17 00:00:00 2001 From: Muhammad Afif Date: Fri, 10 Oct 2025 17:24:32 +0700 Subject: [PATCH 076/126] add & repair: roles schema,roles service & roles router, roles controller --- controllers/roles.controller.js | 71 ++++++++++++++ controllers/roles.controllers.js | 160 ------------------------------- db/role.db.js | 95 ------------------ db/roles.db.js | 114 ++++++++++++++++++++++ routes/roles.route.js | 18 ++-- services/roles.service.js | 88 +++++++++++++++++ validate/roles.schema.js | 23 +++++ 7 files changed, 307 insertions(+), 262 deletions(-) create mode 100644 controllers/roles.controller.js delete mode 100644 controllers/roles.controllers.js delete mode 100644 db/role.db.js create mode 100644 db/roles.db.js create mode 100644 services/roles.service.js create mode 100644 validate/roles.schema.js diff --git a/controllers/roles.controller.js b/controllers/roles.controller.js new file mode 100644 index 0000000..d7ee09b --- /dev/null +++ b/controllers/roles.controller.js @@ -0,0 +1,71 @@ +const RolesService = require('../services/roles.service'); +const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); +const { updateRolesSchema, insertRolesSchema } = require('../validate/roles.schema'); + +class RolesController { + // Get all Roles + static async getAll(req, res) { + const queryParams = req.query; + + const results = await RolesService.getAllRoles(queryParams); + const response = await setResponsePaging(queryParams, results, 'Roles found') + + res.status(response.statusCode).json(response); + } + + // Get Roles by ID + static async getById(req, res) { + const { id } = req.params; + + const results = await RolesService.getRolesById(id); + const response = await setResponse(results, 'Roles found') + + res.status(response.statusCode).json(response); + } + + // Create Roles + static async create(req, res) { + const { error, value } = await checkValidate(insertRolesSchema, req) + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id + + const results = await RolesService.createRoles(value); + const response = await setResponse(results, 'Roles created successfully') + + return res.status(response.statusCode).json(response); + } + + // Update Roles + static async update(req, res) { + const { id } = req.params; + + const { error, value } = checkValidate(updateRolesSchema, req) + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id + + const results = await RolesService.updateRoles(id, value); + const response = await setResponse(results, 'Roles updated successfully') + + res.status(response.statusCode).json(response); + } + + // Soft delete Roles + static async delete(req, res) { + const { id } = req.params; + + const results = await RolesService.deleteRoles(id, req.user.user_id); + const response = await setResponse(results, 'Roles deleted successfully') + + res.status(response.statusCode).json(response); + } +} + +module.exports = RolesController; diff --git a/controllers/roles.controllers.js b/controllers/roles.controllers.js deleted file mode 100644 index cdd8864..0000000 --- a/controllers/roles.controllers.js +++ /dev/null @@ -1,160 +0,0 @@ -const roleDb = require("../db/role.db"); -const { setResponse } = require("../helpers/utils"); - -module.exports = { - getAllRoles: async (req, res) => { - try { - const { search } = req.query; - const roles = await roleDb.getAllRolesDb(search || ''); - - return res.status(200).json(setResponse(roles, 'Roles retrieved successfully', 200)); - } catch (err) { - return res.status(err.statusCode || 500).json( - setResponse([], err.message || 'Failed to retrieve roles', err.statusCode || 500) - ); - } - }, - - getRolesById: async (req, res, next) => { - try { - const { id } = req.params; - - if (!id) { - return res.status(400).json(setResponse(null, "Role ID is required", 400)); - } - - const role = await roleDb.getRoleByIdDb(id); - - if (!role) { - return res.status(404).json(setResponse(null, "Role not found", 404)); - } - - return res.status(200).json(setResponse(role, "Role retrieved successfully", 200)); - } catch (err) { - next(err); - } - }, - - createRoles: async (req, res, next) => { - try { - let { role_name, role_description, role_level, updated_by} = req.body; - - if (!role_name || role_level === undefined || role_level === null) { - return res.status(400).json( - setResponse( - null, - "Please provide role_name and role_level", - 400 - ) - ); - } - - const level = parseInt(role_level); - if (isNaN(level)) { - return res.status(400).json( - setResponse( - null, - "role_level must be a number", - 400 - ) - ); - } - - const dataToCreate = { - role_name, - role_description, - updated_by, - role_level: level, - }; - - Object.keys(dataToCreate).forEach( - (key) => dataToCreate[key] === undefined && delete dataToCreate[key] - ); - - const insertedId = await roleDb.createRoleDb(dataToCreate); - - const newRole = insertedId - ? await roleDb.getRoleByIdDb(insertedId) - : { role_id: null, ...dataToCreate }; - - return res.status(201).json(setResponse(newRole, "Role has been created!", 201)); - } catch (err) { - next(err); - } - }, - - updateRoles: async (req, res, next) => { - try { - const { id } = req.params; - const { role_name, role_description, role_level, updated_by } = req.body; - - if (!id) { - return res.status(400).json(setResponse(null, "Role ID is required", 400)); - } - - const dataToUpdate = {}; - - if (role_name) dataToUpdate.role_name = role_name; - - if (Object.prototype.hasOwnProperty.call(req.body, "role_description")) { - dataToUpdate.role_description = role_description; - } - - if (role_level !== undefined && role_level !== null) { - const level = parseInt(role_level); - if (isNaN(level)) { - return res.status(400).json(setResponse(null, "role_level must be a number", 400)); - } - dataToUpdate.role_level = level; - } - - if (updated_by) dataToUpdate.updated_by = updated_by; - - if (Object.keys(dataToUpdate).length === 0) { - return res.status(400).json(setResponse(null, "No valid data provided for update", 400)); - } - - const existingRole = await roleDb.getRoleByIdDb(id); - if (!existingRole || existingRole.length === 0) { - return res.status(404).json(setResponse(null, "Role not found", 404)); - } - - await roleDb.updateRoleDb(id, dataToUpdate); - - const updatedRole = await roleDb.getRoleByIdDb(id); - - return res.status(200).json(setResponse(updatedRole, "Role has been updated successfully", 200)); - } catch (err) { - next(err); - } - }, - - - deleteRoles: async (req, res, next) => { - try { - const { id } = req.params; - const deletedBy = req.user?.id || 1; - - if (!id) { - return res.status(400).json(setResponse(null, "Role ID is required", 400)); - } - - const existingRole = await roleDb.getRoleByIdDb(id); - if (!existingRole) { - return res.status(404).json(setResponse(null, "Role not found", 404)); - } - - await roleDb.deleteRoleDb(id, deletedBy); - - return res.status(200).json( - setResponse( - null, - "Role has been soft deleted successfully", - 200 - ) - ); - } catch (err) { - next(err); - } - }, -}; \ No newline at end of file diff --git a/db/role.db.js b/db/role.db.js deleted file mode 100644 index e49b110..0000000 --- a/db/role.db.js +++ /dev/null @@ -1,95 +0,0 @@ -const pool = require("../config"); - -// Get all roles -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 = ` - SELECT - r.role_id, - r.role_name, - 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, queryParams); - return result.recordset; -}; - -// Get role by ID -const getRoleByIdDb = async (id) => { - const queryText = ` - SELECT - role_id, - role_name, - role_description, - role_level, - created_at, - updated_at, - updated_by, - deleted_at, - deleted_by - FROM m_roles - WHERE role_id = $1 AND deleted_at IS NULL - `; - const result = await pool.query(queryText, [id]); - return result.recordset; -}; - -// Create role -const createRoleDb = async (data) => { - const roles = { ...data }; - - const { query, values } = pool.buildDynamicInsert("m_roles", { - ...roles, - }); - - const result = await pool.query(query, values); - return result.recordset[0]?.inserted_id || null; -}; - - - -// Update role -const updateRoleDb = async (id, data) => { - const { query, values } = pool.buildDynamicUpdate( - "m_roles", - { ...data }, - { role_id: id } - ); - await pool.query(query, values); - return true; -}; - -// Soft delete role -const deleteRoleDb = async (id, deletedBy) => { - const queryText = ` - UPDATE m_roles - SET deleted_at = GETDATE(), - deleted_by = $1 - WHERE role_id = $2 - `; - await pool.query(queryText, [deletedBy, id]); - return true; -}; - -module.exports = { - getAllRolesDb, - getRoleByIdDb, - createRoleDb, - updateRoleDb, - deleteRoleDb, -}; diff --git a/db/roles.db.js b/db/roles.db.js new file mode 100644 index 0000000..82215e8 --- /dev/null +++ b/db/roles.db.js @@ -0,0 +1,114 @@ +const pool = require("../config"); + +// Get all roles +const getAllRolesDb = async (searchParams = {}) => { + let queryParams = []; + + // Pagination + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1; + queryParams = [Number(searchParams.limit ?? 10), page]; + } + + // Filtering + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [ + { column: "r.role_name", param: searchParams.role_name, type: "string" }, + { column: "r.role_level", param: searchParams.role_level, type: "number" }, + ], + queryParams + ); + + queryParams = whereParamAnd ? whereParamAnd : queryParams; + + const queryText = ` + SELECT + COUNT(*) OVER() AS total_data, + r.role_id, + r.role_name, + r.role_description, + r.role_level, + r.created_at, + r.updated_at, + r.updated_by, + r.deleted_at, + r.deleted_by, + r.created_by + FROM m_roles r + WHERE r.deleted_at IS NULL + ${whereConditions.length > 0 ? `AND ${whereConditions.join(" AND ")}` : ""} + ORDER BY r.role_id ASC + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ''}; + `; + + const result = await pool.query(queryText, queryParams); + + const total = result?.recordset.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; + + return { data: result.recordset, total }; +}; + +// Get role by ID +const getRolesByIdDb = async (id) => { + const queryText = ` + SELECT + r.role_id, + r.role_name, + r.role_description, + r.role_level, + r.created_at, + r.updated_at, + r.updated_by, + r.deleted_at, + r.deleted_by, + r.created_by + FROM m_roles r + WHERE r.role_id = $1 AND r.deleted_at IS NULL + `; + const result = await pool.query(queryText, [id]); + return result.recordset; +}; + +// Create role +const createRolesDB = async (data) => { + const store = { ...data }; + + const { query: queryText, values } = pool.buildDynamicInsert("m_roles", store); + const result = await pool.query(queryText, values); + + const insertedId = result.recordset[0]?.inserted_id; + return insertedId ? await getRolesByIdDb(insertedId) : null; +}; + +// Update role +const updateRolesDb = async (id, data) => { + const store = { ...data }; + const whereData = { role_id: id }; + + const { query: queryText, values } = pool.buildDynamicUpdate("m_roles", store, whereData); + await pool.query(`${queryText} AND deleted_at IS NULL`, values); + + return getRolesByIdDb(id); +}; + +// Soft delete role +const deleteRolesDb = async (id, deletedBy) => { + const queryText = ` + UPDATE m_roles + SET deleted_at = CURRENT_TIMESTAMP, + deleted_by = $1 + WHERE role_id = $2 AND deleted_at IS NULL + `; + await pool.query(queryText, [deletedBy, id]); + return true; +}; + +module.exports = { + getAllRolesDb, + getRolesByIdDb, + createRolesDB, + updateRolesDb, + deleteRolesDb, +}; diff --git a/routes/roles.route.js b/routes/roles.route.js index 743760a..44094ed 100644 --- a/routes/roles.route.js +++ b/routes/roles.route.js @@ -1,13 +1,17 @@ const express = require('express'); +const Rolesontroller = require('../controllers/roles.controller'); +const verifyToken = require("../middleware/verifyToken") +const verifyAccess = require("../middleware/verifyAccess") + const router = express.Router(); -const { createRoles, getAllRoles, deleteRoles, updateRoles, getRolesById } = require("../controllers/roles.controllers"); - -router.post("/roles", createRoles); -router.get("/roles", getAllRoles); -router.get("/roles/:id", getRolesById) -router.delete("/roles/:id", deleteRoles); -router.put("/roles/:id", updateRoles); +router.route("/") + .get(verifyToken.verifyAccessToken, Rolesontroller.getAll) + .post(verifyToken.verifyAccessToken, verifyAccess(), Rolesontroller.create); +router.route("/:id") + .get(verifyToken.verifyAccessToken, Rolesontroller.getById) + .put(verifyToken.verifyAccessToken, verifyAccess(), Rolesontroller.update) + .delete(verifyToken.verifyAccessToken, verifyAccess(), Rolesontroller.delete); module.exports = router; \ No newline at end of file diff --git a/services/roles.service.js b/services/roles.service.js new file mode 100644 index 0000000..db73834 --- /dev/null +++ b/services/roles.service.js @@ -0,0 +1,88 @@ +const { + getAllRolesDb, + getRolesByIdDb, + createRolesDB, + updateRolesDb, + deleteRolesDb +} = require('../db/roles.db'); +const { ErrorHandler } = require('../helpers/error'); + +class RolesService { + // Get all Roles + static async getAllRoles(param) { + try { + const results = await getAllRolesDb(param); + + results.data.map(element => { + }); + + return results + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Get Roles by ID + static async getRolesById(id) { + try { + const result = await getRolesByIdDb(id); + + if (result.length < 1) throw new ErrorHandler(404, 'Roles not found'); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Create Roles + static async createRoles(data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const result = await createRolesDB(data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Update Roles + static async updateRoles(id, data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const dataExist = await getRolesByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Roles not found'); + } + + const result = await updateRolesDb(id, data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Soft delete Roles + static async deleteRoles(id, userId) { + try { + const dataExist = await getRolesByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Roles not found'); + } + + const result = await deleteRolesDb(id, userId); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } +} + +module.exports = RolesService; diff --git a/validate/roles.schema.js b/validate/roles.schema.js new file mode 100644 index 0000000..f8bb585 --- /dev/null +++ b/validate/roles.schema.js @@ -0,0 +1,23 @@ +// ======================== +// Device Validation + +const Joi = require("joi"); + +// ======================== +const insertRolesSchema = Joi.object({ + role_name: Joi.string().max(100).required(), + role_level: Joi.number().required(), + role_description: Joi.string().max(100).required(), +}); + +const updateRolesSchema = Joi.object({ + role_name: Joi.string().max(100), + role_level: Joi.number(), + role_description: Joi.string().max(100), +}).min(1); + + +// ✅ Export dengan CommonJS +module.exports = { + insertRolesSchema, updateRolesSchema +}; \ No newline at end of file From ee303081127fafff53d59727ceaabc3a34a22f47 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 10 Oct 2025 19:49:48 +0700 Subject: [PATCH 077/126] update crud user --- controllers/users.controller.js | 198 +++++++++++--------------------- db/user.db.js | 113 ++++++++++-------- routes/users.route.js | 29 ++--- 3 files changed, 141 insertions(+), 199 deletions(-) diff --git a/controllers/users.controller.js b/controllers/users.controller.js index 47c6d2d..328ea2f 100644 --- a/controllers/users.controller.js +++ b/controllers/users.controller.js @@ -1,169 +1,103 @@ -const userService = require("../services/user.service"); -const { ErrorHandler } = require("../helpers/error"); -const { setResponse } = require("../helpers/utils"); -const { userSchema, newPasswordSchema } = require("../validate/user.schema"); +const UserService = require('../services/user.service'); +const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); +const { userSchema, newPasswordSchema } = require('../validate/user.schema'); class UserController { // Get all users - static async getAllUsers(req, res) { - try { - const users = await userService.getAllUsers(); - return res.status(200).json(setResponse(users, "Users retrieved successfully", 200)); - } catch (error) { - return res - .status(error.statusCode || 500) - .json(setResponse(null, error.message, error.statusCode || 500)); - } + static async getAll(req, res) { + const queryParams = req.query; + + const results = await UserService.getAllUsers(queryParams); + const response = await setResponsePaging(queryParams, results, 'Users retrieved successfully'); + + res.status(response.statusCode).json(response); } // 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)); - } + static async getById(req, res) { + const { id } = req.params; + + const results = await UserService.getUserById(id); + const response = await setResponse(results, 'User retrieved successfully'); + + res.status(response.statusCode).json(response); } - // Create new user - static async createUser(req, res) { - try { - const { error, value } = userSchema.validate(req.body, { abortEarly: false }); + // Create user + static async create(req, res) { + const { error, value } = await checkValidate(userSchema, req); - 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)); + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); } + + value.approved_by = req.user.user_id; + + const results = await UserService.createUser(value); + const response = await setResponse(results, 'User created successfully'); + + res.status(response.statusCode).json(response); } // Update user - static async updateUser(req, res) { - try { - const { id } = req.params; - const { - fullname, - name, - email, - phone, - role_id, - is_sa, - is_active, - is_approve - } = req.body; - const updatedById = req.user?.user_id; + static async update(req, res) { + const { id } = req.params; + const { error, value } = await checkValidate(userSchema, req); - const result = await userService.updateUser({ - user_id: parseInt(id, 10), - fullname, - name, - email, - phone, - role_id, - is_sa, - is_active, - is_approve, - updatedById - }); - - console.log("PARAM ID:", req.params); - console.log("BODY:", req.body); - - return res.status(200).json(setResponse(result, "User updated successfully", 200)); - } catch (error) { - return res - .status(error.statusCode || 500) - .json(setResponse(null, error.message, error.statusCode || 500)); + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); } + + value.updated_by = req.user.user_id; + + const results = await UserService.updateUser(id, value); + const response = await setResponse(results, 'User updated successfully'); + + res.status(response.statusCode).json(response); } - // Delete user - static async deleteUser(req, res) { - try { - const { id } = req.params; - const deletedBy = req.user?.user_id; + // Soft delete user + static async delete(req, res) { + const { id } = req.params; - const result = await userService.deleteUser(id, deletedBy); - 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)); - } + const results = await UserService.deleteUser(id, req.user.user_id); + const response = await setResponse(results, 'User deleted successfully'); + + res.status(response.statusCode).json(response); } // Change user password static async changePassword(req, res) { - try { - const { new_password } = req.body; const { id } = req.params; + const { error, value } = await checkValidate(newPasswordSchema, req); - if (!id || !new_password) { - throw new ErrorHandler(400, "user_id and new_password are required"); + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); } - const { error } = newPasswordSchema.validate({ new_password }); + const results = await UserService.changeUserPassword(id, value.new_password); + const response = await setResponse(results, 'Password changed successfully'); - if (error) { - const errors = error.details.reduce((acc, cur) => { - const field = Array.isArray(cur.path) ? cur.path.join('.') : String(cur.path); - if (!acc[field]) acc[field] = []; - acc[field].push(cur.message); - return acc; - }, {}); - return res.status(400).json(setResponse(errors, 'Validation failed', 400)); - } - - const result = await userService.changeUserPassword(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)); - } + res.status(response.statusCode).json(response); } // Get all status users - static async getAllStatusUsers(req, res) { - try { - const result = await userService.getAllStatusUsers(); - return res.status(200).json(setResponse(result, "Status list retrieved successfully", 200)); - } catch (error) { - return res - .status(error.statusCode || 500) - .json(setResponse(null, error.message, error.statusCode || 500)); - } + static async getAllStatus(req, res) { + const results = await UserService.getAllStatusUsers(); + const response = await setResponse(results, 'Status list retrieved successfully'); + + res.status(response.statusCode).json(response); } // Approve user - static async approveUser(req, res) { - try { - const { id } = req.params; - const approverId = req.user?.user_id || null; + static async approve(req, res) { + 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)); - } + const results = await UserService.approveUser(id, approverId); + const response = await setResponse(results, 'User approved successfully'); + + res.status(response.statusCode).json(response); } } diff --git a/db/user.db.js b/db/user.db.js index 3776a26..bc77252 100644 --- a/db/user.db.js +++ b/db/user.db.js @@ -1,20 +1,45 @@ -const { query, buildFilterQuery, buildDynamicUpdate } = require("../config"); +const pool = 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" }, - ]); + let queryParams = []; - const whereClause = whereConditions.length - ? `AND ${whereConditions.join(" AND ")}` - : ""; + // Pagination + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1; + queryParams = [Number(searchParams.limit ?? 10), page]; + } + + // Search + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + [ + "u.user_fullname", + "u.user_name", + "u.user_email", + "r.role_name" + ], + searchParams.criteria, + queryParams + ); + + queryParams = whereParamOr ? whereParamOr : queryParams; + + // Filter + const { whereConditions, whereParamAnd } = pool.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" }, + ], + queryParams + ); + + queryParams = whereParamAnd ? whereParamAnd : queryParams; const queryText = ` SELECT + COUNT(*) OVER() AS total_data, u.user_id, u.user_fullname, 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, @@ -24,11 +49,21 @@ const getAllUsersDb = async (searchParams = {}) => { FROM m_users u LEFT JOIN m_roles r ON u.role_id = r.role_id LEFT JOIN m_users approver ON u.approved_by = approver.user_id - WHERE u.deleted_at IS NULL ${whereClause} + WHERE u.deleted_at IS NULL + ${whereConditions.length > 0 ? ` AND ${whereConditions.join(' AND ')}` : ''} + ${whereOrConditions ? whereOrConditions : ''} ORDER BY u.user_id ASC + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ''}; `; - const result = await query(queryText, queryParams); - return result.recordset; + + const result = await pool.query(queryText, queryParams); + + const total = + result?.recordset.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; + + return { data: result.recordset, total }; }; // Get user by ID @@ -46,7 +81,7 @@ const getUserByIdDb = async (id) => { LEFT JOIN m_users approver ON u.approved_by = approver.user_id WHERE u.user_id = $1 AND u.deleted_at IS NULL `; - const result = await query(queryText, [id]); + const result = await pool.query(queryText, [id]); return result.recordset[0]; }; @@ -61,7 +96,7 @@ const getUserByUserEmailDb = async (email) => { LEFT JOIN m_roles r ON u.role_id = r.role_id WHERE u.user_email = $1 AND u.deleted_at IS NULL `; - const result = await query(queryText, [email]); + const result = await pool.query(queryText, [email]); return result.recordset[0]; }; @@ -76,54 +111,35 @@ const getUserByUsernameDb = async (username) => { LEFT JOIN m_roles r ON u.role_id = r.role_id WHERE u.user_name = $1 AND u.deleted_at IS NULL `; - const result = await query(queryText, [username]); + const result = await pool.query(queryText, [username]); return result.recordset[0]; }; // Create user const createUserDb = async (data) => { - const queryText = ` - INSERT INTO m_users - (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; + const { query: queryText, values } = pool.buildDynamicInsert("m_users", data); + const result = await pool.query(queryText, values); + const insertedId = result.recordset[0]?.inserted_id; + return insertedId ? await getUserByIdDb(insertedId) : null; }; // Update user const updateUserDb = async (userId, data) => { - const { query: queryText, values } = buildDynamicUpdate("m_users", data, { user_id: userId }); - const finalQuery = queryText.replace("WHERE", "WHERE deleted_at IS NULL AND"); - await query(finalQuery, values); - return true; + const { query: queryText, values } = pool.buildDynamicUpdate("m_users", data, { + user_id: userId, + }); + await pool.query(`${queryText} AND deleted_at IS NULL`, values); + return getUserByIdDb(userId); }; // Change user password const changeUserPasswordDb = async (userId, newPassword) => { const queryText = ` UPDATE m_users - SET user_password = $1, updated_at = GETDATE() + SET user_password = $1, updated_at = CURRENT_TIMESTAMP WHERE user_id = $2 AND deleted_at IS NULL `; - await query(queryText, [newPassword, userId]); + await pool.query(queryText, [newPassword, userId]); return true; }; @@ -132,14 +148,13 @@ const deleteUserDb = async (userId, deletedBy) => { const queryText = ` UPDATE m_users SET - deleted_at = GETDATE(), + deleted_at = CURRENT_TIMESTAMP, deleted_by = $1, is_active = 0 WHERE user_id = $2 AND deleted_at IS NULL `; - - await query(queryText, [deletedBy, userId]); + await pool.query(queryText, [deletedBy, userId]); return true; }; diff --git a/routes/users.route.js b/routes/users.route.js index 5e6ab74..ff0a02b 100644 --- a/routes/users.route.js +++ b/routes/users.route.js @@ -5,26 +5,19 @@ const verifyAccess = require('../middleware/verifyAccess'); const router = express.Router(); -// Get all users -router.get('/', verifyToken.verifyAccessToken, UserController.getAllUsers); +router.route('/') + .get(verifyToken.verifyAccessToken, UserController.getAllUsers) + .post(verifyToken.verifyAccessToken, verifyAccess(), UserController.createUser); -// Get user by ID -router.get('/:id', verifyToken.verifyAccessToken, UserController.getUserById); +router.route('/:id') + .get(verifyToken.verifyAccessToken, UserController.getUserById) + .put(verifyToken.verifyAccessToken, verifyAccess(), UserController.updateUser) + .delete(verifyToken.verifyAccessToken, verifyAccess(), UserController.deleteUser); -// Create new user -router.post('/', verifyToken.verifyAccessToken, verifyAccess(), UserController.createUser); - -// 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('/change-password/:id') + .put(verifyToken.verifyAccessToken, verifyAccess(), UserController.changePassword); +router.route('/:id/approve') + .put(verifyToken.verifyAccessToken, verifyAccess(), UserController.approveUser); module.exports = router; From 425b1ed5540d7e3aa006ce9ac4bc0eae0a8e8112 Mon Sep 17 00:00:00 2001 From: Muhammad Afif Date: Fri, 10 Oct 2025 20:06:56 +0700 Subject: [PATCH 078/126] add: CRUD tags --- controllers/tags.controller.js | 71 ++++++++++++++++ db/tag.db.js | 116 -------------------------- db/tags.db.js | 144 +++++++++++++++++++++++++++++++++ routes/index.js | 2 + routes/roles.route.js | 12 +-- routes/tags.route.js | 17 ++++ services/tags.service.js | 89 ++++++++++++++++++++ validate/tags.schema.js | 29 +++++++ 8 files changed, 358 insertions(+), 122 deletions(-) create mode 100644 controllers/tags.controller.js delete mode 100644 db/tag.db.js create mode 100644 db/tags.db.js create mode 100644 routes/tags.route.js create mode 100644 services/tags.service.js create mode 100644 validate/tags.schema.js diff --git a/controllers/tags.controller.js b/controllers/tags.controller.js new file mode 100644 index 0000000..fa3dc16 --- /dev/null +++ b/controllers/tags.controller.js @@ -0,0 +1,71 @@ +const TagsService = require('../services/tags.service'); +const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); +const { insertTagsSchema, updateTagsSchema } = require('../validate/tags.schema'); + +class TagsController { + // Get all devices + static async getAll(req, res) { + const queryParams = req.query; + + const results = await TagsService.getAllTags(queryParams); + const response = await setResponsePaging(queryParams, results, 'Tags found') + + res.status(response.statusCode).json(response); + } + + // Get device by ID + static async getById(req, res) { + const { id } = req.params; + + const results = await TagsService.getTagByID(id); + const response = await setResponse(results, 'Tags found') + + res.status(response.statusCode).json(response); + } + + // Create device + static async create(req, res) { + const { error, value } = await checkValidate(insertTagsSchema, req) + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id + + const results = await TagsService.createTags(value); + const response = await setResponse(results, 'Tags created successfully') + + return res.status(response.statusCode).json(response); + } + + // Update device + static async update(req, res) { + const { id } = req.params; + + const { error, value } = checkValidate(updateTagsSchema, req) + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id + + const results = await TagsService.updateTags(id, value); + const response = await setResponse(results, 'Tags updated successfully') + + res.status(response.statusCode).json(response); + } + + // Soft delete device + static async delete(req, res) { + const { id } = req.params; + + const results = await TagsService.deleteTags(id, req.user.user_id); + const response = await setResponse(results, 'Tags deleted successfully') + + res.status(response.statusCode).json(response); + } +} + +module.exports = TagsController; diff --git a/db/tag.db.js b/db/tag.db.js deleted file mode 100644 index ed4e825..0000000 --- a/db/tag.db.js +++ /dev/null @@ -1,116 +0,0 @@ -const { query, buildFilterQuery, buildDynamicUpdate } = require("../config"); - -// Get all tags -const getAllTagsDb = async (searchParams = {}) => { - const { whereConditions, queryParams } = buildFilterQuery([ - { column: "mt.tag_name", param: searchParams.name, type: "string" }, - { column: "mt.tag_code", param: searchParams.code, type: "string" }, - { - column: "md.device_name", - param: searchParams.deviceName, - type: "string", - }, - { - column: "pss.sub_section_name", - param: searchParams.subSectionName, - type: "string", - }, - ]); - - const whereClause = whereConditions.length - ? `AND ${whereConditions.join(" AND ")}` - : ""; - - const queryText = ` - SELECT - mt.tag_id, mt.device_id, mt.tag_code, mt.tag_name, mt.tag_number, - mt.data_type, mt.unit, mt.is_active, mt.sub_section_id, - mt.created_at, mt.updated_at, mt.deleted_at, - md.device_name,md.ip_address, - pss.sub_section_code, pss.sub_section_name - FROM m_tags mt - INNER JOIN m_device md ON mt.device_id = md.device_id - INNER JOIN plant_sub_section pss ON mt.sub_section_id = pss.sub_section_id - WHERE mt.deleted_at IS NULL ${whereClause} - ORDER BY mt.tag_id ASC - `; - const result = await query(queryText, queryParams); - return result.recordset; -}; - -// Get tag by ID -const getTagByIdDb = async (id) => { - const queryText = ` - SELECT - mt.tag_id, mt.device_id, mt.tag_code, mt.tag_name, mt.tag_number, - mt.data_type, mt.unit, mt.is_active, mt.sub_section_id, - mt.created_at, mt.updated_at, mt.deleted_at, - md.device_name, - pss.sub_section_code, pss.sub_section_name - FROM m_tags mt - LEFT JOIN m_device md ON mt.device_id = md.device_id - LEFT JOIN plant_sub_section pss ON mt.sub_section_id = pss.sub_section_id - WHERE mt.tag_id = $1 AND mt.deleted_at IS NULL - `; - const result = await query(queryText, [id]); - return result.recordset[0]; -}; - -// Create tag -const createTagDb = async (data) => { - const queryText = ` - INSERT INTO m_tags - (device_id, tag_code, tag_name, tag_number, data_type, unit, is_active, sub_section_id, created_by) - VALUES - ($1,$2,$3,$4,$5,$6,$7,$8,$9); - SELECT SCOPE_IDENTITY() as tag_id; - `; - - const values = [ - data.device_id, - data.tag_code, - data.tag_name, - data.tag_number, - data.data_type, - data.unit, - data.is_active || 1, //default aktif - data.sub_section_id, - data.created_by, - ]; - - const result = await query(queryText, values); - return result.recordset[0]?.tag_id; -}; - -const updateTagDb = async (tagId, data) => { - const { query: queryText, values } = buildDynamicUpdate("m_tags", data, { - tag_id: tagId, - updated_at: "GETDATE()", - }); - const finalQuery = queryText.replace("WHERE", "WHERE deleted_at IS NULL AND"); - await query(finalQuery, values); - return true; -}; - -const deleteTagDb = async (tagId, deletedBy) => { - const queryText = ` - UPDATE m_tags - SET - deleted_at = GETDATE(), - deleted_by = $1, - is_active = 0 - WHERE tag_id = $2 - AND deleted_at IS NULL - `; - - await query(queryText, [deletedBy, tagId]); - return true; -}; - -module.exports = { - getAllTagsDb, - getTagByIdDb, - createTagDb, - updateTagDb, - deleteTagDb, -}; diff --git a/db/tags.db.js b/db/tags.db.js new file mode 100644 index 0000000..407e9eb --- /dev/null +++ b/db/tags.db.js @@ -0,0 +1,144 @@ +const pool = require("../config"); + +// Get all tags +const getAllTagsDb = async (searchParams = {}) => { + let queryParams = []; + + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1; + queryParams = [Number(searchParams.limit ?? 10), page]; + } + + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + [ + "a.tag_name", + "a.tag_code", + "a.tag_number", + "a.data_type", + "a.unit", + "b.device_name", + "c.sub_section_name", + ], + searchParams.criteria, + queryParams + ); + + if (whereParamOr) queryParams = whereParamOr; + + // Filter tambahan (AND conditions) + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [ + { column: "a.tag_name", param: searchParams.name, type: "string" }, + { column: "a.tag_code", param: searchParams.code, type: "string" }, + { column: "a.data_type", param: searchParams.data, type: "string" }, + { column: "a.unit", param: searchParams.unit, type: "string" }, + { column: "b.device_name", param: searchParams.device, type: "string" }, + { + column: "b.device_description", + param: searchParams.device, + type: "string", + }, + { column: "b.ip_address", param: searchParams.device, type: "string" }, + { + column: "c.sub_section_name", + param: searchParams.subsection, + type: "string", + }, + ], + queryParams + ); + + if (whereParamAnd) queryParams = whereParamAnd; + + const queryText = ` + SELECT + COUNT(*) OVER() AS total_data, + a.*, + b.device_name, + b.device_location, + b.device_description, + c.sub_section_name + FROM m_tags a + LEFT JOIN m_device b ON a.device_id = b.device_id + LEFT JOIN plant_sub_section c ON a.sub_section_id = c.sub_section_id + WHERE a.deleted_at IS NULL + ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} + ${whereOrConditions ? ` ${whereOrConditions}` : ""} + ORDER BY a.tag_id ASC + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""} + `; + + const result = await pool.query(queryText, queryParams); + + const total = + result?.recordset?.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; + + return { data: result.recordset, total }; +}; + +const getTagsByIdDb = async (id) => { + const queryText = ` + SELECT + a.*, + b.device_name, + b.device_location, + b.device_description, + c.sub_section_name + FROM m_tags a + LEFT JOIN m_device b ON a.device_id = b.device_id + LEFT JOIN plant_sub_section c ON a.sub_section_id = c.sub_section_id + WHERE a.tag_id = $1 AND a.deleted_at IS NULL + `; + const result = await pool.query(queryText, [id]); + return result.recordset; +}; + +const createTagsDb = async (data) => { + const newCode = await pool.generateKode("TAG", "m_tags", "tag_code"); + + const store = { + ...data, + tag_code: newCode, + }; + + const { query: queryText, values } = pool.buildDynamicInsert("m_tags", store); + const result = await pool.query(queryText, values); + const insertedId = result.recordset[0]?.inserted_id; + + return insertedId ? await getTagsByIdDb(insertedId) : null; +}; + +const updateTagsDb = async (id, data) => { + const store = { ...data }; + const whereData = { tag_id: id }; + + const { query: queryText, values } = pool.buildDynamicUpdate( + "m_tags", + store, + whereData + ); + + await pool.query(`${queryText} AND deleted_at IS NULL`, values); + return getTagsByIdDb(id); +}; + +// Soft delete tag +const deleteTagsDb = async (id, deletedBy) => { + const queryText = ` + UPDATE m_tags + SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 + WHERE tag_id = $2 AND deleted_at IS NULL + `; + await pool.query(queryText, [deletedBy, id]); + return true; +}; + +module.exports = { + getAllTagsDb, + getTagsByIdDb, + createTagsDb, + updateTagsDb, + deleteTagsDb, +}; diff --git a/routes/index.js b/routes/index.js index 5765b89..de52665 100644 --- a/routes/index.js +++ b/routes/index.js @@ -3,10 +3,12 @@ const auth = require("./auth.route"); const users = require("./users.route"); const device = require('./device.route'); const roles = require('./roles.route') +const tags = require("./tags.route") router.use("/auth", auth); router.use("/user", users); router.use("/device", device); router.use("/roles", roles); +router.use("/tags", tags) module.exports = router; diff --git a/routes/roles.route.js b/routes/roles.route.js index 44094ed..0ce2d4a 100644 --- a/routes/roles.route.js +++ b/routes/roles.route.js @@ -1,17 +1,17 @@ const express = require('express'); -const Rolesontroller = require('../controllers/roles.controller'); +const RolesController = require('../controllers/roles.controller'); const verifyToken = require("../middleware/verifyToken") const verifyAccess = require("../middleware/verifyAccess") const router = express.Router(); router.route("/") - .get(verifyToken.verifyAccessToken, Rolesontroller.getAll) - .post(verifyToken.verifyAccessToken, verifyAccess(), Rolesontroller.create); + .get(verifyToken.verifyAccessToken, RolesController.getAll) + .post(verifyToken.verifyAccessToken, verifyAccess(), RolesController.create); router.route("/:id") - .get(verifyToken.verifyAccessToken, Rolesontroller.getById) - .put(verifyToken.verifyAccessToken, verifyAccess(), Rolesontroller.update) - .delete(verifyToken.verifyAccessToken, verifyAccess(), Rolesontroller.delete); + .get(verifyToken.verifyAccessToken, RolesController.getById) + .put(verifyToken.verifyAccessToken, verifyAccess(), RolesController.update) + .delete(verifyToken.verifyAccessToken, verifyAccess(), RolesController.delete); module.exports = router; \ No newline at end of file diff --git a/routes/tags.route.js b/routes/tags.route.js new file mode 100644 index 0000000..4160284 --- /dev/null +++ b/routes/tags.route.js @@ -0,0 +1,17 @@ +const express = require('express'); +const TagsController = require('../controllers/tags.controller'); +const verifyToken = require("../middleware/verifyToken") +const verifyAccess = require("../middleware/verifyAccess") + +const router = express.Router(); + +router.route("/") + .get(verifyToken.verifyAccessToken, TagsController.getAll) + .post(verifyToken.verifyAccessToken, verifyAccess(), TagsController.create); + +router.route("/:id") + .get(verifyToken.verifyAccessToken, TagsController.getById) + .put(verifyToken.verifyAccessToken, verifyAccess(), TagsController.update) + .delete(verifyToken.verifyAccessToken, verifyAccess(), TagsController.delete); + +module.exports = router; \ No newline at end of file diff --git a/services/tags.service.js b/services/tags.service.js new file mode 100644 index 0000000..9f180ae --- /dev/null +++ b/services/tags.service.js @@ -0,0 +1,89 @@ +const { + getAllTagsDb, + getTagsByIdDb, + createTagsDb, + updateTagsDb, + deleteTagsDb + } = require('../db/tags.db'); + const { ErrorHandler } = require('../helpers/error'); + + class TagsService { + // Get all devices + static async getAllTags(param) { + try { + const results = await getAllTagsDb(param); + + results.data.map(element => { + }); + + return results + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Get device by ID + static async getTagByID(id) { + try { + const result = await getTagsByIdDb(id); + + if (result.length < 1) throw new ErrorHandler(404, 'Tags not found'); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Create device + static async createTags(data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const result = await createTagsDb(data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Update device + static async updateTags(id, data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const dataExist = await getTagsByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Tags not found'); + } + + const result = await updateTagsDb(id, data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Soft delete device + static async deleteTags(id, userId) { + try { + const dataExist = await getTagsByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Tags not found'); + } + + const result = await deleteTagsDb(id, userId); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + } + + module.exports = TagsService; + \ No newline at end of file diff --git a/validate/tags.schema.js b/validate/tags.schema.js new file mode 100644 index 0000000..c73a465 --- /dev/null +++ b/validate/tags.schema.js @@ -0,0 +1,29 @@ +// ======================== +// Device Validation + +const Joi = require("joi"); + +// ======================== +const insertTagsSchema = Joi.object({ + device_id: Joi.number().required(), + tag_name: Joi.string().max(200).required(), + tag_number: Joi.number().required(), + is_active: Joi.boolean().required(), + data_type: Joi.string().max(50).required(), + unit: Joi.string().max(50).required(), +}); + +const updateTagsSchema = Joi.object({ + device_id: Joi.number(), + tag_name: Joi.string().max(200), + tag_number: Joi.number(), + is_active: Joi.boolean(), + data_type: Joi.string().max(50), + unit: Joi.string().max(50), +}).min(1); + +// ✅ Export dengan CommonJS +module.exports = { + insertTagsSchema, + updateTagsSchema, +}; From 7302a3320c0a313108bfa7287fa389eb23a6c0ce Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 10 Oct 2025 13:39:55 +0700 Subject: [PATCH 079/126] fix: user route --- routes/users.route.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/routes/users.route.js b/routes/users.route.js index ff0a02b..d8ce102 100644 --- a/routes/users.route.js +++ b/routes/users.route.js @@ -6,18 +6,21 @@ const verifyAccess = require('../middleware/verifyAccess'); const router = express.Router(); router.route('/') - .get(verifyToken.verifyAccessToken, UserController.getAllUsers) - .post(verifyToken.verifyAccessToken, verifyAccess(), UserController.createUser); + .get(verifyToken.verifyAccessToken, UserController.getAll) + .post(verifyToken.verifyAccessToken, verifyAccess(), UserController.create); router.route('/:id') - .get(verifyToken.verifyAccessToken, UserController.getUserById) - .put(verifyToken.verifyAccessToken, verifyAccess(), UserController.updateUser) - .delete(verifyToken.verifyAccessToken, verifyAccess(), UserController.deleteUser); + .get(verifyToken.verifyAccessToken, UserController.getById) + .put(verifyToken.verifyAccessToken, verifyAccess(), UserController.update) + .delete(verifyToken.verifyAccessToken, verifyAccess(), UserController.delete); router.route('/change-password/:id') - .put(verifyToken.verifyAccessToken, verifyAccess(), UserController.changePassword); + .put(verifyToken.verifyAccessToken, verifyAccess(), UserController.changePassword); router.route('/:id/approve') - .put(verifyToken.verifyAccessToken, verifyAccess(), UserController.approveUser); + .put(verifyToken.verifyAccessToken, verifyAccess(), UserController.approve); + +router.route('/status/all') + .get(verifyToken.verifyAccessToken, UserController.getAllStatus); module.exports = router; From 36a2609512de9acf91db183bbac7c8933aa7fec1 Mon Sep 17 00:00:00 2001 From: Muhammad Afif Date: Fri, 10 Oct 2025 20:48:24 +0700 Subject: [PATCH 080/126] repair: device_location to ip_address --- db/tags.db.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/db/tags.db.js b/db/tags.db.js index 407e9eb..faae300 100644 --- a/db/tags.db.js +++ b/db/tags.db.js @@ -25,7 +25,6 @@ const getAllTagsDb = async (searchParams = {}) => { if (whereParamOr) queryParams = whereParamOr; - // Filter tambahan (AND conditions) const { whereConditions, whereParamAnd } = pool.buildFilterQuery( [ { column: "a.tag_name", param: searchParams.name, type: "string" }, @@ -55,7 +54,7 @@ const getAllTagsDb = async (searchParams = {}) => { COUNT(*) OVER() AS total_data, a.*, b.device_name, - b.device_location, + b.ip_address, b.device_description, c.sub_section_name FROM m_tags a @@ -83,7 +82,7 @@ const getTagsByIdDb = async (id) => { SELECT a.*, b.device_name, - b.device_location, + b.ip_address, b.device_description, c.sub_section_name FROM m_tags a From 62823524bcb0d352d6862147ab295ec5fbbb2497 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 10 Oct 2025 14:24:41 +0700 Subject: [PATCH 081/126] update: auth --- controllers/auth.controller.js | 197 ++++++++++++++----------------- services/auth.service.js | 209 ++++++++++++++++++--------------- 2 files changed, 198 insertions(+), 208 deletions(-) diff --git a/controllers/auth.controller.js b/controllers/auth.controller.js index 386f1f3..2508de5 100644 --- a/controllers/auth.controller.js +++ b/controllers/auth.controller.js @@ -1,139 +1,114 @@ const AuthService = require('../services/auth.service'); -const { registerSchema, loginSchema } = require('../validate/auth.schema') -const { setResponse } = require('../helpers/utils'); +const { registerSchema, loginSchema } = require('../validate/auth.schema'); +const { setResponse, checkValidate } = require('../helpers/utils'); const { createCaptcha } = require('../utils/captcha'); class AuthController { - - // Registration + // Register static async register(req, res) { - try { - const { error, value } = registerSchema.validate(req.body, { abortEarly: false }); + const { error, value } = await checkValidate(registerSchema, req); - if (error) { - const errors = error.details.reduce((acc, cur) => { - const field = Array.isArray(cur.path) ? cur.path.join('.') : String(cur.path); - if (!acc[field]) acc[field] = []; - acc[field].push(cur.message); - return acc; - }, {}); - return res.status(400).json(setResponse(errors, 'Validation failed', 400)); - } - - if (value.phone && value.phone.startsWith('0')) { - value.phone = '+62' + value.phone.slice(1); - } - - const { user, tokens } = await AuthService.register(value); - - // Set refresh token di cookie - res.cookie('refreshToken', tokens.refreshToken, { - httpOnly: true, - secure: false, // masih dev - sameSite: 'lax', - maxAge: 7 * 24 * 60 * 60 * 1000 // 7 hari - }); - - return res.status(201).json( - setResponse( - { - user: { ...user, approved: false }, - accessToken: tokens.accessToken - }, - 'User registered successfully. Waiting for admin approval.', - 201 - ) - ); - } catch (err) { - return res.status(err.statusCode || 500).json( - setResponse([], err.message || 'Register failed', err.statusCode || 500) - ); + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); } - } - // Captcha - static async generateCaptcha(req, res) { - try { - const { svg, text } = createCaptcha(); - - res.setHeader('X-Captcha-Text', text); - - return res.status(200).json({ data: { svg, text } }); - } catch (err) { - return res.status(500).json(setResponse([], 'Captcha failed', 500)); + if (value.phone && value.phone.startsWith('0')) { + value.phone = '+62' + value.phone.slice(1); } + + const { user, tokens } = await AuthService.register(value); + + // Set refresh token di cookie + res.cookie('refreshToken', tokens.refreshToken, { + httpOnly: true, + secure: false, // masih dev + sameSite: 'lax', + maxAge: 7 * 24 * 60 * 60 * 1000 // 7 hari + }); + + const response = await setResponse( + { + user: { ...user, approved: false }, + accessToken: tokens.accessToken + }, + 'User registered successfully. Waiting for admin approval.', + 201 + ); + + return res.status(response.statusCode).json(response); } // Login static async login(req, res) { - try { - const { error, value } = loginSchema.validate(req.body, { abortEarly: false }); - if (error) return res.status(400).json(setResponse([], 'Validation failed', 400)); + const { error, value } = await checkValidate(loginSchema, req); - const { identifier, password, captcha, captchaText } = value; - - if (!captcha || captcha.toLowerCase() !== captchaText.toLowerCase()) { - return res.status(400).json(setResponse([], 'Invalid captcha', 400)); - } - - const { user, tokens } = await AuthService.login({ identifier, password }); - - // Set refresh token di cookie - res.cookie('refreshToken', tokens.refreshToken, { - httpOnly: true, - secure: false, // masih dev - sameSite: 'lax', - maxAge: 7 * 24 * 60 * 60 * 1000 // 7 hari - }); - - return res.status(200).json( - setResponse( - { - user: { ...user, approved: true }, - accessToken: tokens.accessToken - }, - 'Login successful', - 200 - ) - ); - } catch (err) { - return res.status(err.statusCode || 500).json( - setResponse([], err.message || 'Login failed', err.statusCode || 500) - ); + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); } + + const { identifier, password, captcha, captchaText } = value; + + if (!captcha || captcha.toLowerCase() !== captchaText.toLowerCase()) { + return res.status(400).json(setResponse([], 'Invalid captcha', 400)); + } + + const { user, tokens } = await AuthService.login({ identifier, password }); + + // Set refresh token di cookie + res.cookie('refreshToken', tokens.refreshToken, { + httpOnly: true, + secure: false, // masih dev + sameSite: 'lax', + maxAge: 7 * 24 * 60 * 60 * 1000 // 7 hari + }); + + const response = await setResponse( + { + user: { ...user, approved: true }, + accessToken: tokens.accessToken + }, + 'Login successful', + 200 + ); + + return res.status(response.statusCode).json(response); } // Refresh Token static async refreshToken(req, res) { - try { - const refreshToken = req.cookies?.refreshToken; - if (!refreshToken) { - return res.status(401).json(setResponse(null, 'Refresh token is required', 401)); - } + const refreshToken = req.cookies?.refreshToken; - const result = await AuthService.refreshToken(refreshToken); - - return res.status(200).json(setResponse(result, 'Token refreshed successfully', 200)); - } catch (err) { - const status = err.statusCode && err.statusCode < 500 ? err.statusCode : 401; - return res.status(status).json( - setResponse(null, err.message || 'Refresh token invalid', status) - ); + if (!refreshToken) { + return res.status(401).json(setResponse(null, 'Refresh token is required', 401)); } + + const result = await AuthService.refreshToken(refreshToken); + const response = await setResponse(result, 'Token refreshed successfully', 200); + + return res.status(response.statusCode).json(response); } // Logout static async logout(req, res) { - try { - res.clearCookie('refreshToken', { - httpOnly: true, - sameSite: 'none', - secure: true - }); - return res.status(200).json(setResponse(null, 'Logged out successfully', 200)); - } catch (err) { - return res.status(500).json(setResponse(null, 'Logout failed', 500)); - } + res.clearCookie('refreshToken', { + httpOnly: true, + sameSite: 'none', + secure: true + }); + + const response = await setResponse(null, 'Logged out successfully', 200); + return res.status(response.statusCode).json(response); + } + + // Captcha + static async generateCaptcha(req, res) { + const { svg, text } = createCaptcha(); + + // munculkan di header untuk keperluan dev + res.setHeader('X-Captcha-Text', text); + + const response = await setResponse({ svg, text }, 'Captcha generated', 200); + return res.status(response.statusCode).json(response); } } diff --git a/services/auth.service.js b/services/auth.service.js index 6f084a9..ff73748 100644 --- a/services/auth.service.js +++ b/services/auth.service.js @@ -1,5 +1,5 @@ -const { - getUserByUserEmailDb, +const { + getUserByUserEmailDb, createUserDb, getUserByUsernameDb } = require('../db/user.db'); @@ -8,116 +8,131 @@ const { ErrorHandler } = require('../helpers/error'); const JWTService = require('../utils/jwt'); class AuthService { - // Register static async register({ fullname, name, email, phone, password }) { - const existingUser = await getUserByUserEmailDb(email); - if (existingUser) { - throw new ErrorHandler(400, 'Email already registered'); + try { + const existingUser = await getUserByUserEmailDb(email); + if (existingUser) { + throw new ErrorHandler(400, 'Email already registered'); + } + + 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: null, + is_sa: 0, + is_active: 1, + is_approve: 0, + approved_by: null, + approved_at: null + }); + + const newUser = { + user_id: userId, + user_fullname: fullname, + user_name: name, + user_email: email, + user_phone: phone + }; + + const tokens = JWTService.generateTokenPair(newUser); + + return { user: newUser, tokens }; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); } - - 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: null, - is_sa: 0, - is_active: 1, - is_approve: 0, - approved_by: null, - approved_at: null - }); - - const newUser = { - user_id: userId, - user_fullname: fullname, - user_name: name, - user_email: email, - user_phone: phone - }; - - // generate token pair - const tokens = JWTService.generateTokenPair(newUser); - - return { user: newUser, tokens }; } // Login static async login({ identifier, password }) { - let user; + try { + let user; - if (identifier.includes('@')) { - user = await getUserByUserEmailDb(identifier); - } else { - user = await getUserByUsernameDb(identifier); + if (identifier.includes('@')) { + user = await getUserByUserEmailDb(identifier); + } else { + user = await getUserByUsernameDb(identifier); + } + + if (!user) { + throw new ErrorHandler(401, 'Invalid credentials'); + } + + const passwordMatch = await comparePassword(password, user.user_password); + if (!passwordMatch) { + throw new ErrorHandler(401, 'Invalid credentials'); + } + + if (!user.is_active) { + 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 = { + user_id: user.user_id, + user_fullname: user.user_fullname, + user_name: user.user_name, + user_email: user.user_email, + user_phone: user.user_phone, + role_id: user.role_id, + role_name: user.role_name, + is_sa: user.is_sa + }; + + const tokens = JWTService.generateTokenPair(payload); + + return { user: payload, tokens }; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); } - - if (!user) { - throw new ErrorHandler(401, 'Invalid credentials') - } - - const passwordMatch = await comparePassword(password, user.user_password); - if (!passwordMatch) { - throw new ErrorHandler(401, 'Invalid credentials'); - } - - if (!user.is_active) { - 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 = { - user_id: user.user_id, - user_fullname: user.user_fullname, - user_name: user.user_name, - user_email: user.user_email, - user_phone: user.user_phone, - role_id: user.role_id, - role_name: user.role_name, - is_sa: user.is_sa - }; - - const tokens = JWTService.generateTokenPair(payload); - return { user: payload, tokens }; } // Refresh Token static async refreshToken(refreshToken) { - if (!refreshToken) throw new ErrorHandler(401, 'Refresh token is required'); - - let decoded; try { - decoded = JWTService.verifyRefreshToken(refreshToken); - } catch (err) { - if (err.message.includes('expired')) throw new ErrorHandler(401, 'Refresh token expired'); - throw new ErrorHandler(401, 'Invalid refresh token'); + if (!refreshToken) { + throw new ErrorHandler(401, 'Refresh token is required'); + } + + let decoded; + try { + decoded = JWTService.verifyRefreshToken(refreshToken); + } catch (err) { + if (err.message.includes('expired')) { + throw new ErrorHandler(401, 'Refresh token expired'); + } + throw new ErrorHandler(401, 'Invalid refresh token'); + } + + const payload = { + user_id: decoded.user_id, + user_fullname: decoded.user_fullname, + user_name: decoded.user_name, + user_email: decoded.user_email, + user_phone: decoded.user_phone, + role_id: decoded.role_id, + role_name: decoded.role_name, + is_sa: decoded.is_sa + }; + + const accessToken = JWTService.generateAccessToken(payload); + + return { + accessToken, + tokenType: 'Bearer', + expiresIn: 900 + }; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); } - - const payload = { - user_id: decoded.user_id, - user_fullname: decoded.user_fullname, - user_name: decoded.user_name, - user_email: decoded.user_email, - user_phone: decoded.user_phone, - role_id: decoded.role_id, - role_name: decoded.role_name, - is_sa: decoded.is_sa - }; - - const accessToken = JWTService.generateAccessToken(payload); - - return { - accessToken, - tokenType: 'Bearer', - expiresIn: 900 - }; } } From cc4d135a53670adc656e02f0d463fd7041d69244 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 10 Oct 2025 14:26:12 +0700 Subject: [PATCH 082/126] add: brand db --- db/brand.db.js | 116 ++++++++++++++++++++++++++----------------------- 1 file changed, 61 insertions(+), 55 deletions(-) diff --git a/db/brand.db.js b/db/brand.db.js index 5693955..6182e1a 100644 --- a/db/brand.db.js +++ b/db/brand.db.js @@ -1,65 +1,66 @@ const pool = require("../config"); // Get all brands -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 getAllBrandsDb = async (searchParams = {}) => { + let queryParams = []; - const whereClause = whereConditions.length ? `AND ${whereConditions.join(" AND ")}` : ""; + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1; + queryParams = [Number(searchParams.limit ?? 10), page]; + } + + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + ["b.brand_name"], + searchParams.criteria, + queryParams + ); + + queryParams = whereParamOr ? whereParamOr : queryParams; + + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [ + { column: "b.brand_name", param: searchParams.name, type: "string" }, + { column: "b.created_by", param: searchParams.created_by, type: "number" }, + ], + queryParams + ); + + queryParams = whereParamAnd ? whereParamAnd : queryParams; const queryText = ` - SELECT - b.brand_id, - b.brand_name, - b.created_at, - b.updated_at, - b.deleted_at, - b.created_by, - b.updated_by, - b.deleted_by + SELECT COUNT(*) OVER() AS total_data, b.* FROM m_brands b - WHERE b.deleted_at IS NULL ${whereClause} + WHERE b.deleted_at IS NULL + ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} + ${whereOrConditions ? whereOrConditions : ""} ORDER BY b.brand_id ASC + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""}; `; + const result = await pool.query(queryText, queryParams); - return result.recordset; + + const total = result?.recordset.length > 0 ? parseInt(result.recordset[0].total_data, 10) : 0; + + return { data: result.recordset, total }; }; // Get brand by ID const getBrandByIdDb = async (id) => { const queryText = ` - SELECT - brand_id, - brand_name, - created_at, - updated_at, - deleted_at, - created_by, - updated_by, - deleted_by - FROM m_brands - WHERE brand_id = $1 AND deleted_at IS NULL + SELECT b.* + FROM m_brands b + WHERE b.brand_id = $1 AND b.deleted_at IS NULL `; const result = await pool.query(queryText, [id]); - return result.recordset[0]; + return result.recordset; }; // Get brand by name const getBrandByNameDb = async (name) => { const queryText = ` - SELECT - brand_id, - brand_name, - created_at, - updated_at, - deleted_at, - created_by, - updated_by, - deleted_by - FROM m_brands - WHERE brand_name = $1 AND deleted_at IS NULL + SELECT b.* + FROM m_brands b + WHERE b.brand_name = $1 AND b.deleted_at IS NULL `; const result = await pool.query(queryText, [name]); return result.recordset[0]; @@ -67,33 +68,38 @@ const getBrandByNameDb = async (name) => { // Create brand const createBrandDb = async (data) => { - const { query, values } = pool.buildDynamicInsert("m_brands", { + const store = { ...data, created_at: new Date(), - }); - const result = await pool.query(query, values); + }; + + const { query: queryText, values } = pool.buildDynamicInsert("m_brands", store); + const result = await pool.query(queryText, values); const insertedId = result.recordset[0]?.inserted_id; - if (!insertedId) return null; - return getBrandByIdDb(insertedId); + return insertedId ? await getBrandByIdDb(insertedId) : null; }; // Update brand const updateBrandDb = async (id, data) => { - const { query, values } = pool.buildDynamicUpdate( - "m_brands", - { ...data, updated_at: new Date() }, - { brand_id: id } - ); - await pool.query(query, values); + const store = { + ...data, + updated_at: new Date(), + }; + + const whereData = { + brand_id: id, + }; + + const { query: queryText, values } = pool.buildDynamicUpdate("m_brands", store, whereData); + await pool.query(`${queryText} AND deleted_at IS NULL`, values); return getBrandByIdDb(id); }; // Soft delete brand -const softDeleteBrandDb = async (id, deletedBy) => { +const deleteBrandDb = async (id, deletedBy) => { const queryText = ` UPDATE m_brands - SET deleted_at = GETDATE(), - deleted_by = $1 + SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 WHERE brand_id = $2 AND deleted_at IS NULL `; await pool.query(queryText, [deletedBy, id]); @@ -106,5 +112,5 @@ module.exports = { getBrandByNameDb, createBrandDb, updateBrandDb, - softDeleteBrandDb, + deleteBrandDb, }; From 751cd0911e2c732f4b9c411ba207806cec5eb9bb Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Sat, 11 Oct 2025 02:26:00 +0700 Subject: [PATCH 083/126] fix: auth --- controllers/auth.controller.js | 66 ++++++++++++------------------- services/auth.service.js | 71 ++++++++++++++-------------------- validate/auth.schema.js | 10 ++--- 3 files changed, 60 insertions(+), 87 deletions(-) diff --git a/controllers/auth.controller.js b/controllers/auth.controller.js index 2508de5..de1a9dd 100644 --- a/controllers/auth.controller.js +++ b/controllers/auth.controller.js @@ -1,6 +1,6 @@ const AuthService = require('../services/auth.service'); -const { registerSchema, loginSchema } = require('../validate/auth.schema'); const { setResponse, checkValidate } = require('../helpers/utils'); +const { registerSchema, loginSchema } = require('../validate/auth.schema'); const { createCaptcha } = require('../utils/captcha'); class AuthController { @@ -12,30 +12,21 @@ class AuthController { return res.status(400).json(setResponse(error, 'Validation failed', 400)); } - if (value.phone && value.phone.startsWith('0')) { - value.phone = '+62' + value.phone.slice(1); + // Format nomor HP Indonesia + if (value.user_phone && value.user_phone.startsWith('0')) { + value.user_phone = '+62' + value.user_phone.slice(1); } - const { user, tokens } = await AuthService.register(value); - - // Set refresh token di cookie - res.cookie('refreshToken', tokens.refreshToken, { - httpOnly: true, - secure: false, // masih dev - sameSite: 'lax', - maxAge: 7 * 24 * 60 * 60 * 1000 // 7 hari - }); + const results = await AuthService.register(value); const response = await setResponse( { - user: { ...user, approved: false }, - accessToken: tokens.accessToken + user: { ...results.user, approved: false }, }, - 'User registered successfully. Waiting for admin approval.', - 201 + 'User registered successfully. Waiting for admin approval.' ); - return res.status(response.statusCode).json(response); + res.status(response.statusCode).json(response); } // Login @@ -46,32 +37,25 @@ class AuthController { return res.status(400).json(setResponse(error, 'Validation failed', 400)); } - const { identifier, password, captcha, captchaText } = value; + const results = await AuthService.login(value); - if (!captcha || captcha.toLowerCase() !== captchaText.toLowerCase()) { - return res.status(400).json(setResponse([], 'Invalid captcha', 400)); - } - - const { user, tokens } = await AuthService.login({ identifier, password }); - - // Set refresh token di cookie - res.cookie('refreshToken', tokens.refreshToken, { + // Simpan refresh token di cookie + res.cookie('refreshToken', results.tokens.refreshToken, { httpOnly: true, - secure: false, // masih dev + secure: false, sameSite: 'lax', - maxAge: 7 * 24 * 60 * 60 * 1000 // 7 hari + maxAge: 7 * 24 * 60 * 60 * 1000 }); const response = await setResponse( { - user: { ...user, approved: true }, - accessToken: tokens.accessToken + user: { ...results.user, approved: true }, + accessToken: results.tokens.accessToken }, - 'Login successful', - 200 + 'Login successful' ); - return res.status(response.statusCode).json(response); + res.status(response.statusCode).json(response); } // Refresh Token @@ -82,10 +66,10 @@ class AuthController { return res.status(401).json(setResponse(null, 'Refresh token is required', 401)); } - const result = await AuthService.refreshToken(refreshToken); - const response = await setResponse(result, 'Token refreshed successfully', 200); + const results = await AuthService.refreshToken(refreshToken); + const response = await setResponse(results, 'Token refreshed successfully'); - return res.status(response.statusCode).json(response); + res.status(response.statusCode).json(response); } // Logout @@ -96,19 +80,19 @@ class AuthController { secure: true }); - const response = await setResponse(null, 'Logged out successfully', 200); - return res.status(response.statusCode).json(response); + const response = await setResponse(null, 'Logged out successfully'); + res.status(response.statusCode).json(response); } // Captcha static async generateCaptcha(req, res) { const { svg, text } = createCaptcha(); - // munculkan di header untuk keperluan dev + // Tampilkan captcha di header untuk dev res.setHeader('X-Captcha-Text', text); - const response = await setResponse({ svg, text }, 'Captcha generated', 200); - return res.status(response.statusCode).json(response); + const response = await setResponse({ svg, text }, 'Captcha generated'); + res.status(response.statusCode).json(response); } } diff --git a/services/auth.service.js b/services/auth.service.js index ff73748..c700bef 100644 --- a/services/auth.service.js +++ b/services/auth.service.js @@ -9,72 +9,66 @@ const JWTService = require('../utils/jwt'); class AuthService { // Register - static async register({ fullname, name, email, phone, password }) { + static async register(data) { try { - const existingUser = await getUserByUserEmailDb(email); - if (existingUser) { - throw new ErrorHandler(400, 'Email already registered'); + const existingEmail = await getUserByUserEmailDb(data.user_email); + const existingUsername = await getUserByUsernameDb(data.user_name); + + if (existingUsername) { + throw new ErrorHandler(400, 'Username is already taken'); + } + if (existingEmail) { + throw new ErrorHandler(400, 'Email is already taken'); } - const hashedPassword = await hashPassword(password); + const hashedPassword = await hashPassword(data.user_password); const userId = await createUserDb({ - user_fullname: fullname, - user_name: name, - user_email: email, - user_phone: phone, + user_fullname: data.user_fullname, + user_name: data.user_name, + user_email: data.user_email, + user_phone: data.user_phone, user_password: hashedPassword, - role_id: null, - is_sa: 0, - is_active: 1, - is_approve: 0, - approved_by: null, - approved_at: null }); const newUser = { user_id: userId, - user_fullname: fullname, - user_name: name, - user_email: email, - user_phone: phone + user_fullname: data.user_fullname, + user_name: data.user_name, + user_email: data.user_email, + user_phone: data.user_phone }; - const tokens = JWTService.generateTokenPair(newUser); - - return { user: newUser, tokens }; + return { user: newUser }; } catch (error) { throw new ErrorHandler(error.statusCode, error.message); } } // Login - static async login({ identifier, password }) { + static async login(data) { try { - let user; + const { identifier, password, captcha, captchaText } = data; + if (!captcha || captcha.toLowerCase() !== captchaText.toLowerCase()) { + throw new ErrorHandler(400, 'Invalid captcha'); + } + + let user; if (identifier.includes('@')) { user = await getUserByUserEmailDb(identifier); } else { user = await getUserByUsernameDb(identifier); } - if (!user) { - throw new ErrorHandler(401, 'Invalid credentials'); - } + if (!user) throw new ErrorHandler(401, 'Invalid credentials'); const passwordMatch = await comparePassword(password, user.user_password); - if (!passwordMatch) { - throw new ErrorHandler(401, 'Invalid credentials'); - } + if (!passwordMatch) throw new ErrorHandler(401, 'Invalid credentials'); - if (!user.is_active) { - throw new ErrorHandler(403, 'User is inactive'); - } - - if (!user.is_approve) { + if (!user.is_active) 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 = { user_id: user.user_id, @@ -88,7 +82,6 @@ class AuthService { }; const tokens = JWTService.generateTokenPair(payload); - return { user: payload, tokens }; } catch (error) { throw new ErrorHandler(error.statusCode, error.message); @@ -98,10 +91,6 @@ class AuthService { // Refresh Token static async refreshToken(refreshToken) { try { - if (!refreshToken) { - throw new ErrorHandler(401, 'Refresh token is required'); - } - let decoded; try { decoded = JWTService.verifyRefreshToken(refreshToken); diff --git a/validate/auth.schema.js b/validate/auth.schema.js index 11d834d..bba7312 100644 --- a/validate/auth.schema.js +++ b/validate/auth.schema.js @@ -4,17 +4,17 @@ const Joi = require("joi"); // Auth Validation // ======================== const registerSchema = 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() + user_fullname: Joi.string().min(3).max(100).required(), + user_name: Joi.string().alphanum().min(3).max(50).required(), + user_email: Joi.string().email().required(), + user_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() + user_password: Joi.string() .min(8) .pattern(/[A-Z]/, 'uppercase letter') .pattern(/[a-z]/, 'lowercase letter') From e581f5b5bb9888a435cd2a48c51f951fd4ffb07a Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Sat, 11 Oct 2025 02:26:17 +0700 Subject: [PATCH 084/126] fix: crud user --- controllers/users.controller.js | 47 +++---- db/user.db.js | 17 +++ routes/users.route.js | 3 - services/user.service.js | 219 ++++++++++++++------------------ validate/user.schema.js | 24 +++- 5 files changed, 154 insertions(+), 156 deletions(-) diff --git a/controllers/users.controller.js b/controllers/users.controller.js index 328ea2f..05a3fa8 100644 --- a/controllers/users.controller.js +++ b/controllers/users.controller.js @@ -1,6 +1,6 @@ -const UserService = require('../services/user.service'); -const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); -const { userSchema, newPasswordSchema } = require('../validate/user.schema'); +const UserService = require("../services/user.service"); +const { setResponse, setResponsePaging, checkValidate } = require("../helpers/utils"); +const { userSchema, updateUserSchema, newPasswordSchema } = require("../validate/user.schema"); class UserController { // Get all users @@ -8,7 +8,7 @@ class UserController { const queryParams = req.query; const results = await UserService.getAllUsers(queryParams); - const response = await setResponsePaging(queryParams, results, 'Users retrieved successfully'); + const response = await setResponsePaging(queryParams, results, 'Users found'); res.status(response.statusCode).json(response); } @@ -18,7 +18,7 @@ class UserController { const { id } = req.params; const results = await UserService.getUserById(id); - const response = await setResponse(results, 'User retrieved successfully'); + const response = await setResponse(results, 'User found'); res.status(response.statusCode).json(response); } @@ -42,13 +42,14 @@ class UserController { // Update user static async update(req, res) { const { id } = req.params; - const { error, value } = await checkValidate(userSchema, req); + + const { error, value } = await checkValidate(updateUserSchema, req); if (error) { return res.status(400).json(setResponse(error, 'Validation failed', 400)); } - value.updated_by = req.user.user_id; + value.userId = req.user.user_id; const results = await UserService.updateUser(id, value); const response = await setResponse(results, 'User updated successfully'); @@ -56,6 +57,17 @@ class UserController { res.status(response.statusCode).json(response); } + // Approve user + static async approve(req, res) { + const { id } = req.params; + const approverId = req.user.user_id; + + const updatedUser = await UserService.approveUser(id, approverId); + const response = await setResponse(updatedUser, 'User approved successfully'); + + return res.status(response.statusCode).json(response); + } + // Soft delete user static async delete(req, res) { const { id } = req.params; @@ -66,7 +78,7 @@ class UserController { res.status(response.statusCode).json(response); } - // Change user password + // Change password static async changePassword(req, res) { const { id } = req.params; const { error, value } = await checkValidate(newPasswordSchema, req); @@ -80,25 +92,6 @@ class UserController { res.status(response.statusCode).json(response); } - - // Get all status users - static async getAllStatus(req, res) { - const results = await UserService.getAllStatusUsers(); - const response = await setResponse(results, 'Status list retrieved successfully'); - - res.status(response.statusCode).json(response); - } - - // Approve user - static async approve(req, res) { - const { id } = req.params; - const approverId = req.user?.user_id || null; - - const results = await UserService.approveUser(id, approverId); - const response = await setResponse(results, 'User approved successfully'); - - res.status(response.statusCode).json(response); - } } module.exports = UserController; diff --git a/db/user.db.js b/db/user.db.js index bc77252..28f152a 100644 --- a/db/user.db.js +++ b/db/user.db.js @@ -132,6 +132,22 @@ const updateUserDb = async (userId, data) => { return getUserByIdDb(userId); }; +const approveUserDb = async (userId, approverId) => { + const queryText = ` + UPDATE m_users + SET + is_approve = 1, + approved_by = $1, + approved_at = CURRENT_TIMESTAMP, + updated_by = $1, + updated_at = CURRENT_TIMESTAMP + WHERE user_id = $2 AND deleted_at IS NULL + `; + await pool.query(queryText, [approverId, userId]); + return true; // simple, cuma tanda berhasil +}; + + // Change user password const changeUserPasswordDb = async (userId, newPassword) => { const queryText = ` @@ -165,6 +181,7 @@ module.exports = { getUserByUsernameDb, createUserDb, updateUserDb, + approveUserDb, changeUserPasswordDb, deleteUserDb, }; diff --git a/routes/users.route.js b/routes/users.route.js index d8ce102..388e567 100644 --- a/routes/users.route.js +++ b/routes/users.route.js @@ -20,7 +20,4 @@ router.route('/change-password/:id') router.route('/:id/approve') .put(verifyToken.verifyAccessToken, verifyAccess(), UserController.approve); -router.route('/status/all') - .get(verifyToken.verifyAccessToken, UserController.getAllStatus); - module.exports = router; diff --git a/services/user.service.js b/services/user.service.js index 559b4c8..cd923ac 100644 --- a/services/user.service.js +++ b/services/user.service.js @@ -1,174 +1,151 @@ -const { - createUserDb, - getUserByIdDb, +const { getAllUsersDb, + getUserByIdDb, + getUserByUserEmailDb, getUserByUsernameDb, + createUserDb, updateUserDb, + approveUserDb, deleteUserDb, changeUserPasswordDb } = require('../db/user.db'); const { hashPassword } = require('../helpers/hashPassword'); const { ErrorHandler } = require('../helpers/error'); -const statusName = [ - { status: true, status_name: "Aktif" }, - { status: false, status_name: "NonAktif" } -]; - class UserService { - // Get all status users - getAllStatusUsers = async () => { - try { - return statusName; - } catch (error) { - throw new ErrorHandler(error.statusCode || 500, error.message); - } - }; - // Get all users - getAllUsers = async () => { + static async getAllUsers(param) { try { - const results = await getAllUsersDb(); - - results.forEach(user => { - 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; // remove password - }); - + const results = await getAllUsersDb(param); return results; } catch (error) { - throw new ErrorHandler(error.statusCode || 500, error.message); + throw new ErrorHandler(error.statusCode, error.message); } - }; + } // Get user by ID - getUserById = async (id) => { + static async getUserById(id) { try { - const user = await getUserByIdDb(id); - if (!user) throw new ErrorHandler(404, "User not found"); + const result = await getUserByIdDb(id); - 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; + if (!result) throw new ErrorHandler(404, 'User not found'); + + return result; } catch (error) { - throw new ErrorHandler(error.statusCode || 500, error.message); + throw new ErrorHandler(error.statusCode, error.message); } - }; + } - // Create users - createUser = async ({ fullname, name, email, phone, password, role_id = null, is_sa = 0, is_active = 1, approved_by }) => { + // Create user + static async createUser(data) { try { - const existingUser = await getUserByUsernameDb(name); - if (existingUser) throw new ErrorHandler(400, "Username already taken"); + if (!data || typeof data !== 'object') data = {}; - const hashedPassword = await hashPassword(password); + const creatorId = data.userId; - 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() - }); + const existingEmail = await getUserByUserEmailDb(data.user_email); + const existingUsername = await getUserByUsernameDb(data.user_name); - 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) { - throw new ErrorHandler(error.statusCode || 500, error.message); - } - }; - - // Update user - updateUser = async ({ user_id, fullname, name, email, phone, role_id, is_sa, is_active, is_approve, updatedById }) => { - try { - const user = await getUserByIdDb(user_id); - if (!user) throw new ErrorHandler(404, "User not found"); - - // Cek username - if (name && user.user_name.toLowerCase() !== name.toLowerCase()) { - const userByName = await getUserByUsernameDb(name); - if (userByName) throw new ErrorHandler(400, "Username already taken"); + if (existingUsername) { + throw new ErrorHandler(400, 'Username is already taken'); + } + if (existingEmail) { + throw new ErrorHandler(400, 'Email is already taken'); } - const updateData = { - ...(fullname && { user_fullname: fullname }), - ...(name && { user_name: name }), - ...(email && { user_email: email }), - ...(phone && { user_phone: phone }), - ...(role_id !== undefined && { role_id }), - ...(updatedById !== undefined && { updated_by: updatedById }) - }; + if (data.user_password) { + data.user_password = await hashPassword(data.user_password); + } - await updateUserDb(user_id, updateData); + data.is_approve = 1; + data.approved_by = creatorId; + data.created_by = creatorId; + data.updated_by = creatorId; + data.is_sa = 0; + data.is_active = 1; + delete data.userId; - 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; + const result = await createUserDb(data); + return result; } catch (error) { throw new ErrorHandler(error.statusCode || 500, error.message); } - }; + } + + // Update user + static async updateUser(id, data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const existingEmail = await getUserByUserEmailDb(data.user_email); + const existingUsername = await getUserByUsernameDb(data.user_name); + + if (existingUsername) { + throw new ErrorHandler(400, 'Username is already taken'); + } + if (existingEmail) { + throw new ErrorHandler(400, 'Email is already taken') + } + + const userExist = await getUserByIdDb(id); + if (!userExist) throw new ErrorHandler(404, 'User not found'); + + const result = await updateUserDb(id, data); + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } // Approve user - approveUser = async (userId, approverId) => { + static async approveUser(userId, approverId) { try { - const updateData = { - is_approve: 1, - approved_by: approverId, - approved_at: new Date() - }; - await updateUserDb(userId, updateData); + if (!userId) { + throw new ErrorHandler(400, 'User ID is required'); + } - const updatedUser = await getUserByIdDb(userId); - delete updatedUser.user_password; + const existingUser = await getUserByIdDb(userId); + if (!existingUser) { + throw new ErrorHandler(404, 'User not found'); + } + + if (existingUser.is_approve) { + throw new ErrorHandler(400, 'User is already approved'); + } + + const updatedUser = await approveUserDb(userId, approverId); return updatedUser; } catch (error) { throw new ErrorHandler(error.statusCode || 500, error.message); } - }; + } - // Delete user (soft delete) - deleteUser = async (userId, deletedBy) => { + // Soft delete user + static async deleteUser(id, userId) { try { - await deleteUserDb(userId, deletedBy); - return { message: "User deleted successfully" }; + const userExist = await getUserByIdDb(id); + if (!userExist) throw new ErrorHandler(404, 'User not found'); + + const result = await deleteUserDb(id, userId); + return result; } catch (error) { - throw new ErrorHandler(error.statusCode || 500, error.message); + throw new ErrorHandler(error.statusCode, error.message); } - }; + } // Change password - changeUserPassword = async (user_Id, new_Password) => { + static async changeUserPassword(id, newPassword) { try { - const hashedPassword = await hashPassword(new_Password); - await changeUserPasswordDb(user_Id, hashedPassword); - return { message: "Password updated successfully" }; + const userExist = await getUserByIdDb(id); + if (!userExist) throw new ErrorHandler(404, 'User not found'); + + const result = await changeUserPasswordDb(id, newPassword); + return result; } catch (error) { - throw new ErrorHandler(error.statusCode || 500, error.message); + throw new ErrorHandler(error.statusCode, error.message); } - }; + } } -module.exports = new UserService(); +module.exports = UserService; diff --git a/validate/user.schema.js b/validate/user.schema.js index 98dc385..4b7d020 100644 --- a/validate/user.schema.js +++ b/validate/user.schema.js @@ -4,17 +4,17 @@ const Joi = require("joi"); // 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() + user_fullname: Joi.string().min(3).max(100).required(), + user_name: Joi.string().alphanum().min(3).max(50).required(), + user_email: Joi.string().email().required(), + user_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() + user_password: Joi.string() .min(8) .pattern(/[A-Z]/, 'uppercase letter') .pattern(/[a-z]/, 'lowercase letter') @@ -28,6 +28,19 @@ const userSchema = Joi.object({ role_id: Joi.number().integer().min(1) }); +const updateUserSchema = Joi.object({ + user_fullname: Joi.string().min(3).max(100), + user_name: Joi.string().alphanum().min(3).max(50), + user_email: Joi.string().email(), + user_phone: Joi.string() + .pattern(/^(?:\+62|0)8\d{7,10}$/) + .messages({ + 'string.pattern.base': + 'Phone number must be a valid Indonesian number in format +628XXXXXXXXX' + }), + role_id: Joi.number().integer().min(1) +}).min(1); + const newPasswordSchema = Joi.object({ new_password: Joi.string() .min(8) @@ -45,4 +58,5 @@ const newPasswordSchema = Joi.object({ module.exports = { userSchema, newPasswordSchema, + updateUserSchema }; \ No newline at end of file From 988bcaf301279d7cb2228309e9d8b2090b89673f Mon Sep 17 00:00:00 2001 From: Muhammad Afif Date: Sat, 11 Oct 2025 12:13:08 +0700 Subject: [PATCH 085/126] add: schedule db --- controllers/schedule.controller.js | 0 controllers/shift.controller.js | 0 db/roles.db.js | 26 +++++-- db/schedule.db.js | 117 +++++++++++++++++++++++++++++ db/shift.db.js | 0 routes/schedule.route.js | 0 routes/shift.route.js | 0 services/schedule.service.js | 0 services/shift.service.js | 0 utils/date.js | 8 ++ validate/schedule.schema.js | 0 validate/shift.schema.js | 0 12 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 controllers/schedule.controller.js create mode 100644 controllers/shift.controller.js create mode 100644 db/schedule.db.js create mode 100644 db/shift.db.js create mode 100644 routes/schedule.route.js create mode 100644 routes/shift.route.js create mode 100644 services/schedule.service.js create mode 100644 services/shift.service.js create mode 100644 utils/date.js create mode 100644 validate/schedule.schema.js create mode 100644 validate/shift.schema.js diff --git a/controllers/schedule.controller.js b/controllers/schedule.controller.js new file mode 100644 index 0000000..e69de29 diff --git a/controllers/shift.controller.js b/controllers/shift.controller.js new file mode 100644 index 0000000..e69de29 diff --git a/db/roles.db.js b/db/roles.db.js index 82215e8..f6bd051 100644 --- a/db/roles.db.js +++ b/db/roles.db.js @@ -14,7 +14,11 @@ const getAllRolesDb = async (searchParams = {}) => { const { whereConditions, whereParamAnd } = pool.buildFilterQuery( [ { column: "r.role_name", param: searchParams.role_name, type: "string" }, - { column: "r.role_level", param: searchParams.role_level, type: "number" }, + { + column: "r.role_level", + param: searchParams.role_level, + type: "number", + }, ], queryParams ); @@ -38,14 +42,15 @@ const getAllRolesDb = async (searchParams = {}) => { WHERE r.deleted_at IS NULL ${whereConditions.length > 0 ? `AND ${whereConditions.join(" AND ")}` : ""} ORDER BY r.role_id ASC - ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ''}; + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""}; `; const result = await pool.query(queryText, queryParams); - const total = result?.recordset.length > 0 - ? parseInt(result.recordset[0].total_data, 10) - : 0; + const total = + result?.recordset.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; return { data: result.recordset, total }; }; @@ -75,7 +80,10 @@ const getRolesByIdDb = async (id) => { const createRolesDB = async (data) => { const store = { ...data }; - const { query: queryText, values } = pool.buildDynamicInsert("m_roles", store); + const { query: queryText, values } = pool.buildDynamicInsert( + "m_roles", + store + ); const result = await pool.query(queryText, values); const insertedId = result.recordset[0]?.inserted_id; @@ -87,7 +95,11 @@ const updateRolesDb = async (id, data) => { const store = { ...data }; const whereData = { role_id: id }; - const { query: queryText, values } = pool.buildDynamicUpdate("m_roles", store, whereData); + const { query: queryText, values } = pool.buildDynamicUpdate( + "m_roles", + store, + whereData + ); await pool.query(`${queryText} AND deleted_at IS NULL`, values); return getRolesByIdDb(id); diff --git a/db/schedule.db.js b/db/schedule.db.js new file mode 100644 index 0000000..7d6e2bc --- /dev/null +++ b/db/schedule.db.js @@ -0,0 +1,117 @@ +const pool = require("../config"); +const formattedDate = require("../utils/date"); + +// Get all schedules +const getAllScheduleDb = async (searchParams = {}) => { + let queryParams = []; + + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1; + queryParams = [Number(searchParams.limit ?? 10), page]; + } + + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + ["a.schedule_date"], + searchParams.criteria, + queryParams + ); + if (whereParamOr) queryParams = whereParamOr; + + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [{ column: "a.schedule_date", param: searchParams.name, type: "date" }], + queryParams + ); + if (whereParamAnd) queryParams = whereParamAnd; + + const queryText = ` + SELECT + COUNT(*) OVER() AS total_data, + a.*, + b.user_id, + c.shift_id + FROM schedule a + LEFT JOIN m_shift b ON a.shift_id = b.shift_id + LEFT JOIN user_schedule c ON a.user_schedule_id = c.user_schedule_id + WHERE a.deleted_at IS NULL + ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} + ${whereOrConditions ? ` ${whereOrConditions}` : ""} + ORDER BY a.schedule_id ASC + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""} + `; + + const result = await pool.query(queryText, queryParams); + + const data = result.recordset.map((item) => ({ + ...item, + schedule_date: item.schedule_date + ? formattedDate(item.schedule_date) + : null, + })); + + const total = + result?.recordset?.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; + + return { data, total }; +}; + +const getScheduleByIdDb = async (id) => { + const queryText = ` + SELECT + COUNT(*) OVER() AS total_data, + a.*, + b.shift_id, + b.shift_name, + b.start_time, + b.end_time, + c.user_schedule_id, + c.user_id + FROM schedule a + LEFT JOIN m_shift b ON a.shift_id = b.shift_id + LEFT JOIN user_schedule c ON a.user_schedule_id = c.user_schedule_id + WHERE a.schedule_id = $1 AND a.deleted_at IS NULL + `; + const result = await pool.query(queryText, [id]); + + const data = result.recordset.map((item) => ({ + ...item, + schedule_date: item.schedule_date + ? formattedDate(item.schedule_date) + : null, + })); + + return data; +}; + +const updateScheduleDb = async (id, data) => { + const store = { ...data }; + const whereData = { schedule_id: id }; + + const { query: queryText, values } = pool.buildDynamicUpdate( + "schedule", + store, + whereData + ); + + await pool.query(`${queryText} AND deleted_at IS NULL`, values); + return getScheduleByIdDb(id); +}; + +// Soft delete schedule +const deleteScheduleDb = async (id, deletedBy) => { + const queryText = ` + UPDATE schedule + SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 + WHERE schedule_id = $2 AND deleted_at IS NULL + `; + await pool.query(queryText, [deletedBy, id]); + return true; +}; + +module.exports = { + getAllScheduleDb, + getScheduleByIdDb, + updateScheduleDb, + deleteScheduleDb, +}; diff --git a/db/shift.db.js b/db/shift.db.js new file mode 100644 index 0000000..e69de29 diff --git a/routes/schedule.route.js b/routes/schedule.route.js new file mode 100644 index 0000000..e69de29 diff --git a/routes/shift.route.js b/routes/shift.route.js new file mode 100644 index 0000000..e69de29 diff --git a/services/schedule.service.js b/services/schedule.service.js new file mode 100644 index 0000000..e69de29 diff --git a/services/shift.service.js b/services/shift.service.js new file mode 100644 index 0000000..e69de29 diff --git a/utils/date.js b/utils/date.js new file mode 100644 index 0000000..400d9ce --- /dev/null +++ b/utils/date.js @@ -0,0 +1,8 @@ +module.exports = { + formattedDate: (timestamp) => { + let date = new Date(timestamp); + let options = { day: "numeric", month: "long", year: "numeric" }; + let formattedDate = date.toISOString("id-ID", options); + return formattedDate; + }, + }; \ No newline at end of file diff --git a/validate/schedule.schema.js b/validate/schedule.schema.js new file mode 100644 index 0000000..e69de29 diff --git a/validate/shift.schema.js b/validate/shift.schema.js new file mode 100644 index 0000000..e69de29 From 47051766e318ddb0a3c2b3419e41fa2e4ea56573 Mon Sep 17 00:00:00 2001 From: Muhammad Afif Date: Sat, 11 Oct 2025 12:16:24 +0700 Subject: [PATCH 086/126] add: schedule db --- db/schedule.db.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/db/schedule.db.js b/db/schedule.db.js index 7d6e2bc..294d155 100644 --- a/db/schedule.db.js +++ b/db/schedule.db.js @@ -31,7 +31,7 @@ const getAllScheduleDb = async (searchParams = {}) => { c.shift_id FROM schedule a LEFT JOIN m_shift b ON a.shift_id = b.shift_id - LEFT JOIN user_schedule c ON a.user_schedule_id = c.user_schedule_id + LEFT JOIN user_schedule c ON a.schedule_id = c.user_schedule_id WHERE a.deleted_at IS NULL ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} ${whereOrConditions ? ` ${whereOrConditions}` : ""} @@ -59,7 +59,6 @@ const getAllScheduleDb = async (searchParams = {}) => { const getScheduleByIdDb = async (id) => { const queryText = ` SELECT - COUNT(*) OVER() AS total_data, a.*, b.shift_id, b.shift_name, @@ -69,7 +68,7 @@ const getScheduleByIdDb = async (id) => { c.user_id FROM schedule a LEFT JOIN m_shift b ON a.shift_id = b.shift_id - LEFT JOIN user_schedule c ON a.user_schedule_id = c.user_schedule_id + LEFT JOIN user_schedule c ON a.schedule_id = c.user_schedule_id WHERE a.schedule_id = $1 AND a.deleted_at IS NULL `; const result = await pool.query(queryText, [id]); From 7c55e786f31430fbc90ffe18af6a04c701cb2125 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Sat, 11 Oct 2025 16:20:22 +0700 Subject: [PATCH 087/126] add: sub section db --- db/sub_section.db.js | 108 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 db/sub_section.db.js diff --git a/db/sub_section.db.js b/db/sub_section.db.js new file mode 100644 index 0000000..e4358d7 --- /dev/null +++ b/db/sub_section.db.js @@ -0,0 +1,108 @@ +const pool = require("../config"); + +// Get all sub sections +const getAllSubSectionsDb = async (searchParams = {}) => { + let queryParams = []; + + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1; + queryParams = [Number(searchParams.limit ?? 10), page]; + } + + // OR condition (pencarian bebas) + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + ["a.sub_section_code", "a.sub_section_name"], + searchParams.criteria, + queryParams + ); + + queryParams = whereParamOr ?? queryParams; + + // AND condition (filter spesifik) + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [ + { column: "a.sub_section_code", param: searchParams.code, type: "string" }, + { column: "a.sub_section_name", param: searchParams.name, type: "string" }, + ], + queryParams + ); + + queryParams = whereParamAnd ?? queryParams; + + // Query utama + const queryText = ` + SELECT COUNT(*) OVER() AS total_data, a.* + FROM m_plant_sub_section a + WHERE a.deleted_at IS NULL + ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} + ${whereOrConditions ? whereOrConditions : ""} + ORDER BY a.sub_section_id ASC + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""}; + `; + + const result = await pool.query(queryText, queryParams); + const total = + result?.recordset.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; + + return { data: result.recordset, total }; +}; + +// Get sub section by ID +const getSubSectionByIdDb = async (id) => { + const queryText = ` + SELECT a.* + FROM m_plant_sub_section a + WHERE a.sub_section_id = $1 AND a.deleted_at IS NULL + `; + const result = await pool.query(queryText, [id]); + return result.recordset; +}; + +// Create new sub section +const createSubSectionDb = async (data) => { + // Generate kode otomatis + const newCode = await pool.generateKode("SUB", "m_plant_sub_section", "sub_section_code"); + + const store = { + ...data, + sub_section_code: newCode + }; + + const { query: queryText, values } = pool.buildDynamicInsert("m_plant_sub_section", store); + const result = await pool.query(queryText, values); + const insertedId = result.recordset[0]?.inserted_id; + + return insertedId ? await getSubSectionByIdDb(insertedId) : null; +}; + +// Update sub section +const updateSubSectionDb = async (id, data) => { + const store = { ...data }; + const whereData = { sub_section_id: id }; + + const { query: queryText, values } = pool.buildDynamicUpdate("m_plant_sub_section", store, whereData); + await pool.query(`${queryText} AND deleted_at IS NULL`, values); + + return getSubSectionByIdDb(id); +}; + +// Soft delete sub section +const deleteSubSectionDb = async (id, deletedBy) => { + const queryText = ` + UPDATE m_plant_sub_section + SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 + WHERE sub_section_id = $2 AND deleted_at IS NULL + `; + await pool.query(queryText, [deletedBy, id]); + return true; +}; + +module.exports = { + getAllSubSectionsDb, + getSubSectionByIdDb, + createSubSectionDb, + updateSubSectionDb, + deleteSubSectionDb, +}; From ace419fa3eed21cb127b17d91f6e55db2d500f88 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Sat, 11 Oct 2025 16:20:54 +0700 Subject: [PATCH 088/126] add: validate sub section --- validate/sub_section.schema.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 validate/sub_section.schema.js diff --git a/validate/sub_section.schema.js b/validate/sub_section.schema.js new file mode 100644 index 0000000..616ac90 --- /dev/null +++ b/validate/sub_section.schema.js @@ -0,0 +1,31 @@ +const Joi = require("joi"); + +// ======================== +// Plant Sub Section Validation +// ======================== +const insertSubSectionSchema = Joi.object({ + sub_section_name: Joi.string() + .max(200) + .required() + .messages({ + "string.base": "Sub section name must be a string", + "string.max": "Sub section name cannot exceed 200 characters", + "any.required": "Sub section name is required" + }), +}); + +const updateSubSectionSchema = Joi.object({ + sub_section_name: Joi.string() + .max(200) + .messages({ + "string.base": "Sub section name must be a string", + "string.max": "Sub section name cannot exceed 200 characters", + }), +}).min(1).messages({ + "object.min": "At least one field must be provided to update", +}); + +module.exports = { + insertSubSectionSchema, + updateSubSectionSchema +}; From d5c53b2953949ecde813279dcd5674f293508d7e Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Sat, 11 Oct 2025 16:21:14 +0700 Subject: [PATCH 089/126] add: crud subsection --- controllers/sub_section.controller.js | 71 ++++++++++++++++++++++ services/sub_section.service.js | 87 +++++++++++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 controllers/sub_section.controller.js create mode 100644 services/sub_section.service.js diff --git a/controllers/sub_section.controller.js b/controllers/sub_section.controller.js new file mode 100644 index 0000000..f2a9655 --- /dev/null +++ b/controllers/sub_section.controller.js @@ -0,0 +1,71 @@ +const SubSectionService = require('../services/sub_section.service'); +const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); +const { insertSubSectionSchema, updateSubSectionSchema } = require('../validate/sub_section.schema'); + +class SubSectionController { + // Get all sub sections + static async getAll(req, res) { + const queryParams = req.query; + + const results = await SubSectionService.getAll(queryParams); + const response = await setResponsePaging(queryParams, results, 'Sub section found'); + + res.status(response.statusCode).json(response); + } + + // Get sub section by ID + static async getById(req, res) { + const { id } = req.params; + + const results = await SubSectionService.getById(id); + const response = await setResponse(results, 'Sub section found'); + + res.status(response.statusCode).json(response); + } + + // Create sub section + static async create(req, res) { + const { error, value } = await checkValidate(insertSubSectionSchema, req); + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id; + + const results = await SubSectionService.create(value); + const response = await setResponse(results, 'Sub section created successfully'); + + return res.status(response.statusCode).json(response); + } + + // Update sub section + static async update(req, res) { + const { id } = req.params; + + const { error, value } = checkValidate(updateSubSectionSchema, req); + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id; + + const results = await SubSectionService.update(id, value); + const response = await setResponse(results, 'Sub section updated successfully'); + + res.status(response.statusCode).json(response); + } + + // Soft delete sub section + static async delete(req, res) { + const { id } = req.params; + + const results = await SubSectionService.delete(id, req.user.user_id); + const response = await setResponse(results, 'Sub section deleted successfully'); + + res.status(response.statusCode).json(response); + } +} + +module.exports = SubSectionController; \ No newline at end of file diff --git a/services/sub_section.service.js b/services/sub_section.service.js new file mode 100644 index 0000000..8860bfd --- /dev/null +++ b/services/sub_section.service.js @@ -0,0 +1,87 @@ +const { + getAllSubSectionsDb, + getSubSectionByIdDb, + createSubSectionDb, + updateSubSectionDb, + deleteSubSectionDb +} = require('../db/sub_section.db'); +const { ErrorHandler } = require('../helpers/error'); + +class SubSectionService { + // Get all sub sections + static async getAll(param) { + try { + const results = await getAllSubSectionsDb(param); + + results.data.map(el => {}); + + return results; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Get sub section by ID + static async getById(id) { + try { + const result = await getSubSectionByIdDb(id); + + if (result.length < 1) throw new ErrorHandler(404, 'Sub section not found'); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Create sub section + static async create(data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const result = await createSubSectionDb(data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Update sub section + static async update(id, data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const dataExist = await getSubSectionByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Sub section not found'); + } + + const result = await updateSubSectionDb(id, data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Soft delete sub section + static async delete(id, userId) { + try { + const dataExist = await getSubSectionByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Sub section not found'); + } + + const result = await deleteSubSectionDb(id, userId); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } +} + +module.exports = SubSectionService; From 95189e20141e66869c4a54ab6663e4c63c78be3a Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Sat, 11 Oct 2025 16:21:38 +0700 Subject: [PATCH 090/126] add: sub section sroute --- routes/index.js | 4 +++- routes/sub_section.route.js | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 routes/sub_section.route.js diff --git a/routes/index.js b/routes/index.js index de52665..f3480a7 100644 --- a/routes/index.js +++ b/routes/index.js @@ -4,11 +4,13 @@ const users = require("./users.route"); const device = require('./device.route'); const roles = require('./roles.route') const tags = require("./tags.route") +const subSection = require("./sub_section.route") router.use("/auth", auth); router.use("/user", users); router.use("/device", device); router.use("/roles", roles); -router.use("/tags", tags) +router.use("/tags", tags); +router.use("/plant-sub-section", subSection); module.exports = router; diff --git a/routes/sub_section.route.js b/routes/sub_section.route.js new file mode 100644 index 0000000..58adde9 --- /dev/null +++ b/routes/sub_section.route.js @@ -0,0 +1,17 @@ +const express = require('express'); +const PlantSubSectionController = require('../controllers/sub_section.controller'); +const verifyToken = require('../middleware/verifyToken'); +const verifyAccess = require('../middleware/verifyAccess'); + +const router = express.Router(); + +router.route('/') + .get(verifyToken.verifyAccessToken, PlantSubSectionController.getAll) + .post(verifyToken.verifyAccessToken, verifyAccess(), PlantSubSectionController.create); + +router.route('/:id') + .get(verifyToken.verifyAccessToken, PlantSubSectionController.getById) + .put(verifyToken.verifyAccessToken, verifyAccess(), PlantSubSectionController.update) + .delete(verifyToken.verifyAccessToken, verifyAccess(), PlantSubSectionController.delete); + +module.exports = router; \ No newline at end of file From 662038d9532a9874cb4d82d1dc47c73d7bdd8209 Mon Sep 17 00:00:00 2001 From: Muhammad Afif Date: Mon, 13 Oct 2025 10:54:39 +0700 Subject: [PATCH 091/126] add: CRUD shift --- controllers/shift.controller.js | 71 ++++++++++++++++++++ db/schedule.db.js | 8 ++- db/shift.db.js | 112 ++++++++++++++++++++++++++++++++ routes/index.js | 3 + routes/shift.route.js | 17 +++++ services/shift.service.js | 88 +++++++++++++++++++++++++ utils/time.js | 11 ++++ validate/shift.schema.js | 42 ++++++++++++ 8 files changed, 350 insertions(+), 2 deletions(-) create mode 100644 utils/time.js diff --git a/controllers/shift.controller.js b/controllers/shift.controller.js index e69de29..ce5ae5c 100644 --- a/controllers/shift.controller.js +++ b/controllers/shift.controller.js @@ -0,0 +1,71 @@ +const ShiftService = require('../services/shift.service'); +const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); +const { updateShiftSchema, insertShiftSchema } = require('../validate/shift.schema'); + +class ShiftController { + // Get all Shift + static async getAll(req, res) { + const queryParams = req.query; + + const results = await ShiftService.getAllShift(queryParams); + const response = await setResponsePaging(queryParams, results, 'Shift found') + + res.status(response.statusCode).json(response); + } + + // Get Shift by ID + static async getById(req, res) { + const { id } = req.params; + + const results = await ShiftService.getShiftById(id); + const response = await setResponse(results, 'Shift found') + + res.status(response.statusCode).json(response); + } + + // Create Shift + static async create(req, res) { + const { error, value } = await checkValidate(insertShiftSchema, req) + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id + + const results = await ShiftService.createShift(value); + const response = await setResponse(results, 'Shift created successfully') + + return res.status(response.statusCode).json(response); + } + + // Update Shift + static async update(req, res) { + const { id } = req.params; + + const { error, value } = checkValidate(updateShiftSchema, req) + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id + + const results = await ShiftService.updateShift(id, value); + const response = await setResponse(results, 'Shift updated successfully') + + res.status(response.statusCode).json(response); + } + + // Soft delete Shift + static async delete(req, res) { + const { id } = req.params; + + const results = await ShiftService.deleteShift(id, req.user.user_id); + const response = await setResponse(results, 'Shift deleted successfully') + + res.status(response.statusCode).json(response); + } +} + +module.exports = ShiftController; diff --git a/db/schedule.db.js b/db/schedule.db.js index 294d155..8c0df04 100644 --- a/db/schedule.db.js +++ b/db/schedule.db.js @@ -27,8 +27,12 @@ const getAllScheduleDb = async (searchParams = {}) => { SELECT COUNT(*) OVER() AS total_data, a.*, - b.user_id, - c.shift_id + b.shift_id, + b.shift_name, + b.start_time, + b.end_time, + c.user_schedule_id, + c.user_id FROM schedule a LEFT JOIN m_shift b ON a.shift_id = b.shift_id LEFT JOIN user_schedule c ON a.schedule_id = c.user_schedule_id diff --git a/db/shift.db.js b/db/shift.db.js index e69de29..fd62f5c 100644 --- a/db/shift.db.js +++ b/db/shift.db.js @@ -0,0 +1,112 @@ +const pool = require("../config"); + +// Get all roles +const getAllShiftDb = async (searchParams = {}) => { + let queryParams = []; + + // Pagination + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1; + queryParams = [Number(searchParams.limit ?? 10), page]; + } + + // Filtering + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [ + { column: "a.shift_name", param: searchParams.shift_name, type: "string" }, + { + column: "a.start_time", + param: searchParams.start_time, + type: "time", + }, + { + column: "a.end_time", + param: searchParams.end_time, + type: "time", + }, + ], + queryParams + ); + + queryParams = whereParamAnd ? whereParamAnd : queryParams; + + const queryText = ` + SELECT + COUNT(*) OVER() AS total_data, + a.* + FROM m_shift a + WHERE a.deleted_at IS NULL + ${whereConditions.length > 0 ? `AND ${whereConditions.join(" AND ")}` : ""} + ORDER BY a.shift_id ASC + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""}; + `; + + const result = await pool.query(queryText, queryParams); + + const total = + result?.recordset.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; + + return { data: result.recordset, total }; +}; + +// Get role by ID +const getShiftByIdDb = async (id) => { + const queryText = ` + SELECT + a.* + FROM m_shift a + WHERE a.shift_id = $1 AND a.deleted_at IS NULL + `; + const result = await pool.query(queryText, [id]); + return result.recordset; +}; + +// Create role +const createShiftDB = async (data) => { + const store = { ...data }; + + const { query: queryText, values } = pool.buildDynamicInsert( + "m_shift", + store + ); + const result = await pool.query(queryText, values); + + const insertedId = result.recordset[0]?.inserted_id; + return insertedId ? await getShiftByIdDb(insertedId) : null; +}; + +// Update role +const updateShiftDb = async (id, data) => { + const store = { ...data }; + const whereData = { shift_id: id }; + + const { query: queryText, values } = pool.buildDynamicUpdate( + "m_shift", + store, + whereData + ); + await pool.query(`${queryText} AND deleted_at IS NULL`, values); + + return getShiftByIdDb(id); +}; + +const deleteShiftDb = async (id, deletedBy) => { + const queryText = ` + UPDATE m_shift + SET deleted_at = CURRENT_TIMESTAMP, + deleted_by = $1 + WHERE shift_id = $2 AND deleted_at IS NULL + `; + await pool.query(queryText, [deletedBy, id]); + return true; +}; + +module.exports = { + getAllShiftDb, + getShiftByIdDb, + createShiftDB, + updateShiftDb, + deleteShiftDb, +}; diff --git a/routes/index.js b/routes/index.js index f3480a7..7e351bb 100644 --- a/routes/index.js +++ b/routes/index.js @@ -5,6 +5,7 @@ const device = require('./device.route'); const roles = require('./roles.route') const tags = require("./tags.route") const subSection = require("./sub_section.route") +const shift = require("./shift.route") router.use("/auth", auth); router.use("/user", users); @@ -12,5 +13,7 @@ router.use("/device", device); router.use("/roles", roles); router.use("/tags", tags); router.use("/plant-sub-section", subSection); +router.use("/shift", shift) + module.exports = router; diff --git a/routes/shift.route.js b/routes/shift.route.js index e69de29..322bb81 100644 --- a/routes/shift.route.js +++ b/routes/shift.route.js @@ -0,0 +1,17 @@ +const express = require('express'); +const ShiftController = require('../controllers/shift.controller'); +const verifyToken = require("../middleware/verifyToken") +const verifyAccess = require("../middleware/verifyAccess") + +const router = express.Router(); + +router.route("/") + .get(verifyToken.verifyAccessToken, ShiftController.getAll) + .post(verifyToken.verifyAccessToken, verifyAccess(), ShiftController.create); + +router.route("/:id") + .get(verifyToken.verifyAccessToken, ShiftController.getById) + .put(verifyToken.verifyAccessToken, verifyAccess(), ShiftController.update) + .delete(verifyToken.verifyAccessToken, verifyAccess(), ShiftController.delete); + +module.exports = router; \ No newline at end of file diff --git a/services/shift.service.js b/services/shift.service.js index e69de29..93439d2 100644 --- a/services/shift.service.js +++ b/services/shift.service.js @@ -0,0 +1,88 @@ +const { + getAllShiftDb, + getShiftByIdDb, + createShiftDB, + updateShiftDb, + deleteShiftDb +} = require('../db/shift.db'); +const { ErrorHandler } = require('../helpers/error'); + +class ShiftService { + // Get all Shift + static async getAllShift(param) { + try { + const results = await getAllShiftDb(param); + + results.data.map(element => { + }); + + return results + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Get Shift by ID + static async getShiftById(id) { + try { + const result = await getShiftByIdDb(id); + + if (result.length < 1) throw new ErrorHandler(404, 'Shift not found'); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Create Shift + static async createShift(data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const result = await createShiftDB(data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Update Shift + static async updateShift(id, data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const dataExist = await getShiftByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Shift not found'); + } + + const result = await updateShiftDb(id, data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Soft delete Shift + static async deleteShift(id, userId) { + try { + const dataExist = await getShiftByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Shift not found'); + } + + const result = await deleteShiftDb(id, userId); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } +} + +module.exports = ShiftService; diff --git a/utils/time.js b/utils/time.js new file mode 100644 index 0000000..9a982d7 --- /dev/null +++ b/utils/time.js @@ -0,0 +1,11 @@ +module.exports = { + formattedTime: (timestamp) => { + let date = new Date(timestamp); + let hours = date.getHours().toString().padStart(2, "0"); + let minutes = date.getMinutes().toString().padStart(2, "0"); + let seconds = date.getSeconds().toString().padStart(2, "0"); + let formattedTime = `${hours}:${minutes}:${seconds}`; + return formattedTime; + }, + }; + \ No newline at end of file diff --git a/validate/shift.schema.js b/validate/shift.schema.js index e69de29..49c6981 100644 --- a/validate/shift.schema.js +++ b/validate/shift.schema.js @@ -0,0 +1,42 @@ +// ======================== +// Device Validation + +const Joi = require("joi"); + +// ======================== +const timePattern = /^([01]\d|2[0-3]):([0-5]\d)(:[0-5]\d)?$/; + +const insertShiftSchema = Joi.object({ + shift_name: Joi.string().max(100).required(), + start_time: Joi.string() + .pattern(timePattern) + .required() + .messages({ + "string.pattern.base": "start_time harus dalam format HH:mm atau HH:mm:ss", + }), + end_time: Joi.string() + .pattern(timePattern) + .required() + .messages({ + "string.pattern.base": "end_time harus dalam format HH:mm atau HH:mm:ss", + }), +}); + +const updateShiftSchema = Joi.object({ + shift_name: Joi.string().max(100), + start_time: Joi.string() + .pattern(timePattern) + .messages({ + "string.pattern.base": "start_time harus dalam format HH:mm atau HH:mm:ss", + }), + end_time: Joi.string() + .pattern(timePattern) + .messages({ + "string.pattern.base": "end_time harus dalam format HH:mm atau HH:mm:ss", + }), +}).min(1); + +module.exports = { + insertShiftSchema, + updateShiftSchema, +}; From 8de83c27166391d38d0d3d1210b1fa99c285ba18 Mon Sep 17 00:00:00 2001 From: Muhammad Afif Date: Mon, 13 Oct 2025 10:56:47 +0700 Subject: [PATCH 092/126] repair: tags db --- db/tags.db.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/db/tags.db.js b/db/tags.db.js index faae300..ef93e96 100644 --- a/db/tags.db.js +++ b/db/tags.db.js @@ -59,7 +59,7 @@ const getAllTagsDb = async (searchParams = {}) => { c.sub_section_name FROM m_tags a LEFT JOIN m_device b ON a.device_id = b.device_id - LEFT JOIN plant_sub_section c ON a.sub_section_id = c.sub_section_id + LEFT JOIN m_plant_subsection c ON a.sub_section_id = c.sub_section_id WHERE a.deleted_at IS NULL ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} ${whereOrConditions ? ` ${whereOrConditions}` : ""} @@ -87,7 +87,7 @@ const getTagsByIdDb = async (id) => { c.sub_section_name FROM m_tags a LEFT JOIN m_device b ON a.device_id = b.device_id - LEFT JOIN plant_sub_section c ON a.sub_section_id = c.sub_section_id + LEFT JOIN m_plant_subsection c ON a.sub_section_id = c.sub_section_id WHERE a.tag_id = $1 AND a.deleted_at IS NULL `; const result = await pool.query(queryText, [id]); From 9b8cb9d752ba28f505a9d3364689ac9ece626543 Mon Sep 17 00:00:00 2001 From: Muhammad Afif Date: Mon, 13 Oct 2025 11:52:09 +0700 Subject: [PATCH 093/126] repair: shift db & roles --- db/roles.db.js | 89 ++++++++++++++------------------------- db/shift.db.js | 63 +++++++++++---------------- services/roles.service.js | 4 +- services/shift.service.js | 4 +- utils/time.js | 11 ----- 5 files changed, 60 insertions(+), 111 deletions(-) delete mode 100644 utils/time.js diff --git a/db/roles.db.js b/db/roles.db.js index f6bd051..56c42c5 100644 --- a/db/roles.db.js +++ b/db/roles.db.js @@ -1,96 +1,71 @@ const pool = require("../config"); -// Get all roles const getAllRolesDb = async (searchParams = {}) => { let queryParams = []; - // Pagination + // Handle pagination if (searchParams.limit) { const page = Number(searchParams.page ?? 1) - 1; queryParams = [Number(searchParams.limit ?? 10), page]; } - // Filtering + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + ["a.role_name", "a.role_level", "a.role_description"], + searchParams.criteria, + queryParams + ); + if (whereParamOr) queryParams = whereParamOr; + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( [ - { column: "r.role_name", param: searchParams.role_name, type: "string" }, - { - column: "r.role_level", - param: searchParams.role_level, - type: "number", - }, + { column: "a.role_name", param: searchParams.role_name, type: "string" }, + { column: "a.role_level", param: searchParams.start_time, type: "string" }, + { column: "a.role_description", param: searchParams.role_description, type: "string" }, ], queryParams ); - - queryParams = whereParamAnd ? whereParamAnd : queryParams; + if (whereParamAnd) queryParams = whereParamAnd; const queryText = ` SELECT - COUNT(*) OVER() AS total_data, - r.role_id, - r.role_name, - r.role_description, - r.role_level, - r.created_at, - r.updated_at, - r.updated_by, - r.deleted_at, - r.deleted_by, - r.created_by - FROM m_roles r - WHERE r.deleted_at IS NULL - ${whereConditions.length > 0 ? `AND ${whereConditions.join(" AND ")}` : ""} - ORDER BY r.role_id ASC - ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""}; + COUNT(*) OVER() AS total_data, + a.* + FROM m_roles a + WHERE a.deleted_at IS NULL + ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} + ${whereOrConditions ? ` ${whereOrConditions}` : ""} + ORDER BY a.role_id ASC + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""} `; const result = await pool.query(queryText, queryParams); - const total = - result?.recordset.length > 0 + result?.recordset?.length > 0 ? parseInt(result.recordset[0].total_data, 10) : 0; return { data: result.recordset, total }; }; -// Get role by ID const getRolesByIdDb = async (id) => { const queryText = ` SELECT - r.role_id, - r.role_name, - r.role_description, - r.role_level, - r.created_at, - r.updated_at, - r.updated_by, - r.deleted_at, - r.deleted_by, - r.created_by - FROM m_roles r - WHERE r.role_id = $1 AND r.deleted_at IS NULL + a.* + FROM m_roles a + WHERE a.role_id = $1 AND a.deleted_at IS NULL `; const result = await pool.query(queryText, [id]); - return result.recordset; + return result.recordset?.[0] || null; }; -// Create role -const createRolesDB = async (data) => { - const store = { ...data }; - - const { query: queryText, values } = pool.buildDynamicInsert( - "m_roles", - store - ); +const insertRolesDb = async (store) => { + const { query: queryText, values } = pool.buildDynamicInsert("m_roles", store); const result = await pool.query(queryText, values); + const insertedId = result.recordset?.[0]?.inserted_id; - const insertedId = result.recordset[0]?.inserted_id; return insertedId ? await getRolesByIdDb(insertedId) : null; }; -// Update role const updateRolesDb = async (id, data) => { const store = { ...data }; const whereData = { role_id: id }; @@ -100,17 +75,15 @@ const updateRolesDb = async (id, data) => { store, whereData ); - await pool.query(`${queryText} AND deleted_at IS NULL`, values); + await pool.query(`${queryText} AND deleted_at IS NULL`, values); return getRolesByIdDb(id); }; -// Soft delete role const deleteRolesDb = async (id, deletedBy) => { const queryText = ` UPDATE m_roles - SET deleted_at = CURRENT_TIMESTAMP, - deleted_by = $1 + SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 WHERE role_id = $2 AND deleted_at IS NULL `; await pool.query(queryText, [deletedBy, id]); @@ -120,7 +93,7 @@ const deleteRolesDb = async (id, deletedBy) => { module.exports = { getAllRolesDb, getRolesByIdDb, - createRolesDB, + insertRolesDb, updateRolesDb, deleteRolesDb, }; diff --git a/db/shift.db.js b/db/shift.db.js index fd62f5c..74b0091 100644 --- a/db/shift.db.js +++ b/db/shift.db.js @@ -1,83 +1,71 @@ const pool = require("../config"); -// Get all roles const getAllShiftDb = async (searchParams = {}) => { let queryParams = []; - // Pagination + // Handle pagination if (searchParams.limit) { const page = Number(searchParams.page ?? 1) - 1; queryParams = [Number(searchParams.limit ?? 10), page]; } - // Filtering + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + ["a.shift_name", "a.start_time", "a.end_time"], + searchParams.criteria, + queryParams + ); + if (whereParamOr) queryParams = whereParamOr; + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( [ - { column: "a.shift_name", param: searchParams.shift_name, type: "string" }, - { - column: "a.start_time", - param: searchParams.start_time, - type: "time", - }, - { - column: "a.end_time", - param: searchParams.end_time, - type: "time", - }, + { column: "a.shift_name", param: searchParams.name, type: "string" }, + { column: "a.start_time", param: searchParams.start_time, type: "time" }, + { column: "a.end_time", param: searchParams.end_time, type: "time" }, ], queryParams ); - - queryParams = whereParamAnd ? whereParamAnd : queryParams; + if (whereParamAnd) queryParams = whereParamAnd; const queryText = ` SELECT - COUNT(*) OVER() AS total_data, + COUNT(*) OVER() AS total_data, a.* FROM m_shift a WHERE a.deleted_at IS NULL - ${whereConditions.length > 0 ? `AND ${whereConditions.join(" AND ")}` : ""} + ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} + ${whereOrConditions ? ` ${whereOrConditions}` : ""} ORDER BY a.shift_id ASC - ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""}; + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""} `; const result = await pool.query(queryText, queryParams); - const total = - result?.recordset.length > 0 + result?.recordset?.length > 0 ? parseInt(result.recordset[0].total_data, 10) : 0; return { data: result.recordset, total }; }; -// Get role by ID const getShiftByIdDb = async (id) => { const queryText = ` SELECT - a.* + a.* FROM m_shift a WHERE a.shift_id = $1 AND a.deleted_at IS NULL `; const result = await pool.query(queryText, [id]); - return result.recordset; + return result.recordset?.[0] || null; }; -// Create role -const createShiftDB = async (data) => { - const store = { ...data }; - - const { query: queryText, values } = pool.buildDynamicInsert( - "m_shift", - store - ); +const insertShiftDb = async (store) => { + const { query: queryText, values } = pool.buildDynamicInsert("m_shift", store); const result = await pool.query(queryText, values); + const insertedId = result.recordset?.[0]?.inserted_id; - const insertedId = result.recordset[0]?.inserted_id; return insertedId ? await getShiftByIdDb(insertedId) : null; }; -// Update role const updateShiftDb = async (id, data) => { const store = { ...data }; const whereData = { shift_id: id }; @@ -87,16 +75,15 @@ const updateShiftDb = async (id, data) => { store, whereData ); - await pool.query(`${queryText} AND deleted_at IS NULL`, values); + await pool.query(`${queryText} AND deleted_at IS NULL`, values); return getShiftByIdDb(id); }; const deleteShiftDb = async (id, deletedBy) => { const queryText = ` UPDATE m_shift - SET deleted_at = CURRENT_TIMESTAMP, - deleted_by = $1 + SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 WHERE shift_id = $2 AND deleted_at IS NULL `; await pool.query(queryText, [deletedBy, id]); @@ -106,7 +93,7 @@ const deleteShiftDb = async (id, deletedBy) => { module.exports = { getAllShiftDb, getShiftByIdDb, - createShiftDB, + insertShiftDb, updateShiftDb, deleteShiftDb, }; diff --git a/services/roles.service.js b/services/roles.service.js index db73834..863ae80 100644 --- a/services/roles.service.js +++ b/services/roles.service.js @@ -1,7 +1,7 @@ const { getAllRolesDb, getRolesByIdDb, - createRolesDB, + insertRolesDb, updateRolesDb, deleteRolesDb } = require('../db/roles.db'); @@ -40,7 +40,7 @@ class RolesService { try { if (!data || typeof data !== 'object') data = {}; - const result = await createRolesDB(data); + const result = await insertRolesDb(data); return result; } catch (error) { diff --git a/services/shift.service.js b/services/shift.service.js index 93439d2..8706771 100644 --- a/services/shift.service.js +++ b/services/shift.service.js @@ -1,7 +1,7 @@ const { getAllShiftDb, getShiftByIdDb, - createShiftDB, + insertShiftDb, updateShiftDb, deleteShiftDb } = require('../db/shift.db'); @@ -40,7 +40,7 @@ class ShiftService { try { if (!data || typeof data !== 'object') data = {}; - const result = await createShiftDB(data); + const result = await insertShiftDb(data); return result; } catch (error) { diff --git a/utils/time.js b/utils/time.js deleted file mode 100644 index 9a982d7..0000000 --- a/utils/time.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - formattedTime: (timestamp) => { - let date = new Date(timestamp); - let hours = date.getHours().toString().padStart(2, "0"); - let minutes = date.getMinutes().toString().padStart(2, "0"); - let seconds = date.getSeconds().toString().padStart(2, "0"); - let formattedTime = `${hours}:${minutes}:${seconds}`; - return formattedTime; - }, - }; - \ No newline at end of file From 110c73ae9d7f5daafb5527a1b54a4dfde5981f77 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Mon, 13 Oct 2025 12:37:33 +0700 Subject: [PATCH 094/126] fix create user --- services/user.service.js | 44 ++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/services/user.service.js b/services/user.service.js index cd923ac..2131a8e 100644 --- a/services/user.service.js +++ b/services/user.service.js @@ -42,31 +42,35 @@ class UserService { try { if (!data || typeof data !== 'object') data = {}; - const creatorId = data.userId; + const creatorId = data.userId || null; - const existingEmail = await getUserByUserEmailDb(data.user_email); - const existingUsername = await getUserByUsernameDb(data.user_name); + // cek duplikasi username & email + const [existingUsername, existingEmail] = await Promise.all([ + getUserByUsernameDb(data.user_name), + getUserByUserEmailDb(data.user_email) + ]); - if (existingUsername) { - throw new ErrorHandler(400, 'Username is already taken'); - } - if (existingEmail) { - throw new ErrorHandler(400, 'Email is already taken'); - } + if (existingUsername) throw new ErrorHandler(400, 'Username is already taken'); + if (existingEmail) throw new ErrorHandler(400, 'Email is already taken'); - if (data.user_password) { - data.user_password = await hashPassword(data.user_password); - } + // hash password + const hashedPassword = await hashPassword(data.user_password); - data.is_approve = 1; - data.approved_by = creatorId; - data.created_by = creatorId; - data.updated_by = creatorId; - data.is_sa = 0; - data.is_active = 1; - delete data.userId; + // siapkan data untuk insert + const userData = { + ...data, + user_password: hashedPassword, + is_approve: 1, + approved_by: creatorId, + created_by: creatorId, + updated_by: creatorId, + is_sa: 0, + is_active: 1 + }; - const result = await createUserDb(data); + delete userData.userId; + + const result = await createUserDb(userData); return result; } catch (error) { throw new ErrorHandler(error.statusCode || 500, error.message); From 2c295ffd36255fad5fb79c81c0b40a83761fe475 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Tue, 14 Oct 2025 11:02:04 +0700 Subject: [PATCH 095/126] fix: join table name --- db/tags.db.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/db/tags.db.js b/db/tags.db.js index ef93e96..4d2bdff 100644 --- a/db/tags.db.js +++ b/db/tags.db.js @@ -59,7 +59,7 @@ const getAllTagsDb = async (searchParams = {}) => { c.sub_section_name FROM m_tags a LEFT JOIN m_device b ON a.device_id = b.device_id - LEFT JOIN m_plant_subsection c ON a.sub_section_id = c.sub_section_id + LEFT JOIN m_plant_sub_section c ON a.sub_section_id = c.sub_section_id WHERE a.deleted_at IS NULL ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} ${whereOrConditions ? ` ${whereOrConditions}` : ""} @@ -87,7 +87,7 @@ const getTagsByIdDb = async (id) => { c.sub_section_name FROM m_tags a LEFT JOIN m_device b ON a.device_id = b.device_id - LEFT JOIN m_plant_subsection c ON a.sub_section_id = c.sub_section_id + LEFT JOIN m_plant_sub_section c ON a.sub_section_id = c.sub_section_id WHERE a.tag_id = $1 AND a.deleted_at IS NULL `; const result = await pool.query(queryText, [id]); From 3896f4103d7057af85908614b1c160d7fa0008ec Mon Sep 17 00:00:00 2001 From: Muhammad Afif Date: Tue, 14 Oct 2025 11:11:56 +0700 Subject: [PATCH 096/126] add: CRUD schedule --- controllers/schedule.controller.js | 71 ++++++++++++++++++++++++ db/schedule.db.js | 50 ++++++----------- routes/index.js | 2 + routes/schedule.route.js | 17 ++++++ services/schedule.service.js | 88 ++++++++++++++++++++++++++++++ validate/schedule.schema.js | 28 ++++++++++ 6 files changed, 224 insertions(+), 32 deletions(-) diff --git a/controllers/schedule.controller.js b/controllers/schedule.controller.js index e69de29..f8798b3 100644 --- a/controllers/schedule.controller.js +++ b/controllers/schedule.controller.js @@ -0,0 +1,71 @@ +const ScheduleService = require('../services/schedule.service'); +const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); +const { updateScheduleSchema, insertScheduleSchema } = require('../validate/schedule.schema'); + +class ScheduleController { + // Get all Schedule + static async getAll(req, res) { + const queryParams = req.query; + + const results = await ScheduleService.getAllSchedule(queryParams); + const response = await setResponsePaging(queryParams, results, 'Schedule found') + + res.status(response.statusCode).json(response); + } + + // Get Schedule by ID + static async getById(req, res) { + const { id } = req.params; + + const results = await ScheduleService.getScheduleById(id); + const response = await setResponse(results, 'Schedule found') + + res.status(response.statusCode).json(response); + } + + // Create Schedule + static async create(req, res) { + const { error, value } = await checkValidate(insertScheduleSchema, req) + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id + + const results = await ScheduleService.insertScheduleDb(value); + const response = await setResponse(results, 'Schedule created successfully') + + return res.status(response.statusCode).json(response); + } + + // Update Schedule + static async update(req, res) { + const { id } = req.params; + + const { error, value } = checkValidate(updateScheduleSchema, req) + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id + + const results = await ScheduleService.updateSchedule(id, value); + const response = await setResponse(results, 'Schedule updated successfully') + + res.status(response.statusCode).json(response); + } + + // Soft delete Schedule + static async delete(req, res) { + const { id } = req.params; + + const results = await ScheduleService.deleteSchedule(id, req.user.user_id); + const response = await setResponse(results, 'Schedule deleted successfully') + + res.status(response.statusCode).json(response); + } +} + +module.exports = ScheduleController; diff --git a/db/schedule.db.js b/db/schedule.db.js index 8c0df04..814d5f7 100644 --- a/db/schedule.db.js +++ b/db/schedule.db.js @@ -1,5 +1,5 @@ const pool = require("../config"); -const formattedDate = require("../utils/date"); +// const formattedDate = require("../utils/date"); // Get all schedules const getAllScheduleDb = async (searchParams = {}) => { @@ -27,15 +27,11 @@ const getAllScheduleDb = async (searchParams = {}) => { SELECT COUNT(*) OVER() AS total_data, a.*, - b.shift_id, b.shift_name, b.start_time, - b.end_time, - c.user_schedule_id, - c.user_id + b.end_time FROM schedule a LEFT JOIN m_shift b ON a.shift_id = b.shift_id - LEFT JOIN user_schedule c ON a.schedule_id = c.user_schedule_id WHERE a.deleted_at IS NULL ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} ${whereOrConditions ? ` ${whereOrConditions}` : ""} @@ -44,47 +40,36 @@ const getAllScheduleDb = async (searchParams = {}) => { `; const result = await pool.query(queryText, queryParams); - - const data = result.recordset.map((item) => ({ - ...item, - schedule_date: item.schedule_date - ? formattedDate(item.schedule_date) - : null, - })); - - const total = - result?.recordset?.length > 0 - ? parseInt(result.recordset[0].total_data, 10) - : 0; - - return { data, total }; + const total = + result?.recordset?.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; + + return { data: result.recordset, total }; }; const getScheduleByIdDb = async (id) => { const queryText = ` SELECT a.*, - b.shift_id, b.shift_name, b.start_time, - b.end_time, - c.user_schedule_id, - c.user_id + b.end_time FROM schedule a LEFT JOIN m_shift b ON a.shift_id = b.shift_id - LEFT JOIN user_schedule c ON a.schedule_id = c.user_schedule_id WHERE a.schedule_id = $1 AND a.deleted_at IS NULL `; const result = await pool.query(queryText, [id]); - const data = result.recordset.map((item) => ({ - ...item, - schedule_date: item.schedule_date - ? formattedDate(item.schedule_date) - : null, - })); + return result.recordset?.[0] || null; +}; - return data; +const insertScheduleDb = async (store) => { + const { query: queryText, values } = pool.buildDynamicInsert("schedule", store); + const result = await pool.query(queryText, values); + const insertedId = result.recordset?.[0]?.inserted_id; + + return insertedId ? await getScheduleByIdDb(insertedId) : null; }; const updateScheduleDb = async (id, data) => { @@ -115,6 +100,7 @@ const deleteScheduleDb = async (id, deletedBy) => { module.exports = { getAllScheduleDb, getScheduleByIdDb, + insertScheduleDb, updateScheduleDb, deleteScheduleDb, }; diff --git a/routes/index.js b/routes/index.js index 7e351bb..85b34ce 100644 --- a/routes/index.js +++ b/routes/index.js @@ -6,6 +6,7 @@ const roles = require('./roles.route') const tags = require("./tags.route") const subSection = require("./sub_section.route") const shift = require("./shift.route") +const schedule = require("./schedule.route") router.use("/auth", auth); router.use("/user", users); @@ -14,6 +15,7 @@ router.use("/roles", roles); router.use("/tags", tags); router.use("/plant-sub-section", subSection); router.use("/shift", shift) +router.use("/schedule", schedule) module.exports = router; diff --git a/routes/schedule.route.js b/routes/schedule.route.js index e69de29..4c5e3e2 100644 --- a/routes/schedule.route.js +++ b/routes/schedule.route.js @@ -0,0 +1,17 @@ +const express = require('express'); +const ScheduleController = require('../controllers/schedule.controller'); +const verifyToken = require("../middleware/verifyToken") +const verifyAccess = require("../middleware/verifyAccess") + +const router = express.Router(); + +router.route("/") + .get(verifyToken.verifyAccessToken, ScheduleController.getAll) + .post(verifyToken.verifyAccessToken, verifyAccess(), ScheduleController.create); + +router.route("/:id") + .get(verifyToken.verifyAccessToken, ScheduleController.getById) + .put(verifyToken.verifyAccessToken, verifyAccess(), ScheduleController.update) + .delete(verifyToken.verifyAccessToken, verifyAccess(), ScheduleController.delete); + +module.exports = router; \ No newline at end of file diff --git a/services/schedule.service.js b/services/schedule.service.js index e69de29..ea33e1d 100644 --- a/services/schedule.service.js +++ b/services/schedule.service.js @@ -0,0 +1,88 @@ +const { + getAllScheduleDb, + getScheduleByIdDb, + insertScheduleDb, + updateScheduleDb, + deleteScheduleDb +} = require('../db/schedule.db'); +const { ErrorHandler } = require('../helpers/error'); + +class ScheduleService { + // Get all Schedule + static async getAllSchedule(param) { + try { + const results = await getAllScheduleDb(param); + + results.data.map(element => { + }); + + return results + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Get Schedule by ID + static async getScheduleById(id) { + try { + const result = await getScheduleByIdDb(id); + + if (result.length < 1) throw new ErrorHandler(404, 'Schedule not found'); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Create Schedule + static async insertScheduleDb(data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const result = await insertScheduleDb(data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Update Schedule + static async updateSchedule(id, data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const dataExist = await getScheduleByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Schedule not found'); + } + + const result = await updateScheduleDb(id, data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Soft delete Schedule + static async deleteSchedule(id, userId) { + try { + const dataExist = await getScheduleByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Schedule not found'); + } + + const result = await deleteScheduleDb(id, userId); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } +} + +module.exports = ScheduleService; diff --git a/validate/schedule.schema.js b/validate/schedule.schema.js index e69de29..5252030 100644 --- a/validate/schedule.schema.js +++ b/validate/schedule.schema.js @@ -0,0 +1,28 @@ +// ======================== +// Schedule Validation + +const Joi = require("joi"); + +const datePattern = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/; + +const insertScheduleSchema = Joi.object({ + schedule_date: Joi.string().pattern(datePattern).required().messages({ + "string.pattern.base": "schedule_date harus dalam format YYYY-MM-DD", + "any.required": "schedule_date wajib diisi", + }), + is_active: Joi.boolean().required(), + shift_id: Joi.number(), +}); + +const updateScheduleSchema = Joi.object({ + schedule_date: Joi.string().pattern(datePattern).messages({ + "string.pattern.base": "schedule_date harus dalam format YYYY-MM-DD", + }), + is_active: Joi.boolean(), + shift_id: Joi.number(), +}).min(1); + +module.exports = { + insertScheduleSchema, + updateScheduleSchema, +}; From 7f06416abf3a27a0ae1eaf592fb2e7c6759d5cae Mon Sep 17 00:00:00 2001 From: Muhammad Afif Date: Tue, 14 Oct 2025 17:37:20 +0700 Subject: [PATCH 097/126] repair: tags db --- db/tags.db.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/db/tags.db.js b/db/tags.db.js index 4d2bdff..41b61c9 100644 --- a/db/tags.db.js +++ b/db/tags.db.js @@ -33,11 +33,10 @@ const getAllTagsDb = async (searchParams = {}) => { { column: "a.unit", param: searchParams.unit, type: "string" }, { column: "b.device_name", param: searchParams.device, type: "string" }, { - column: "b.device_description", + column: "b.device_code", param: searchParams.device, type: "string", }, - { column: "b.ip_address", param: searchParams.device, type: "string" }, { column: "c.sub_section_name", param: searchParams.subsection, @@ -54,8 +53,7 @@ const getAllTagsDb = async (searchParams = {}) => { COUNT(*) OVER() AS total_data, a.*, b.device_name, - b.ip_address, - b.device_description, + b.device_code, c.sub_section_name FROM m_tags a LEFT JOIN m_device b ON a.device_id = b.device_id @@ -82,8 +80,7 @@ const getTagsByIdDb = async (id) => { SELECT a.*, b.device_name, - b.ip_address, - b.device_description, + b.device_code, c.sub_section_name FROM m_tags a LEFT JOIN m_device b ON a.device_id = b.device_id From 243416fbd6d70ef2d0cc0fa507cddedee48adc50 Mon Sep 17 00:00:00 2001 From: Muhammad Afif Date: Wed, 15 Oct 2025 10:24:29 +0700 Subject: [PATCH 098/126] repair: tags schema --- validate/tags.schema.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validate/tags.schema.js b/validate/tags.schema.js index c73a465..3c083c6 100644 --- a/validate/tags.schema.js +++ b/validate/tags.schema.js @@ -5,7 +5,7 @@ const Joi = require("joi"); // ======================== const insertTagsSchema = Joi.object({ - device_id: Joi.number().required(), + device_id: Joi.number(), tag_name: Joi.string().max(200).required(), tag_number: Joi.number().required(), is_active: Joi.boolean().required(), From 7aaf35f7e97d7c773309110c9acee599b6bb95d8 Mon Sep 17 00:00:00 2001 From: Fachba Date: Wed, 15 Oct 2025 12:07:18 +0700 Subject: [PATCH 099/126] env example --- .env.example | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index 3be6766..8368363 100644 --- a/.env.example +++ b/.env.example @@ -1,18 +1,18 @@ -# # SQL DB Connection Colo -# SQL_HOST=117.102.231.130 +# SQL DB Connection Colo +SQL_HOST=117.102.231.130 +SQL_DATABASE=cod_piu +SQL_USERNAME=sa +SQL_PASSWORD=@R3M4niA. +SQL_PORT=1433 + +# SQL_HOST=203.153.114.226 +# SQL_PORT=1112 # SQL_DATABASE=piu # SQL_USERNAME=sa -# SQL_PASSWORD=@R3M4niA. -# SQL_PORT=1433 - -SQL_HOST=203.153.114.226 -SQL_PORT=1112 -SQL_DATABASE=piu -SQL_USERNAME=sa -SQL_PASSWORD=piu123 +# SQL_PASSWORD=piu123 # Application Port - express server listens on this port (default 9000). -PORT=9528 +PORT=9530 ENDPOINT_WA=http://203.153.114.226:9529/send # ENDPOINT_WA=http://localhost:9529/send ENDPOINT_FE=http://203.153.114.226:9527 From 32d25cef6ad8940d021375aaec3c9dc5b98b4641 Mon Sep 17 00:00:00 2001 From: Muhammad Afif Date: Wed, 15 Oct 2025 15:50:56 +0700 Subject: [PATCH 100/126] repair device db: device code - device name --- db/device.db.js | 103 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 70 insertions(+), 33 deletions(-) diff --git a/db/device.db.js b/db/device.db.js index 3d770ec..4c429bb 100644 --- a/db/device.db.js +++ b/db/device.db.js @@ -2,46 +2,66 @@ const pool = require("../config"); // Get all devices const getAllDevicesDb = async (searchParams = {}) => { - - queryParams = [] + queryParams = []; if (searchParams.limit) { - const page = Number(searchParams.page ?? 1) - 1 + const page = Number(searchParams.page ?? 1) - 1; queryParams = [Number(searchParams.limit ?? 10), page]; } - const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike([ - "a.device_name", - "a.device_code", - "a.device_location", - "a.ip_address", - "b.brand_name" - ], searchParams.criteria, queryParams); + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + ["a.device_code", "a.device_location", "a.ip_address", "b.brand_name"], + searchParams.criteria, + queryParams + ); - queryParams = whereParamOr ? whereParamOr : queryParams + queryParams = whereParamOr ? whereParamOr : queryParams; - const { whereConditions, whereParamAnd } = pool.buildFilterQuery([ - { column: "a.device_name", param: searchParams.name, type: "string" }, - { column: "a.device_code", param: searchParams.code, type: "string" }, - { column: "a.device_location", param: searchParams.location, type: "string" }, - { column: "b.brand_name", param: searchParams.brand, type: "string" }, - ], queryParams); + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [ + { column: "a.device_code", param: searchParams.code, type: "string" }, + { + column: "a.device_location", + param: searchParams.location, + type: "string", + }, + { column: "b.brand_name", param: searchParams.brand, type: "string" }, + ], + queryParams + ); - queryParams = whereParamAnd ? whereParamAnd : queryParams + queryParams = whereParamAnd ? whereParamAnd : queryParams; const queryText = ` - SELECT COUNT(*) OVER() AS total_data, a.*, b.brand_name + SELECT COUNT(*) OVER() AS total_data, + a.device_status, + a.device_location, + a.device_description, + a.ip_address, + a.created_by, + a.updated_by, + a.deleted_by, + a.created_at, + a.updated_at, + a.deleted_at, + CONCAT(a.device_code, ' - ', a.device_name) AS device_code, + b.brand_name FROM m_device a LEFT JOIN m_brands b ON a.brand_id = b.brand_id WHERE a.deleted_at IS NULL - ${whereConditions.length > 0 ? ` AND ${whereConditions.join(' AND ')}` : ''} - ${whereOrConditions ? whereOrConditions : ''} + ${ + whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : "" + } + ${whereOrConditions ? whereOrConditions : ""} ORDER BY a.device_id ASC - ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ''}; + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""}; `; const result = await pool.query(queryText, queryParams); // Menghitung total data. - const total = result?.recordset.length > 0 ? parseInt(result.recordset[0].total_data, 10) : 0; + const total = + result?.recordset.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; // Mengembalikan data dan total. return { data: result.recordset, total }; @@ -49,7 +69,19 @@ const getAllDevicesDb = async (searchParams = {}) => { const getDeviceByIdDb = async (id) => { const queryText = ` - SELECT a.*, b.brand_name + SELECT + a.device_status, + a.device_location, + a.device_description, + a.ip_address, + a.created_by, + a.updated_by, + a.deleted_by, + a.created_at, + a.updated_at, + a.deleted_at, + CONCAT(a.device_code, ' - ', a.device_name) AS device_code, + b.brand_name FROM m_device a LEFT JOIN m_brands b ON a.brand_id = b.brand_id WHERE a.device_id = $1 AND a.deleted_at IS NULL @@ -59,32 +91,37 @@ const getDeviceByIdDb = async (id) => { }; const createDeviceDb = async (data) => { - - const newCode = await pool.generateKode("DVC", "m_device", "device_code") + const newCode = await pool.generateKode("DVC", "m_device", "device_code"); const store = { ...data, device_code: newCode, - } + }; - const { query: queryText, values } = pool.buildDynamicInsert("m_device", store); + const { query: queryText, values } = pool.buildDynamicInsert( + "m_device", + store + ); const result = await pool.query(queryText, values); const insertedId = result.recordset[0]?.inserted_id; return insertedId ? await getDeviceByIdDb(insertedId) : null; }; const updateDeviceDb = async (id, data) => { - const store = { - ...data - } + ...data, + }; // Kondisi WHERE const whereData = { - device_id: id + device_id: id, }; - const { query: queryText, values } = pool.buildDynamicUpdate("m_device", store, whereData); + const { query: queryText, values } = pool.buildDynamicUpdate( + "m_device", + store, + whereData + ); await pool.query(`${queryText} AND deleted_at IS NULL`, values); return getDeviceByIdDb(id); }; From 7e05fc589b808eba28d0fee91eccadee918ae388 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 15 Oct 2025 11:14:32 +0700 Subject: [PATCH 101/126] fix: hash password for change user password --- services/user.service.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/user.service.js b/services/user.service.js index 2131a8e..dc187af 100644 --- a/services/user.service.js +++ b/services/user.service.js @@ -144,7 +144,8 @@ class UserService { const userExist = await getUserByIdDb(id); if (!userExist) throw new ErrorHandler(404, 'User not found'); - const result = await changeUserPasswordDb(id, newPassword); + const hashedPassword = await hashPassword(newPassword); + const result = await changeUserPasswordDb(id, hashedPassword); return result; } catch (error) { throw new ErrorHandler(error.statusCode, error.message); From 7ad8c6b3fe77b597afeea659b65f629451e72d67 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 15 Oct 2025 12:08:39 +0700 Subject: [PATCH 102/126] add: rejectuserdb + default approve 2 --- db/user.db.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/db/user.db.js b/db/user.db.js index 28f152a..c8772b6 100644 --- a/db/user.db.js +++ b/db/user.db.js @@ -132,11 +132,12 @@ const updateUserDb = async (userId, data) => { return getUserByIdDb(userId); }; +// Approve user const approveUserDb = async (userId, approverId) => { const queryText = ` UPDATE m_users SET - is_approve = 1, + is_approve = 2, approved_by = $1, approved_at = CURRENT_TIMESTAMP, updated_by = $1, @@ -144,9 +145,24 @@ const approveUserDb = async (userId, approverId) => { WHERE user_id = $2 AND deleted_at IS NULL `; await pool.query(queryText, [approverId, userId]); - return true; // simple, cuma tanda berhasil + return true; }; +// Reject user +const rejectUserDb = async (userId, approverId) => { + const queryText = ` + UPDATE m_users + SET + is_approve = 0, + approved_by = $1, + approved_at = CURRENT_TIMESTAMP, + updated_by = $1, + updated_at = CURRENT_TIMESTAMP + WHERE user_id = $2 AND deleted_at IS NULL + `; + await pool.query(queryText, [approverId, userId]); + return true; +} // Change user password const changeUserPasswordDb = async (userId, newPassword) => { @@ -182,6 +198,7 @@ module.exports = { createUserDb, updateUserDb, approveUserDb, + rejectUserDb, changeUserPasswordDb, deleteUserDb, }; From c76953bf89b99bf0044973eb42f590d8ab3a7659 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 15 Oct 2025 12:09:01 +0700 Subject: [PATCH 103/126] add: reject user --- controllers/users.controller.js | 11 +++++++++++ routes/users.route.js | 3 +++ services/user.service.js | 34 ++++++++++++++++++++++++++++++++- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/controllers/users.controller.js b/controllers/users.controller.js index 05a3fa8..aef8858 100644 --- a/controllers/users.controller.js +++ b/controllers/users.controller.js @@ -68,6 +68,17 @@ class UserController { return res.status(response.statusCode).json(response); } + // Reject user + static async reject(req, res) { + const { id } = req.params; + const approverId = req.user.user_id; + + const updatedUser = await UserService.rejectUser(id, approverId); + const response = await setResponse(updatedUser, 'User rejected successfully'); + + return res.status(response.statusCode).json(response); + } + // Soft delete user static async delete(req, res) { const { id } = req.params; diff --git a/routes/users.route.js b/routes/users.route.js index 388e567..2b7abbb 100644 --- a/routes/users.route.js +++ b/routes/users.route.js @@ -20,4 +20,7 @@ router.route('/change-password/:id') router.route('/:id/approve') .put(verifyToken.verifyAccessToken, verifyAccess(), UserController.approve); +router.route('/:id/reject') + .put(verifyToken.verifyAccessToken, verifyAccess(), UserController.reject); + module.exports = router; diff --git a/services/user.service.js b/services/user.service.js index dc187af..b5f25cd 100644 --- a/services/user.service.js +++ b/services/user.service.js @@ -6,6 +6,7 @@ const { createUserDb, updateUserDb, approveUserDb, + rejectUserDb, deleteUserDb, changeUserPasswordDb } = require('../db/user.db'); @@ -114,10 +115,14 @@ class UserService { throw new ErrorHandler(404, 'User not found'); } - if (existingUser.is_approve) { + if (existingUser.is_approve === 2) { throw new ErrorHandler(400, 'User is already approved'); } + if (existingUser.is_approve === 0) { + throw new ErrorHandler(400, 'User is already rejected'); + } + const updatedUser = await approveUserDb(userId, approverId); return updatedUser; } catch (error) { @@ -125,6 +130,33 @@ class UserService { } } + // Reject user + static async rejectUser(userId, approverId) { + try { + if (!userId) { + throw new ErrorHandler(400, 'User ID is required'); + } + + const existingUser = await getUserByIdDb(userId); + if (!existingUser) { + throw new ErrorHandler(404, 'User not found'); + } + + if (existingUser.is_approve === 2) { + throw new ErrorHandler(400, 'User is already approved'); + } + + if (existingUser.is_approve === 0) { + throw new ErrorHandler(400, 'User is already rejected'); + } + + const updatedUser = await rejectUserDb(userId, approverId); + return updatedUser; + } catch (error) { + throw new ErrorHandler(error.statusCode || 500, error.message); + } + } + // Soft delete user static async deleteUser(id, userId) { try { From 1be51f634bb232186fc6b5dbb9967d85b53a6a02 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 15 Oct 2025 12:10:41 +0700 Subject: [PATCH 104/126] fix: default create user approve : 2 --- services/user.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/user.service.js b/services/user.service.js index b5f25cd..d2688b3 100644 --- a/services/user.service.js +++ b/services/user.service.js @@ -61,7 +61,7 @@ class UserService { const userData = { ...data, user_password: hashedPassword, - is_approve: 1, + is_approve: 2, approved_by: creatorId, created_by: creatorId, updated_by: creatorId, From 2907cb634753ea0e313912d5fe15289dc0047a29 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 16 Oct 2025 11:24:30 +0700 Subject: [PATCH 105/126] update validation schema --- services/auth.service.js | 3 +-- validate/user.schema.js | 15 +++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/services/auth.service.js b/services/auth.service.js index c700bef..3027da7 100644 --- a/services/auth.service.js +++ b/services/auth.service.js @@ -67,8 +67,7 @@ class AuthService { if (!passwordMatch) throw new ErrorHandler(401, 'Invalid credentials'); if (!user.is_active) throw new ErrorHandler(403, 'User is inactive'); - if (!user.is_approve) - throw new ErrorHandler(403, 'Your account has not been approved by admin yet.'); + if (!user.is_approve) throw new ErrorHandler(403, 'Your account has not been approved by admin yet.'); const payload = { user_id: user.user_id, diff --git a/validate/user.schema.js b/validate/user.schema.js index 4b7d020..19b4be9 100644 --- a/validate/user.schema.js +++ b/validate/user.schema.js @@ -29,16 +29,15 @@ const userSchema = Joi.object({ }); const updateUserSchema = Joi.object({ - user_fullname: Joi.string().min(3).max(100), - user_name: Joi.string().alphanum().min(3).max(50), - user_email: Joi.string().email(), + user_fullname: Joi.string().min(3).max(100).optional(), + user_name: Joi.string().alphanum().min(3).max(50).optional(), + user_email: Joi.string().email().optional(), user_phone: Joi.string() .pattern(/^(?:\+62|0)8\d{7,10}$/) - .messages({ - 'string.pattern.base': - 'Phone number must be a valid Indonesian number in format +628XXXXXXXXX' - }), - role_id: Joi.number().integer().min(1) + .message('Phone number must be a valid Indonesian number in format +628XXXXXXXXX') + .optional(), + role_id: Joi.number().integer().min(1).optional(), + is_active: Joi.boolean().optional() }).min(1); const newPasswordSchema = Joi.object({ From 7364c3b9c167462feab7750ac79246da8bbf9f28 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 16 Oct 2025 12:29:47 +0700 Subject: [PATCH 106/126] add: column query device _id --- db/device.db.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/db/device.db.js b/db/device.db.js index 4c429bb..c5373a7 100644 --- a/db/device.db.js +++ b/db/device.db.js @@ -32,7 +32,8 @@ const getAllDevicesDb = async (searchParams = {}) => { queryParams = whereParamAnd ? whereParamAnd : queryParams; const queryText = ` - SELECT COUNT(*) OVER() AS total_data, + SELECT COUNT(*) OVER() AS total_data, + a.device_id, a.device_status, a.device_location, a.device_description, @@ -69,7 +70,8 @@ const getAllDevicesDb = async (searchParams = {}) => { const getDeviceByIdDb = async (id) => { const queryText = ` - SELECT + SELECT + a.device_id, a.device_status, a.device_location, a.device_description, From b1feffe39e2da7ac7277ae32bcd9d0c851db8ae2 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 16 Oct 2025 13:12:57 +0700 Subject: [PATCH 107/126] fix: device.db.js --- db/device.db.js | 68 +++++++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/db/device.db.js b/db/device.db.js index c5373a7..366963d 100644 --- a/db/device.db.js +++ b/db/device.db.js @@ -2,29 +2,36 @@ const pool = require("../config"); // Get all devices const getAllDevicesDb = async (searchParams = {}) => { - queryParams = []; + let queryParams = []; + + // Pagination if (searchParams.limit) { const page = Number(searchParams.page ?? 1) - 1; queryParams = [Number(searchParams.limit ?? 10), page]; } + // Search const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( - ["a.device_code", "a.device_location", "a.ip_address", "b.brand_name"], + [ + "a.device_name", + "a.device_code", + "a.device_location", + "a.ip_address", + "b.brand_name", + ], searchParams.criteria, queryParams ); queryParams = whereParamOr ? whereParamOr : queryParams; + // Filter const { whereConditions, whereParamAnd } = pool.buildFilterQuery( [ { column: "a.device_code", param: searchParams.code, type: "string" }, - { - column: "a.device_location", - param: searchParams.location, - type: "string", - }, + { column: "a.device_location", param: searchParams.location, type: "string" }, { column: "b.brand_name", param: searchParams.brand, type: "string" }, + { column: "a.device_status", param: searchParams.status, type: "string" }, ], queryParams ); @@ -32,46 +39,48 @@ const getAllDevicesDb = async (searchParams = {}) => { queryParams = whereParamAnd ? whereParamAnd : queryParams; const queryText = ` - SELECT COUNT(*) OVER() AS total_data, - a.device_id, - a.device_status, - a.device_location, - a.device_description, - a.ip_address, - a.created_by, - a.updated_by, - a.deleted_by, - a.created_at, - a.updated_at, - a.deleted_at, - CONCAT(a.device_code, ' - ', a.device_name) AS device_code, - b.brand_name + SELECT + COUNT(*) OVER() AS total_data, + a.device_id, + a.device_code, + a.device_name, + a.device_status, + a.device_location, + a.device_description, + a.ip_address, + a.created_by, + a.updated_by, + a.deleted_by, + a.created_at, + a.updated_at, + a.deleted_at, + b.brand_id, + b.brand_name FROM m_device a LEFT JOIN m_brands b ON a.brand_id = b.brand_id WHERE a.deleted_at IS NULL - ${ - whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : "" - } - ${whereOrConditions ? whereOrConditions : ""} + ${whereConditions.length > 0 ? `AND ${whereConditions.join(' AND ')}` : ''} + ${whereOrConditions ? whereOrConditions : ''} ORDER BY a.device_id ASC - ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""}; + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ''}; `; + const result = await pool.query(queryText, queryParams); - // Menghitung total data. const total = result?.recordset.length > 0 ? parseInt(result.recordset[0].total_data, 10) : 0; - // Mengembalikan data dan total. return { data: result.recordset, total }; }; + const getDeviceByIdDb = async (id) => { const queryText = ` SELECT - a.device_id, + a.device_id, + a.device_name, a.device_status, a.device_location, a.device_description, @@ -82,7 +91,6 @@ const getDeviceByIdDb = async (id) => { a.created_at, a.updated_at, a.deleted_at, - CONCAT(a.device_code, ' - ', a.device_name) AS device_code, b.brand_name FROM m_device a LEFT JOIN m_brands b ON a.brand_id = b.brand_id From d56d23cd71c209007a2892fab9c85f84bea9a55a Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 16 Oct 2025 07:52:41 +0700 Subject: [PATCH 108/126] fix: query device_status to is_active --- db/device.db.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/db/device.db.js b/db/device.db.js index 366963d..851ba60 100644 --- a/db/device.db.js +++ b/db/device.db.js @@ -31,7 +31,7 @@ const getAllDevicesDb = async (searchParams = {}) => { { column: "a.device_code", param: searchParams.code, type: "string" }, { column: "a.device_location", param: searchParams.location, type: "string" }, { column: "b.brand_name", param: searchParams.brand, type: "string" }, - { column: "a.device_status", param: searchParams.status, type: "string" }, + { column: "a.is_active", param: searchParams.status, type: "string" }, ], queryParams ); @@ -44,7 +44,7 @@ const getAllDevicesDb = async (searchParams = {}) => { a.device_id, a.device_code, a.device_name, - a.device_status, + a.is_active, a.device_location, a.device_description, a.ip_address, @@ -81,7 +81,7 @@ const getDeviceByIdDb = async (id) => { SELECT a.device_id, a.device_name, - a.device_status, + a.is_active, a.device_location, a.device_description, a.ip_address, From bf9cdf5eeb4b6b5f039cc4d542cd8273fea6e37e Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 16 Oct 2025 07:53:50 +0700 Subject: [PATCH 109/126] add: props register --- services/auth.service.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/auth.service.js b/services/auth.service.js index 3027da7..44b5948 100644 --- a/services/auth.service.js +++ b/services/auth.service.js @@ -29,6 +29,9 @@ class AuthService { user_email: data.user_email, user_phone: data.user_phone, user_password: hashedPassword, + is_sa: 0, + is_active: 1, + is_approve: 1, }); const newUser = { From a036326792a734036784a044c5e7d51ade8d6b40 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 16 Oct 2025 08:10:26 +0700 Subject: [PATCH 110/126] fix: device status to is_active --- validate/device.schema.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/validate/device.schema.js b/validate/device.schema.js index a8e74e5..4d1f65e 100644 --- a/validate/device.schema.js +++ b/validate/device.schema.js @@ -6,7 +6,7 @@ const Joi = require("joi"); // ======================== const insertDeviceSchema = Joi.object({ device_name: Joi.string().max(100).required(), - device_status: Joi.boolean().required(), + is_active: Joi.boolean().required(), device_location: Joi.string().max(100).required(), device_description: Joi.string().required(), ip_address: Joi.string() @@ -19,7 +19,7 @@ const insertDeviceSchema = Joi.object({ const updateDeviceSchema = Joi.object({ device_name: Joi.string().max(100), - device_status: Joi.boolean(), + is_active: Joi.boolean(), device_location: Joi.string().max(100), device_description: Joi.string(), ip_address: Joi.string() From b2399d486ebc3c329580018d258d933d1c7c2516 Mon Sep 17 00:00:00 2001 From: Muhammad Afif Date: Thu, 16 Oct 2025 15:25:01 +0700 Subject: [PATCH 111/126] revisi db tags: add device_code_name --- db/device.db.js | 38 ++++++++------------------------------ 1 file changed, 8 insertions(+), 30 deletions(-) diff --git a/db/device.db.js b/db/device.db.js index 851ba60..c28d9a0 100644 --- a/db/device.db.js +++ b/db/device.db.js @@ -40,22 +40,10 @@ const getAllDevicesDb = async (searchParams = {}) => { const queryText = ` SELECT - COUNT(*) OVER() AS total_data, - a.device_id, - a.device_code, - a.device_name, - a.is_active, - a.device_location, - a.device_description, - a.ip_address, - a.created_by, - a.updated_by, - a.deleted_by, - a.created_at, - a.updated_at, - a.deleted_at, - b.brand_id, - b.brand_name + COUNT(*) OVER() AS total_data, + a.*, + b.brand_name, + COALESCE(a.device_code, '') + ' - ' + COALESCE(a.device_name, '') AS device_code_name FROM m_device a LEFT JOIN m_brands b ON a.brand_id = b.brand_id WHERE a.deleted_at IS NULL @@ -78,20 +66,10 @@ const getAllDevicesDb = async (searchParams = {}) => { const getDeviceByIdDb = async (id) => { const queryText = ` - SELECT - a.device_id, - a.device_name, - a.is_active, - a.device_location, - a.device_description, - a.ip_address, - a.created_by, - a.updated_by, - a.deleted_by, - a.created_at, - a.updated_at, - a.deleted_at, - b.brand_name + SELECT + a.*, + b.brand_name, + COALESCE(a.device_code, '') + ' - ' + COALESCE(a.device_name, '') AS device_code_name FROM m_device a LEFT JOIN m_brands b ON a.brand_id = b.brand_id WHERE a.device_id = $1 AND a.deleted_at IS NULL From ba8c7a53465744b2422910ee3070da0bb877dec8 Mon Sep 17 00:00:00 2001 From: Muhammad Afif Date: Thu, 16 Oct 2025 16:26:37 +0700 Subject: [PATCH 112/126] repair shift: schmea & db --- db/shift.db.js | 1 + validate/shift.schema.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/db/shift.db.js b/db/shift.db.js index 74b0091..4dc4192 100644 --- a/db/shift.db.js +++ b/db/shift.db.js @@ -21,6 +21,7 @@ const getAllShiftDb = async (searchParams = {}) => { { column: "a.shift_name", param: searchParams.name, type: "string" }, { column: "a.start_time", param: searchParams.start_time, type: "time" }, { column: "a.end_time", param: searchParams.end_time, type: "time" }, + { column: "a.is_active", param: searchParams.status, type: "string" }, ], queryParams ); diff --git a/validate/shift.schema.js b/validate/shift.schema.js index 49c6981..54439cb 100644 --- a/validate/shift.schema.js +++ b/validate/shift.schema.js @@ -8,6 +8,7 @@ const timePattern = /^([01]\d|2[0-3]):([0-5]\d)(:[0-5]\d)?$/; const insertShiftSchema = Joi.object({ shift_name: Joi.string().max(100).required(), + is_active:Joi.boolean().required(), start_time: Joi.string() .pattern(timePattern) .required() @@ -24,6 +25,7 @@ const insertShiftSchema = Joi.object({ const updateShiftSchema = Joi.object({ shift_name: Joi.string().max(100), + is_active:Joi.boolean(), start_time: Joi.string() .pattern(timePattern) .messages({ From c4be5804482ee23cffb3a155eb0a1626d5a69eb5 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 16 Oct 2025 10:03:38 +0700 Subject: [PATCH 113/126] add: status db --- db/status.db.js | 117 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 db/status.db.js diff --git a/db/status.db.js b/db/status.db.js new file mode 100644 index 0000000..37cbd2a --- /dev/null +++ b/db/status.db.js @@ -0,0 +1,117 @@ +const pool = require("../config"); + +// Get all status +const getAllStatusDb = async (searchParams = {}) => { + let queryParams = []; + + // Pagination + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1; + queryParams = [Number(searchParams.limit ?? 10), page]; + } + + // Search + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + ["a.status_name", "a.status_description"], + searchParams.criteria, + queryParams + ); + + queryParams = whereParamOr ? whereParamOr : queryParams; + + // Filter + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [ + { column: "a.status_number", param: searchParams.number, type: "number" }, + { column: "a.is_active", param: searchParams.is_active, type: "boolean" }, + ], + queryParams + ); + + queryParams = whereParamAnd ? whereParamAnd : queryParams; + + const queryText = ` + SELECT + COUNT(*) OVER() AS total_data, + a.* + FROM m_status a + WHERE a.deleted_at IS NULL + ${whereConditions.length > 0 ? `AND ${whereConditions.join(' AND ')}` : ''} + ${whereOrConditions ? whereOrConditions : ''} + ORDER BY a.status_id ASC + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ''}; + `; + + const result = await pool.query(queryText, queryParams); + + const total = + result?.recordset.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; + + return { data: result.recordset, total }; +}; + +const getStatusByIdDb = async (id) => { + const queryText = ` + SELECT * + FROM m_status a + WHERE a.status_id = $1 AND a.deleted_at IS NULL + `; + const result = await pool.query(queryText, [id]); + return result.recordset; +}; + +// Check if status_number already exists +const checkStatusNumberExistsDb = async (status_number) => { + const queryText = ` + SELECT 1 + FROM m_status + WHERE status_number = $1 AND deleted_at IS NULL + `; + const result = await pool.query(queryText, [status_number]); + return result.recordset.length > 0; +}; + + +const createStatusDb = async (data) => { + const { query: queryText, values } = pool.buildDynamicInsert( + "m_status", + data + ); + const result = await pool.query(queryText, values); + const insertedId = result.recordset[0]?.inserted_id; + return insertedId ? await getStatusByIdDb(insertedId) : null; +}; + +const updateStatusDb = async (id, data) => { + const store = { ...data }; + const whereData = { status_id: id }; + + const { query: queryText, values } = pool.buildDynamicUpdate( + "m_status", + store, + whereData + ); + await pool.query(`${queryText} AND deleted_at IS NULL`, values); + return getStatusByIdDb(id); +}; + +const deleteStatusDb = async (id, deletedBy) => { + const queryText = ` + UPDATE m_status + SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 + WHERE status_id = $2 AND deleted_at IS NULL + `; + await pool.query(queryText, [deletedBy, id]); + return true; +}; + +module.exports = { + getAllStatusDb, + getStatusByIdDb, + createStatusDb, + updateStatusDb, + deleteStatusDb, + checkStatusNumberExistsDb, +}; From 5c44b8c3f02d2306a766a5f77f2cbadcf84a5b9b Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 16 Oct 2025 10:05:26 +0700 Subject: [PATCH 114/126] add: validation --- validate/status.schema.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 validate/status.schema.js diff --git a/validate/status.schema.js b/validate/status.schema.js new file mode 100644 index 0000000..fa8ac56 --- /dev/null +++ b/validate/status.schema.js @@ -0,0 +1,24 @@ +const Joi = require("joi"); + +// ======================== +// Status Validation +// ======================== +const insertStatusSchema = Joi.object({ + status_number: Joi.number().integer().required(), + status_name: Joi.string().max(200).required(), + status_color: Joi.string().max(200).required(), + status_description: Joi.string().allow('', null) +}); + +const updateStatusSchema = Joi.object({ + status_number: Joi.number().integer(), + status_name: Joi.string().max(200), + status_color: Joi.string().max(200), + status_description: Joi.string().allow('', null) +}).min(1); + +// ✅ Export dengan CommonJS +module.exports = { + insertStatusSchema, + updateStatusSchema +}; From 4d080946a6e8f0211c1bd7203051c93db3597fa0 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 16 Oct 2025 10:05:38 +0700 Subject: [PATCH 115/126] add crud m_status --- controllers/status.controller.js | 73 +++++++++++++++++++++++++ services/status.service.js | 92 ++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 controllers/status.controller.js create mode 100644 services/status.service.js diff --git a/controllers/status.controller.js b/controllers/status.controller.js new file mode 100644 index 0000000..859be33 --- /dev/null +++ b/controllers/status.controller.js @@ -0,0 +1,73 @@ +const StatusService = require('../services/status.service'); +const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); +const { insertStatusSchema, updateStatusSchema } = require('../validate/status.schema'); + +class StatusController { + // Get all status + static async getAll(req, res) { + const queryParams = req.query; + + const results = await StatusService.getAllStatus(queryParams); + const response = await setResponsePaging(queryParams, results, 'Status found'); + + res.status(response.statusCode).json(response); + } + + // Get status by ID + static async getById(req, res) { + const { id } = req.params; + + const results = await StatusService.getStatusById(id); + const response = await setResponse(results, 'Status found'); + + res.status(response.statusCode).json(response); + } + + // Create status + static async create(req, res) { + const { error, value } = await checkValidate(insertStatusSchema, req); + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id; + + const results = await StatusService.createStatus(value); + const response = await setResponse(results, 'Status created successfully'); + + return res.status(response.statusCode).json(response); + } + + // Update status + static async update(req, res) { + const { id } = req.params; + + console.log("REQ BODY:", req.body); + + const { error, value } = await checkValidate(updateStatusSchema, req); + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id; + + const results = await StatusService.updateStatus(id, value); + const response = await setResponse(results, 'Status updated successfully'); + + res.status(response.statusCode).json(response); + } + + // Soft delete status + static async delete(req, res) { + const { id } = req.params; + + const results = await StatusService.deleteStatus(id, req.user.user_id); + const response = await setResponse(results, 'Status deleted successfully'); + + res.status(response.statusCode).json(response); + } +} + +module.exports = StatusController; diff --git a/services/status.service.js b/services/status.service.js new file mode 100644 index 0000000..cfc3113 --- /dev/null +++ b/services/status.service.js @@ -0,0 +1,92 @@ +const { + getAllStatusDb, + getStatusByIdDb, + createStatusDb, + updateStatusDb, + deleteStatusDb, + checkStatusNumberExistsDb +} = require('../db/status.db'); +const { ErrorHandler } = require('../helpers/error'); + +class StatusService { + // Get all status + static async getAllStatus(param) { + try { + const results = await getAllStatusDb(param); + + results.data.map(element => { + }); + + return results; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Get status by ID + static async getStatusById(id) { + try { + const result = await getStatusByIdDb(id); + + if (result.length < 1) throw new ErrorHandler(404, 'Status not found'); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + static async createStatus(data) { + try { + if (!data || typeof data !== 'object') data = {}; + + if (data.status_number) { + const exists = await checkStatusNumberExistsDb(data.status_number); + if (exists) throw new ErrorHandler(400, 'Status number already exists'); + } + + const result = await createStatusDb(data); + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode || 500, error.message); + } + } + + // Update status + static async updateStatus(id, data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const dataExist = await getStatusByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Status not found'); + } + + const result = await updateStatusDb(id, data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Soft delete status + static async deleteStatus(id, userId) { + try { + const dataExist = await getStatusByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Status not found'); + } + + const result = await deleteStatusDb(id, userId); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } +} + +module.exports = StatusService; From b146fe3ffa8f2301d1e28e0066d46da2f472949b Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Thu, 16 Oct 2025 10:05:55 +0700 Subject: [PATCH 116/126] add: status route --- routes/index.js | 6 ++++-- routes/status.route.js | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 routes/status.route.js diff --git a/routes/index.js b/routes/index.js index 85b34ce..509d1b5 100644 --- a/routes/index.js +++ b/routes/index.js @@ -7,6 +7,7 @@ const tags = require("./tags.route") const subSection = require("./sub_section.route") const shift = require("./shift.route") const schedule = require("./schedule.route") +const status = require("./status.route") router.use("/auth", auth); router.use("/user", users); @@ -14,8 +15,9 @@ router.use("/device", device); router.use("/roles", roles); router.use("/tags", tags); router.use("/plant-sub-section", subSection); -router.use("/shift", shift) -router.use("/schedule", schedule) +router.use("/shift", shift); +router.use("/schedule", schedule); +router.use("/status", status) module.exports = router; diff --git a/routes/status.route.js b/routes/status.route.js new file mode 100644 index 0000000..eb4995f --- /dev/null +++ b/routes/status.route.js @@ -0,0 +1,17 @@ +const express = require('express'); +const StatusController = require('../controllers/status.controller'); +const verifyToken = require("../middleware/verifyToken"); +const verifyAccess = require("../middleware/verifyAccess"); + +const router = express.Router(); + +router.route("/") + .get(verifyToken.verifyAccessToken, StatusController.getAll) + .post(verifyToken.verifyAccessToken, verifyAccess(), StatusController.create); + +router.route("/:id") + .get(verifyToken.verifyAccessToken, StatusController.getById) + .put(verifyToken.verifyAccessToken, verifyAccess(), StatusController.update) + .delete(verifyToken.verifyAccessToken, verifyAccess(), StatusController.delete); + +module.exports = router; From 2bb87124307c218d91a63dd46028349888b69408 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 17 Oct 2025 10:44:26 +0700 Subject: [PATCH 117/126] add: validate is_active --- validate/roles.schema.js | 8 +++++--- validate/shift.schema.js | 7 ++++--- validate/status.schema.js | 12 +++++++----- validate/sub_section.schema.js | 6 ++++-- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/validate/roles.schema.js b/validate/roles.schema.js index f8bb585..d23436a 100644 --- a/validate/roles.schema.js +++ b/validate/roles.schema.js @@ -8,12 +8,14 @@ const insertRolesSchema = Joi.object({ role_name: Joi.string().max(100).required(), role_level: Joi.number().required(), role_description: Joi.string().max(100).required(), + is_active: Joi.boolean().required(), }); const updateRolesSchema = Joi.object({ - role_name: Joi.string().max(100), - role_level: Joi.number(), - role_description: Joi.string().max(100), + role_name: Joi.string().max(100).optional(), + role_level: Joi.number().optional(), + role_description: Joi.string().max(100).optional(), + is_active: Joi.boolean().optional() }).min(1); diff --git a/validate/shift.schema.js b/validate/shift.schema.js index 54439cb..8a9bc0e 100644 --- a/validate/shift.schema.js +++ b/validate/shift.schema.js @@ -24,18 +24,19 @@ const insertShiftSchema = Joi.object({ }); const updateShiftSchema = Joi.object({ - shift_name: Joi.string().max(100), - is_active:Joi.boolean(), + shift_name: Joi.string().max(100).optional(), + is_active:Joi.boolean().optional(), start_time: Joi.string() .pattern(timePattern) .messages({ "string.pattern.base": "start_time harus dalam format HH:mm atau HH:mm:ss", - }), + }).optional(), end_time: Joi.string() .pattern(timePattern) .messages({ "string.pattern.base": "end_time harus dalam format HH:mm atau HH:mm:ss", }), + is_active: Joi.boolean().optional() }).min(1); module.exports = { diff --git a/validate/status.schema.js b/validate/status.schema.js index fa8ac56..9b79b65 100644 --- a/validate/status.schema.js +++ b/validate/status.schema.js @@ -7,14 +7,16 @@ const insertStatusSchema = Joi.object({ status_number: Joi.number().integer().required(), status_name: Joi.string().max(200).required(), status_color: Joi.string().max(200).required(), - status_description: Joi.string().allow('', null) + status_description: Joi.string().allow('', null), + is_active: Joi.boolean().required(), }); const updateStatusSchema = Joi.object({ - status_number: Joi.number().integer(), - status_name: Joi.string().max(200), - status_color: Joi.string().max(200), - status_description: Joi.string().allow('', null) + status_number: Joi.number().integer().optional(), + status_name: Joi.string().max(200).optional(), + status_color: Joi.string().max(200).optional(), + status_description: Joi.string().allow('', null).optional(), + is_active: Joi.boolean().optional() }).min(1); // ✅ Export dengan CommonJS diff --git a/validate/sub_section.schema.js b/validate/sub_section.schema.js index 616ac90..fe1e570 100644 --- a/validate/sub_section.schema.js +++ b/validate/sub_section.schema.js @@ -11,7 +11,8 @@ const insertSubSectionSchema = Joi.object({ "string.base": "Sub section name must be a string", "string.max": "Sub section name cannot exceed 200 characters", "any.required": "Sub section name is required" - }), + }).required(), + is_active: Joi.boolean().required(), }); const updateSubSectionSchema = Joi.object({ @@ -20,7 +21,8 @@ const updateSubSectionSchema = Joi.object({ .messages({ "string.base": "Sub section name must be a string", "string.max": "Sub section name cannot exceed 200 characters", - }), + }).optional(), + is_active: Joi.boolean().optional(), }).min(1).messages({ "object.min": "At least one field must be provided to update", }); From 1aec3825e7670a5f51296a2d47a33a2280097c00 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 17 Oct 2025 11:30:09 +0700 Subject: [PATCH 118/126] add: validate is_active --- validate/tags.schema.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/validate/tags.schema.js b/validate/tags.schema.js index 3c083c6..94350b4 100644 --- a/validate/tags.schema.js +++ b/validate/tags.schema.js @@ -11,6 +11,7 @@ const insertTagsSchema = Joi.object({ is_active: Joi.boolean().required(), data_type: Joi.string().max(50).required(), unit: Joi.string().max(50).required(), + is_alarm: Joi.boolean().required() }); const updateTagsSchema = Joi.object({ @@ -20,6 +21,7 @@ const updateTagsSchema = Joi.object({ is_active: Joi.boolean(), data_type: Joi.string().max(50), unit: Joi.string().max(50), + is_alarm: Joi.boolean().optional() }).min(1); // ✅ Export dengan CommonJS From 7e769a1facab5a82086c24efae39e6b4c751a4c6 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 17 Oct 2025 11:30:24 +0700 Subject: [PATCH 119/126] add: unit db --- db/unit.db.js | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 db/unit.db.js diff --git a/db/unit.db.js b/db/unit.db.js new file mode 100644 index 0000000..ad96fb4 --- /dev/null +++ b/db/unit.db.js @@ -0,0 +1,119 @@ +const pool = require("../config"); + +// Get all units +const getAllUnitsDb = async (searchParams = {}) => { + let queryParams = []; + + // Pagination + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1; + queryParams = [Number(searchParams.limit ?? 10), page]; + } + + // Search + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + ["a.unit_code", "a.unit_name"], + searchParams.criteria, + queryParams + ); + + queryParams = whereParamOr ? whereParamOr : queryParams; + + // Filter + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [ + { column: "a.unit_code", param: searchParams.code, type: "string" }, + { column: "a.unit_name", param: searchParams.name, type: "string" }, + { column: "a.tag_id", param: searchParams.tag, type: "number" }, + { column: "a.is_active", param: searchParams.status, type: "string" }, + ], + queryParams + ); + + queryParams = whereParamAnd ? whereParamAnd : queryParams; + + const queryText = ` + SELECT + COUNT(*) OVER() AS total_data, + a.*, + COALESCE(a.unit_code, '') + ' - ' + COALESCE(a.unit_name, '') AS unit_code_name + FROM m_unit a + WHERE a.deleted_at IS NULL + ${whereConditions.length > 0 ? `AND ${whereConditions.join(" AND ")}` : ""} + ${whereOrConditions ? whereOrConditions : ""} + ORDER BY a.unit_id ASC + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""}; + `; + + const result = await pool.query(queryText, queryParams); + + const total = + result?.recordset.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; + + return { data: result.recordset, total }; +}; + +// Get unit by ID +const getUnitByIdDb = async (id) => { + const queryText = ` + SELECT + a.*, + COALESCE(a.unit_code, '') + ' - ' + COALESCE(a.unit_name, '') AS unit_code_name + FROM m_unit a + WHERE a.unit_id = $1 AND a.deleted_at IS NULL + `; + const result = await pool.query(queryText, [id]); + return result.recordset; +}; + +// Create unit +const createUnitDb = async (data) => { + const newCode = await pool.generateKode("UNT", "m_unit", "unit_code"); + + const store = { + ...data, + unit_code: newCode, + }; + + const { query: queryText, values } = pool.buildDynamicInsert("m_unit", store); + const result = await pool.query(queryText, values); + const insertedId = result.recordset[0]?.inserted_id; + return insertedId ? await getUnitByIdDb(insertedId) : null; +}; + +// Update unit +const updateUnitDb = async (id, data) => { + const store = { ...data }; + const whereData = { unit_id: id }; + + const { query: queryText, values } = pool.buildDynamicUpdate( + "m_unit", + store, + whereData + ); + + await pool.query(`${queryText} AND deleted_at IS NULL`, values); + return getUnitByIdDb(id); +}; + +// Soft delete unit +const deleteUnitDb = async (id, deletedBy) => { + const queryText = ` + UPDATE m_unit + SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 + WHERE unit_id = $2 AND deleted_at IS NULL + `; + await pool.query(queryText, [deletedBy, id]); + return true; +}; + +// Export +module.exports = { + getAllUnitsDb, + getUnitByIdDb, + createUnitDb, + updateUnitDb, + deleteUnitDb, +}; \ No newline at end of file From f2109e5fdf18aadf7fbc23689934ccd40e0cdb7e Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 17 Oct 2025 11:30:46 +0700 Subject: [PATCH 120/126] add: validate unit --- validate/unit.schema.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 validate/unit.schema.js diff --git a/validate/unit.schema.js b/validate/unit.schema.js new file mode 100644 index 0000000..058e91c --- /dev/null +++ b/validate/unit.schema.js @@ -0,0 +1,21 @@ +const Joi = require("joi"); + +// ======================== +// Unit Validation +// ======================== +const insertUnitSchema = Joi.object({ + unit_name: Joi.string().max(100).required(), + tag_id: Joi.number().integer().optional(), + is_active: Joi.boolean().required(), +}); + +const updateUnitSchema = Joi.object({ + unit_name: Joi.string().max(100).optional(), + tag_id: Joi.number().integer().optional(), + is_active: Joi.boolean().optional() +}).min(1); + +module.exports = { + insertUnitSchema, + updateUnitSchema +}; From 90f529bfde922eb5fb33153dfcbf35e8abe4c5f8 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 17 Oct 2025 11:30:55 +0700 Subject: [PATCH 121/126] add: crud unit --- controllers/unit.controller.js | 71 +++++++++++++++++++++++++++ services/unit.service.js | 88 ++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 controllers/unit.controller.js create mode 100644 services/unit.service.js diff --git a/controllers/unit.controller.js b/controllers/unit.controller.js new file mode 100644 index 0000000..4e323f6 --- /dev/null +++ b/controllers/unit.controller.js @@ -0,0 +1,71 @@ +const UnitService = require('../services/unit.service'); +const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); +const { insertUnitSchema, updateUnitSchema } = require('../validate/unit.schema'); + +class UnitController { + // Get all units + static async getAll(req, res) { + const queryParams = req.query; + + const results = await UnitService.getAllUnits(queryParams); + const response = await setResponsePaging(queryParams, results, 'Unit found'); + + res.status(response.statusCode).json(response); + } + + // Get unit by ID + static async getById(req, res) { + const { id } = req.params; + + const results = await UnitService.getUnitById(id); + const response = await setResponse(results, 'Unit found'); + + res.status(response.statusCode).json(response); + } + + // Create unit + static async create(req, res) { + const { error, value } = await checkValidate(insertUnitSchema, req); + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.created_by = req.user.user_id; + + const results = await UnitService.createUnit(value); + const response = await setResponse(results, 'Unit created successfully'); + + return res.status(response.statusCode).json(response); + } + + // Update unit + static async update(req, res) { + const { id } = req.params; + + const { error, value } = checkValidate(updateUnitSchema, req); + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.updated_by = req.user.user_id; + + const results = await UnitService.updateUnit(id, value); + const response = await setResponse(results, 'Unit updated successfully'); + + res.status(response.statusCode).json(response); + } + + // Soft delete unit + static async delete(req, res) { + const { id } = req.params; + + const results = await UnitService.deleteUnit(id, req.user.user_id); + const response = await setResponse(results, 'Unit deleted successfully'); + + res.status(response.statusCode).json(response); + } +} + +module.exports = UnitController; diff --git a/services/unit.service.js b/services/unit.service.js new file mode 100644 index 0000000..c6d6624 --- /dev/null +++ b/services/unit.service.js @@ -0,0 +1,88 @@ +const { + getAllUnitsDb, + getUnitByIdDb, + createUnitDb, + updateUnitDb, + deleteUnitDb +} = require('../db/unit.db'); +const { ErrorHandler } = require('../helpers/error'); + +class UnitService { + // Get all units + static async getAllUnits(param) { + try { + const results = await getAllUnitsDb(param); + + results.data.map(element => { + }); + + return results; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Get unit by ID + static async getUnitById(id) { + try { + const result = await getUnitByIdDb(id); + + if (result.length < 1) throw new ErrorHandler(404, 'Unit not found'); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Create unit + static async createUnit(data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const result = await createUnitDb(data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Update unit + static async updateUnit(id, data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const dataExist = await getUnitByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Unit not found'); + } + + const result = await updateUnitDb(id, data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Soft delete unit + static async deleteUnit(id, userId) { + try { + const dataExist = await getUnitByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Unit not found'); + } + + const result = await deleteUnitDb(id, userId); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } +} + +module.exports = UnitService; \ No newline at end of file From 4083e8544e2847b7afb896e6be51fa73618b401f Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 17 Oct 2025 11:31:10 +0700 Subject: [PATCH 122/126] add: unit routes --- routes/index.js | 16 +++++++++------- routes/unit.route.js | 17 +++++++++++++++++ 2 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 routes/unit.route.js diff --git a/routes/index.js b/routes/index.js index 509d1b5..cc2c2c5 100644 --- a/routes/index.js +++ b/routes/index.js @@ -2,12 +2,13 @@ const router = require("express").Router(); const auth = require("./auth.route"); const users = require("./users.route"); const device = require('./device.route'); -const roles = require('./roles.route') -const tags = require("./tags.route") -const subSection = require("./sub_section.route") -const shift = require("./shift.route") -const schedule = require("./schedule.route") -const status = require("./status.route") +const roles = require('./roles.route'); +const tags = require("./tags.route"); +const subSection = require("./sub_section.route"); +const shift = require("./shift.route"); +const schedule = require("./schedule.route"); +const status = require("./status.route"); +const unit = require("./unit.route") router.use("/auth", auth); router.use("/user", users); @@ -17,7 +18,8 @@ router.use("/tags", tags); router.use("/plant-sub-section", subSection); router.use("/shift", shift); router.use("/schedule", schedule); -router.use("/status", status) +router.use("/status", status); +router.use("/unit", unit); module.exports = router; diff --git a/routes/unit.route.js b/routes/unit.route.js new file mode 100644 index 0000000..24a3c77 --- /dev/null +++ b/routes/unit.route.js @@ -0,0 +1,17 @@ +const express = require('express'); +const UnitController = require('../controllers/unit.controller'); +const verifyToken = require('../middleware/verifyToken'); +const verifyAccess = require('../middleware/verifyAccess'); + +const router = express.Router(); + +router.route('/') + .get(verifyToken.verifyAccessToken, UnitController.getAll) + .post(verifyToken.verifyAccessToken, verifyAccess(), UnitController.create); + +router.route('/:id') + .get(verifyToken.verifyAccessToken, UnitController.getById) + .put(verifyToken.verifyAccessToken, verifyAccess(), UnitController.update) + .delete(verifyToken.verifyAccessToken, verifyAccess(), UnitController.delete); + +module.exports = router; From c71da2d3bbc0da0d8455741eb0bffe59cef5b4ac Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 17 Oct 2025 12:41:19 +0700 Subject: [PATCH 123/126] fix: tag validate --- validate/tags.schema.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/validate/tags.schema.js b/validate/tags.schema.js index 94350b4..29563f1 100644 --- a/validate/tags.schema.js +++ b/validate/tags.schema.js @@ -5,12 +5,13 @@ const Joi = require("joi"); // ======================== const insertTagsSchema = Joi.object({ - device_id: Joi.number(), + device_id: Joi.number().optional(), tag_name: Joi.string().max(200).required(), tag_number: Joi.number().required(), is_active: Joi.boolean().required(), data_type: Joi.string().max(50).required(), unit: Joi.string().max(50).required(), + sub_section_id: Joi.number().optional(), is_alarm: Joi.boolean().required() }); @@ -21,7 +22,8 @@ const updateTagsSchema = Joi.object({ is_active: Joi.boolean(), data_type: Joi.string().max(50), unit: Joi.string().max(50), - is_alarm: Joi.boolean().optional() + is_alarm: Joi.boolean().optional(), + sub_section_id: Joi.number().optional(), }).min(1); // ✅ Export dengan CommonJS From d0394f27eb839fd4c9e6c87686f53255750f0356 Mon Sep 17 00:00:00 2001 From: Muhammad Afif Date: Fri, 17 Oct 2025 13:02:50 +0700 Subject: [PATCH 124/126] add: looping days in schedule_date --- db/schedule.db.js | 46 ++++++++++++++++++++++++++----------- validate/schedule.schema.js | 1 + 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/db/schedule.db.js b/db/schedule.db.js index 814d5f7..436c02d 100644 --- a/db/schedule.db.js +++ b/db/schedule.db.js @@ -1,5 +1,5 @@ const pool = require("../config"); -// const formattedDate = require("../utils/date"); +const { formattedDate } = require("../utils/date"); // Get all schedules const getAllScheduleDb = async (searchParams = {}) => { @@ -40,12 +40,12 @@ const getAllScheduleDb = async (searchParams = {}) => { `; const result = await pool.query(queryText, queryParams); - const total = - result?.recordset?.length > 0 - ? parseInt(result.recordset[0].total_data, 10) - : 0; - - return { data: result.recordset, total }; + const total = + result?.recordset?.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; + + return { data: result.recordset, total }; }; const getScheduleByIdDb = async (id) => { @@ -60,16 +60,36 @@ const getScheduleByIdDb = async (id) => { WHERE a.schedule_id = $1 AND a.deleted_at IS NULL `; const result = await pool.query(queryText, [id]); - return result.recordset?.[0] || null; }; const insertScheduleDb = async (store) => { - const { query: queryText, values } = pool.buildDynamicInsert("schedule", store); - const result = await pool.query(queryText, values); - const insertedId = result.recordset?.[0]?.inserted_id; + const nextDays = Number(store.next_day ?? 0); + const insertedRecords = []; - return insertedId ? await getScheduleByIdDb(insertedId) : null; + for (let i = 0; i <= nextDays; i++) { + const nextDate = new Date(store.schedule_date); + nextDate.setDate(nextDate.getDate() + i); + + const formatted = formattedDate(nextDate); + + const newStore = { + ...store, + schedule_date: formatted, + }; + delete newStore.next_day; + + const { query: queryText, values } = pool.buildDynamicInsert("schedule", newStore); + const result = await pool.query(queryText, values); + const insertedId = result.recordset?.[0]?.inserted_id; + + if (insertedId) { + const record = await getScheduleByIdDb(insertedId); + insertedRecords.push(record); + } + } + + return insertedRecords; }; const updateScheduleDb = async (id, data) => { @@ -77,7 +97,7 @@ const updateScheduleDb = async (id, data) => { const whereData = { schedule_id: id }; const { query: queryText, values } = pool.buildDynamicUpdate( - "schedule", + "schedule", store, whereData ); diff --git a/validate/schedule.schema.js b/validate/schedule.schema.js index 5252030..ee0e37d 100644 --- a/validate/schedule.schema.js +++ b/validate/schedule.schema.js @@ -12,6 +12,7 @@ const insertScheduleSchema = Joi.object({ }), is_active: Joi.boolean().required(), shift_id: Joi.number(), + next_day: Joi.number().required(), }); const updateScheduleSchema = Joi.object({ From c772b7a9df79927649f75187e783189f6b7c8453 Mon Sep 17 00:00:00 2001 From: Muhammad Afif Date: Fri, 17 Oct 2025 13:08:29 +0700 Subject: [PATCH 125/126] repair: shift schema --- db/schedule.db.js | 2 +- validate/shift.schema.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/db/schedule.db.js b/db/schedule.db.js index 436c02d..d3a598c 100644 --- a/db/schedule.db.js +++ b/db/schedule.db.js @@ -64,7 +64,7 @@ const getScheduleByIdDb = async (id) => { }; const insertScheduleDb = async (store) => { - const nextDays = Number(store.next_day ?? 0); + const nextDays = Number(store.next_day ?? 0); // default 0 kalau tidak diisi const insertedRecords = []; for (let i = 0; i <= nextDays; i++) { diff --git a/validate/shift.schema.js b/validate/shift.schema.js index 8a9bc0e..4751778 100644 --- a/validate/shift.schema.js +++ b/validate/shift.schema.js @@ -36,7 +36,6 @@ const updateShiftSchema = Joi.object({ .messages({ "string.pattern.base": "end_time harus dalam format HH:mm atau HH:mm:ss", }), - is_active: Joi.boolean().optional() }).min(1); module.exports = { From 8761f1e07ef2fa47f14e3289766071f94217d6b9 Mon Sep 17 00:00:00 2001 From: Fachba Date: Mon, 20 Oct 2025 10:21:24 +0700 Subject: [PATCH 126/126] check connection db --- app.js | 20 ++++++++++++++++++++ config/index.js | 13 +++++++++++++ 2 files changed, 33 insertions(+) diff --git a/app.js b/app.js index 8674820..c83b7ff 100644 --- a/app.js +++ b/app.js @@ -8,6 +8,7 @@ const helmet = require("helmet"); const compression = require("compression"); const unknownEndpoint = require("./middleware/unKnownEndpoint"); const { handleError } = require("./helpers/error"); +const { checkConnection } = require("./config"); const app = express(); @@ -24,6 +25,25 @@ app.use("/api", routes); app.get("/", (req, res) => res.send("

HAHALO

") ); + +app.get("/check-db", async (req, res) => { + try { + const isConnected = await checkConnection(); + res.json({ + success: isConnected, + message: isConnected + ? "Koneksi database OK" + : "Koneksi database gagal", + }); + } catch (error) { + res.status(500).json({ + success: false, + message: "Terjadi kesalahan saat cek koneksi database", + error: error.message, + }); + } +}); + app.use(unknownEndpoint); app.use(handleError); diff --git a/config/index.js b/config/index.js index 2ee4f79..fe0b713 100644 --- a/config/index.js +++ b/config/index.js @@ -28,6 +28,18 @@ const poolPromise = new sql.ConnectionPool(config) process.exit(1); }); +async function checkConnection() { + try { + const pool = await poolPromise; + await pool.request().query('SELECT 1 AS isConnected'); + console.log('🔍 SQL Server terkoneksi dengan baik'); + return true; + } catch (error) { + console.error('⚠️ Gagal cek koneksi SQL Server:', error); + return false; + } +} + /** * Wrapper query (auto konversi $1 → @p1) */ @@ -223,6 +235,7 @@ async function generateKode(prefix, tableName, columnName) { } module.exports = { + checkConnection, query, buildFilterQuery, buildStringOrIlike,