From f4ab31b4364991ae6a2382942434c0acdcdf4931 Mon Sep 17 00:00:00 2001 From: bragaz_rexita Date: Wed, 17 Sep 2025 13:28:36 +0700 Subject: [PATCH 1/6] Add skeleton --- config/index.js | 204 ++++++++++++++++++++++++++++++++ controllers/auth.controller.js | 26 ++++ controllers/users.controller.js | 172 +++++++++++++++++++++++++++ db/user.db.js | 142 ++++++++++++++++++++++ 4 files changed, 544 insertions(+) create mode 100644 config/index.js create mode 100644 controllers/auth.controller.js create mode 100644 controllers/users.controller.js create mode 100644 db/user.db.js diff --git a/config/index.js b/config/index.js new file mode 100644 index 0000000..8ed5673 --- /dev/null +++ b/config/index.js @@ -0,0 +1,204 @@ +require("dotenv").config(); +const sql = require("mssql"); + +const isProduction = process.env.NODE_ENV === "production"; + +// Config SQL Server +const config = { + user: process.env.SQL_USERNAME, + password: process.env.SQL_PASSWORD, + database: process.env.SQL_DATABASE, + server: process.env.SQL_HOST, + port: parseInt(process.env.SQL_PORT, 10), + options: { + encrypt: false, // true kalau Azure + trustServerCertificate: true, + }, +}; + +// Buat pool global +const poolPromise = new sql.ConnectionPool(config) + .connect() + .then(pool => { + console.log("✅ Koneksi SQL Server berhasil"); + return pool; + }) + .catch(err => { + console.error("❌ Gagal koneksi SQL Server:", err); + process.exit(1); + }); + +/** + * Wrapper query (auto konversi $1 → @p1) + */ +async function query(text, params = []) { + const pool = await poolPromise; + const request = pool.request(); + + params.forEach((param, i) => { + request.input(`p${i + 1}`, param); + }); + + // Ubah $1, $2 jadi @p1, @p2 + const sqlText = text.replace(/\$(\d+)/g, (_, num) => `@p${num}`); + + console.log(sqlText, params); + return request.query(sqlText); +} + +/** + * Build filter query + */ +function buildFilterQuery(filterQuery = [], fixedParams = []) { + let whereConditions = []; + let queryParams = [...fixedParams]; + + filterQuery.forEach((f) => { + if (f.param === undefined || f.param === null || f.param === "") return; + + switch (f.type) { + case "string": + queryParams.push(`%${f.param}%`); + whereConditions.push(`${f.column} LIKE $${queryParams.length} COLLATE SQL_Latin1_General_CP1_CI_AS`); + break; + + case "number": + queryParams.push(f.param); + whereConditions.push(`${f.column} = $${queryParams.length}`); + break; + + case "boolean": + queryParams.push(f.param ? 1 : 0); + whereConditions.push(`${f.column} = $${queryParams.length}`); + break; + } + }); + + return { whereConditions, queryParams }; +} + +/** + * Build OR ILIKE (SQL Server pakai LIKE + COLLATE) + */ +function buildStringOrIlike(columnParam, criteria, fixedParams = []) { + if (!criteria) return { whereClause: "", whereParam: fixedParams }; + + let orStringConditions = []; + let queryParams = [...fixedParams]; + + columnParam.forEach((column) => { + if (!column) return; + queryParams.push(`%${criteria}%`); + orStringConditions.push(`${column} LIKE $${queryParams.length} COLLATE SQL_Latin1_General_CP1_CI_AS`); + }); + + const whereClause = orStringConditions.length + ? `AND (${orStringConditions.join(" OR ")})` + : ""; + + return { whereOrConditions: whereClause, whereParam: queryParams }; +} + +/** + * Build dynamic UPDATE + */ +function buildDynamicUpdate(table, data, where) { + const setParts = []; + const values = []; + let index = 1; + + for (const [key, value] of Object.entries(data)) { + if (value !== undefined && value !== null) { + setParts.push(`${key} = $${index++}`); + values.push(value); + } + } + + if (setParts.length === 0) { + throw new Error("Tidak ada kolom untuk diupdate"); + } + + // updated_at otomatis pakai GETDATE() + setParts.push(`updated_at = GETDATE()`); + + const whereParts = []; + for (const [key, value] of Object.entries(where)) { + whereParts.push(`${key} = $${index++}`); + values.push(value); + } + + const query = ` + UPDATE ${table} + SET ${setParts.join(", ")} + WHERE ${whereParts.join(" AND ")} + `; + + return { query, values }; +} + +/** + * Build dynamic INSERT + */ +function buildDynamicInsert(table, data) { + const columns = []; + const placeholders = []; + const values = []; + let index = 1; + + for (const [key, value] of Object.entries(data)) { + if (value !== undefined && value !== null) { + columns.push(key); + placeholders.push(`$${index++}`); + values.push(value); + } + } + + if (columns.length === 0) { + throw new Error("Tidak ada kolom untuk diinsert"); + } + + // created_at & updated_at otomatis + columns.push("created_at", "updated_at"); + placeholders.push("GETDATE()", "GETDATE()"); + + const query = ` + INSERT INTO ${table} (${columns.join(", ")}) + VALUES (${placeholders.join(", ")}); + SELECT SCOPE_IDENTITY() as inserted_id; + `; + + return { query, values }; +} + +/** + * Generate kode otomatis + */ +async function generateKode(prefix, tableName, columnName) { + const pool = await poolPromise; + const result = await pool.request() + .input("prefix", sql.VarChar, prefix + "%") + .query(` + SELECT TOP 1 ${columnName} as kode + FROM ${tableName} + WHERE ${columnName} LIKE @prefix + ORDER BY ${columnName} DESC + `); + + let nextNumber = 1; + if (result.recordset.length > 0) { + const lastKode = result.recordset[0].kode; + const lastNumber = parseInt(lastKode.replace(prefix, ""), 10); + nextNumber = lastNumber + 1; + } + + return prefix + String(nextNumber).padStart(3, "0"); +} + +module.exports = { + query, + buildFilterQuery, + buildStringOrIlike, + buildDynamicInsert, + buildDynamicUpdate, + generateKode, +}; diff --git a/controllers/auth.controller.js b/controllers/auth.controller.js new file mode 100644 index 0000000..464b3cd --- /dev/null +++ b/controllers/auth.controller.js @@ -0,0 +1,26 @@ +const authService = require("../services/auth.service"); + +const loginUser = async (req, res) => { + const { username, password, role, tenant } = req.body; + const { token, refreshToken, user } = await authService.login( + username, + password, + tenant + ); + + 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, + }); +}; + +module.exports = { + loginUser, +}; diff --git a/controllers/users.controller.js b/controllers/users.controller.js new file mode 100644 index 0000000..ae9548e --- /dev/null +++ b/controllers/users.controller.js @@ -0,0 +1,172 @@ +const userService = require("../services/user.service"); +const { ErrorHandler } = require("../helpers/error"); +const { hashPassword } = require("../helpers/hashPassword"); +const { setResponse, setPaging, setResponsePaging } = require("../helpers/utils"); +const Joi = require("joi"); + +// 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' + ] + } + } + + const results = await userService.getAllUsers(filterQuery) + const response = await setResponsePaging(results.data, results.total, parseInt(limit), parseInt(page)) + + res.status(response.statusCode).json(response) +}; + +const getAllStatusUsers = async (req, res) => { + + const results = await userService.getAllStatusUsers(); + const response = await setResponse(results) + + res.status(response.statusCode).json(response); +}; + +const createUser = async (req, res) => { + + // Lakukan validasi + const { error } = validateTerm.validate(req.body, { stripUnknown: true }); + if (error) { + const response = await setResponse([], error.details[0].message, 400) + return res.status(response.statusCode).json(response); + } + + const results = await userService.createUser({ + userFullname: req.body.user_fullname, + userName: req.body.user_name, + userEmail: req.body.user_email, + userPassword: req.body.user_password, + roleId: req.body.role_id, + isActive: req.body.is_active, // default 1 jika tidak dikirim + userID: req.body.userID, + tenantID: req.body.tenantID + }); + + const response = await setResponse(results); + + res.status(response.statusCode).json(response); +}; + +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); + } + + 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 + }); + + const response = await setResponse(results); + + res.status(response.statusCode).json(response); +}; + +const deleteUser = async (req, res) => { + const { id } = req.params; + const userID = req.userID + + const results = await userService.deleteUser(id, userID); + const response = await setResponse(results) + + res.status(response.statusCode).json(response); +}; + +const getAllRoles = async (req, res) => { + const results = await userService.getAllRoles(req.body.tenantID); + const response = await setResponse(results) + + res.status(response.statusCode).json(response); +}; + +module.exports = { + getAllUsers, + createUser, + getUserById, + updateUser, + deleteUser, + getUserProfile, + getAllRoles, + getAllStatusUsers +}; diff --git a/db/user.db.js b/db/user.db.js new file mode 100644 index 0000000..46471bc --- /dev/null +++ b/db/user.db.js @@ -0,0 +1,142 @@ +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 + `; + + 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 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); + return result.recordset[0]; +}; + +const getUserByIdDb = async (id) => { + const query = ` + SELECT mut.* + FROM m_users mut + WHERE mut.user_id = $1 + `; + const result = await pool.query(query, [id]); + return result.recordset[0]; +}; + +const getUserByUsernameDb = async (username) => { + const query = ` + SELECT mut.* + FROM m_users mut + WHERE LOWER(mut.username) = LOWER($1) + `; + const result = await pool.query(query, [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]; +}; + +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]; +}; + +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 + `; + const result = await pool.query(query, [userID, id]); + return result.recordset[0]; +}; + +const changeUserPasswordDb = async (hashedPassword, userEmail, tenantId) => { + const query = ` + UPDATE m_users + SET user_password = $1 + WHERE user_email = $2 AND tenant_id = $3 + `; + return pool.query(query, [hashedPassword, userEmail, tenantId]); +}; + +const getAllRoleDb = async (tenantId) => { + const query = ` + SELECT * + FROM system.role_tenant + WHERE deleted_at IS NULL AND tenant_id = $1 + `; + const result = await pool.query(query, [tenantId]); + return result.recordset; +}; + +module.exports = { + getAllUsersDb, + getUserByIdDb, + getUserByUserEmailDb, + updateUserDb, + createUserDb, + deleteUserDb, + getUserByUsernameDb, + changeUserPasswordDb, + getAllRoleDb, +}; From 945e0083d20ba22fffd103ac4c7d5f726d04a9ba Mon Sep 17 00:00:00 2001 From: bragaz_rexita Date: Wed, 17 Sep 2025 13:28:50 +0700 Subject: [PATCH 2/6] Add skeleton --- helpers/error.js | 24 +++++++++++ helpers/hashPassword.js | 12 ++++++ helpers/test_helper.js | 13 ++++++ helpers/utils.js | 89 +++++++++++++++++++++++++++++++++++++++++ helpers/validateUser.js | 9 +++++ 5 files changed, 147 insertions(+) create mode 100644 helpers/error.js create mode 100644 helpers/hashPassword.js create mode 100644 helpers/test_helper.js create mode 100644 helpers/utils.js create mode 100644 helpers/validateUser.js diff --git a/helpers/error.js b/helpers/error.js new file mode 100644 index 0000000..a3dbcc7 --- /dev/null +++ b/helpers/error.js @@ -0,0 +1,24 @@ +const { logger } = require("../utils/logger"); +class ErrorHandler extends Error { + constructor(statusCode, message) { + super(); + this.status = "error"; + this.statusCode = statusCode; + this.message = message; + } +} + +const handleError = (err, req, res, next) => { + const { statusCode, message } = err; + logger.error(err); + res.status(statusCode || 500).json({ + status: "error", + statusCode: statusCode || 500, + message: statusCode === 500 ? "An error occurred" : message, + }); + next(); +}; +module.exports = { + ErrorHandler, + handleError, +}; diff --git a/helpers/hashPassword.js b/helpers/hashPassword.js new file mode 100644 index 0000000..c30a35a --- /dev/null +++ b/helpers/hashPassword.js @@ -0,0 +1,12 @@ +const bcrypt = require("bcrypt"); + +const hashPassword = async (password) => { + const salt = await bcrypt.genSalt(); + const hashedPassword = await bcrypt.hash(password, salt); + return hashedPassword; +}; + +const comparePassword = async (password, passwordHash) => + await bcrypt.compare(password, passwordHash); + +module.exports = { hashPassword, comparePassword }; diff --git a/helpers/test_helper.js b/helpers/test_helper.js new file mode 100644 index 0000000..95b8e28 --- /dev/null +++ b/helpers/test_helper.js @@ -0,0 +1,13 @@ +const pool = require("../config"); + +const usersInDb = async () => { + const users = await pool.query("SELECT * FROM USERS"); + return users.rows; +}; + +const productsInDb = async () => { + const products = await pool.query("SELECT * FROM products"); + return products.rows; +}; + +module.exports = { usersInDb, productsInDb }; diff --git a/helpers/utils.js b/helpers/utils.js new file mode 100644 index 0000000..41c60ed --- /dev/null +++ b/helpers/utils.js @@ -0,0 +1,89 @@ +const setResponse = async (data = [], message = "success", statusCode = 200) => { + const response = { + data, + total: data.length, + message, + statusCode + } + + return response +}; + +const setResponsePaging = async (data = [], total, limit, page, message = "success", statusCode = 200) => { + + const totalPages = Math.ceil(total / limit); + + const response = { + message, + statusCode, + data, + total: 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 + } + + return response +}; + +function convertId(items, id, fieldId = "id", fieldName = "name") { + var match = "" + items.forEach(element => { + if (element[fieldId] == id) { + match = element[fieldName] + } + }); + + return match +} + +function formatToYYYYMMDD(date) { + return new Intl.DateTimeFormat('id-ID', { + timeZone: 'Asia/Jakarta', + year: 'numeric', + month: '2-digit', + day: '2-digit', + }).format(date).split('/').reverse().join('-'); // Ubah format dari dd/mm/yyyy ke yyyy-mm-dd +} + +function ensureArray(param) { + return Array.isArray(param) ? param : [param]; +} + +function orderByClauseQuery(orderParams) { + orderParams = ensureArray(orderParams) + + // Transform order parameters to SQL ORDER BY syntax + const validDirections = ['ASC', 'DESC']; + const orderConditions = orderParams.map(param => { + const [field, direction] = param.split(':'); + if (!validDirections.includes(direction.toUpperCase())) { + throw new Error(`Invalid direction: ${direction}`); + } + return `${field} ${direction}`; + }); + + // Gabungkan dengan koma untuk digunakan dalam query + const orderByClause = orderConditions.join(', '); + + return orderByClause +} + +module.exports = { setResponse, setResponsePaging, setPaging, convertId, formatToYYYYMMDD, orderByClauseQuery }; diff --git a/helpers/validateUser.js b/helpers/validateUser.js new file mode 100644 index 0000000..c904bb4 --- /dev/null +++ b/helpers/validateUser.js @@ -0,0 +1,9 @@ +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 2c546a2ec7d19749d14a5a7ce54513a40faba45c Mon Sep 17 00:00:00 2001 From: bragaz_rexita Date: Wed, 17 Sep 2025 13:28:58 +0700 Subject: [PATCH 3/6] Add skeleton --- middleware/unKnownEndpoint.js | 8 ++++++ middleware/verifyAdmin.js | 14 +++++++++++ middleware/verifyToken.js | 47 +++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 middleware/unKnownEndpoint.js create mode 100644 middleware/verifyAdmin.js create mode 100644 middleware/verifyToken.js diff --git a/middleware/unKnownEndpoint.js b/middleware/unKnownEndpoint.js new file mode 100644 index 0000000..c0ce595 --- /dev/null +++ b/middleware/unKnownEndpoint.js @@ -0,0 +1,8 @@ +const { ErrorHandler } = require("../helpers/error"); + +// eslint-disable-next-line no-unused-vars +const unknownEndpoint = (request, response) => { + throw new ErrorHandler(401, "unknown endpoint"); +}; + +module.exports = unknownEndpoint; diff --git a/middleware/verifyAdmin.js b/middleware/verifyAdmin.js new file mode 100644 index 0000000..4a04109 --- /dev/null +++ b/middleware/verifyAdmin.js @@ -0,0 +1,14 @@ +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/verifyToken.js b/middleware/verifyToken.js new file mode 100644 index 0000000..58d4352 --- /dev/null +++ b/middleware/verifyToken.js @@ -0,0 +1,47 @@ +const jwt = require("jsonwebtoken"); +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]; + + 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); + // } + // }); + + 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 + next(); + } catch (error) { + throw new ErrorHandler(401, error.message || "Invalid Token"); + } +}; + +module.exports = verifyToken; From e4d3c8e8d5566195c5286168d9910de868bafb5d Mon Sep 17 00:00:00 2001 From: bragaz_rexita Date: Wed, 17 Sep 2025 13:29:12 +0700 Subject: [PATCH 4/6] Add skeleton --- routes/auth.js | 8 ++++++++ routes/index.js | 8 ++++++++ routes/users.js | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 routes/auth.js create mode 100644 routes/index.js create mode 100644 routes/users.js diff --git a/routes/auth.js b/routes/auth.js new file mode 100644 index 0000000..1c738f0 --- /dev/null +++ b/routes/auth.js @@ -0,0 +1,8 @@ +const router = require("express").Router(); +const { + loginUser, +} = require("../controllers/auth.controller"); + +router.post("/login", loginUser); + +module.exports = router; diff --git a/routes/index.js b/routes/index.js new file mode 100644 index 0000000..92d5909 --- /dev/null +++ b/routes/index.js @@ -0,0 +1,8 @@ +const router = require("express").Router(); +const auth = require("./auth"); +const users = require("./users"); + +router.use("/auth", auth); +router.use("/users", users); + +module.exports = router; diff --git a/routes/users.js b/routes/users.js new file mode 100644 index 0000000..3cbf210 --- /dev/null +++ b/routes/users.js @@ -0,0 +1,33 @@ +const { + getAllUsers, + createUser, + deleteUser, + getUserById, + updateUser, + getUserProfile, + getAllRoles, + getAllStatusUsers +} = require("../controllers/users.controller"); +const router = require("express").Router(); +const verifyAdmin = require("../middleware/verifyAdmin"); +const verifyToken = require("../middleware/verifyToken"); + +router.get("/roles", getAllRoles); + +router.route("/profile") + .get(getUserProfile); + +router.route("/") + .get(verifyToken, getAllUsers) + .post(verifyToken, createUser); + +router + .route("/status") + .get(verifyToken, getAllStatusUsers); + +router.route("/:id") + .get(verifyToken, getUserById) + .put(verifyToken, updateUser) + .delete(verifyToken, deleteUser); + +module.exports = router; From 3feb9aa7232be775fa69ee56ff59ce733bb2758b Mon Sep 17 00:00:00 2001 From: bragaz_rexita Date: Wed, 17 Sep 2025 13:29:20 +0700 Subject: [PATCH 5/6] Add skeleton --- services/auth.service.js | 77 ++++++++++++++++++++++++ services/user.service.js | 124 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 services/auth.service.js create mode 100644 services/user.service.js diff --git a/services/auth.service.js b/services/auth.service.js new file mode 100644 index 0000000..583d300 --- /dev/null +++ b/services/auth.service.js @@ -0,0 +1,77 @@ +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"); + +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); + } + } + + 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"); + } + } + + 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); + } + } +} + +module.exports = new AuthService(); diff --git a/services/user.service.js b/services/user.service.js new file mode 100644 index 0000000..2e95fda --- /dev/null +++ b/services/user.service.js @@ -0,0 +1,124 @@ +const { + createUserDb, + changeUserPasswordDb, + getUserByIdDb, + updateUserDb, + deleteUserDb, + getAllUsersDb, + getUserByUsernameDb, + getAllRoleDb +} = require("../db/user.db"); +const { ErrorHandler } = require("../helpers/error"); +const { convertId } = require("../helpers/utils"); + +const statusName = [ + { + status: true, + status_name: "Aktif" + }, { + status: false, + status_name: "NonAktif" + } +]; + +class UserService { + + getAllStatusUsers = async () => { + try { + return statusName; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + }; + + getAllUsers = async (param) => { + try { + const results = await getAllUsersDb(param); + + 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') + }); + + 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); + } + }; + + getUserById = async (id) => { + try { + const user = await getUserByIdDb(id); + // user.password = undefined; + user.is_active = user.is_active == 1 ? true : false + return user; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + }; + + changeUserPassword = async (password, email, tenantID) => { + try { + return await changeUserPasswordDb(password, email, tenantID); + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + }; + + updateUser = async (param) => { + const { userName, id } = param; + const errors = {}; + try { + + 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"; + } + + if (Object.keys(errors).length > 0) { + throw new ErrorHandler(403, errors); + } + + return await updateUserDb(param); + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + }; + + deleteUser = async (id, userID) => { + try { + return await deleteUserDb(id, userID); + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + }; + + getAllRoles = async (tenantID) => { + try { + return await getAllRoleDb(tenantID); + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + }; +} + +module.exports = new UserService(); From 6212048aecae4fa4fea372ccacc55ad96be739b1 Mon Sep 17 00:00:00 2001 From: bragaz_rexita Date: Wed, 17 Sep 2025 13:29:29 +0700 Subject: [PATCH 6/6] Add skeleton --- .env.example | 43 ++++++++++++++++++++++++++++++ .eslintrc.js | 20 ++++++++++++++ .gitignore | 8 +++--- .prettierrc | 8 ++++++ Procfile | 1 + app.js | 30 +++++++++++++++++++++ ecosystem.config.js | 14 ++++++++++ index.js | 10 +++++++ package.json | 64 +++++++++++++++++++++++++++++++++++++++++++++ utils/logger.js | 9 +++++++ 10 files changed, 203 insertions(+), 4 deletions(-) create mode 100644 .env.example create mode 100644 .eslintrc.js create mode 100644 .prettierrc create mode 100644 Procfile create mode 100644 app.js create mode 100644 ecosystem.config.js create mode 100644 index.js create mode 100644 package.json create mode 100644 utils/logger.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..3be6766 --- /dev/null +++ b/.env.example @@ -0,0 +1,43 @@ +# # SQL DB Connection Colo +# SQL_HOST=117.102.231.130 +# 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 + +# Application Port - express server listens on this port (default 9000). +PORT=9528 +ENDPOINT_WA=http://203.153.114.226:9529/send +# ENDPOINT_WA=http://localhost:9529/send +ENDPOINT_FE=http://203.153.114.226:9527 + +# JWT access secret +SECRET=secret + +# JWT refresh secret +REFRESH_SECRET=refreshsecret + +# mail server settings +# SMTP_FROM=youremail +# SMTP_USER=youremail + +# Stripe secret key - https://stripe.com/docs/keys +# STRIPE_SECRET_KEY=sk_test_4eC39HqLyjWDarjtT1zdp7dc + +# Google OAuth2.0 settings for sign in with Google - https://console.developers.google.com/ +# OAUTH_CLIENT_ID=287280guajkxxxxxxx.apps.googleusercontent.com +# OAUTH_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxx +# OAUTH_REFRESH_TOKEN=1//XXXXXXXXXX + +# Google OAuth2.0 settings for sending emails - https://console.developers.google.com/ +# CLIENT_ID=938729280guajk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com +# CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx +# REFRESH_TOKEN=1//XXXXXXXX + +VITE_KEY_SESSION=PetekRombonganPetekMorekMorakMarek diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..1a9399c --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,20 @@ +module.exports = { + env: { + commonjs: true, + es2021: true, + node: true, + jest: true, + }, + extends: ["eslint:recommended"], + parserOptions: { + ecmaVersion: 12, + }, + parser: "babel-eslint", + plugins: ["babel", "prettier"], + rules: { + "no-console": "warn", + eqeqeq: "error", + // "object-curly-spacing": ["error", "always"], + // "arrow-spacing": ["error", { before: true, after: true }], + }, +}; diff --git a/.gitignore b/.gitignore index 1ae140b..ee0f965 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ +.env node_modules -log -tmp -/public/** -!public/.gitkeep \ No newline at end of file +.vscode +request.http +*.rest diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..f2d1f8e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "tabWidth": 2, + "printWidth": 80, + "singleQuote": false, + "trailingComma": "es5", + "endOfLine": "lf" +} diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..1da0cd6 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: node index.js diff --git a/app.js b/app.js new file mode 100644 index 0000000..8674820 --- /dev/null +++ b/app.js @@ -0,0 +1,30 @@ +const express = require("express"); +require("express-async-errors"); +const cors = require("cors"); +const morgan = require("morgan"); +const cookieParser = require("cookie-parser"); +const routes = require("./routes"); +const helmet = require("helmet"); +const compression = require("compression"); +const unknownEndpoint = require("./middleware/unKnownEndpoint"); +const { handleError } = require("./helpers/error"); + +const app = express(); + +app.set("trust proxy", 1); +app.use(cors({ credentials: true, origin: true })); +app.use(express.json()); +app.use(morgan("dev")); +app.use(compression()); +app.use(helmet()); +app.use(cookieParser()); + +app.use("/api", routes); + +app.get("/", (req, res) => + res.send("

HAHALO

") +); +app.use(unknownEndpoint); +app.use(handleError); + +module.exports = app; diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..83bdbbf --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,14 @@ +module.exports = { + apps: [ + { + name: "bengkel-api", + script: "./index.js", // Path to your entry file + env: { + NODE_ENV: "development", + }, + env_production: { + NODE_ENV: "production", + }, + }, + ], +}; \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..552b3a5 --- /dev/null +++ b/index.js @@ -0,0 +1,10 @@ +require("dotenv").config({ path: __dirname + "/.env" }); +const http = require("http"); +const app = require("./app"); +const { logger } = require("./utils/logger"); + +const server = http.createServer(app); + +const PORT = process.env.PORT || 9524; + +server.listen(PORT, () => logger.info(`Magic happening on port: ${PORT}`)); diff --git a/package.json b/package.json new file mode 100644 index 0000000..0645c2a --- /dev/null +++ b/package.json @@ -0,0 +1,64 @@ +{ + "name": "server", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "cross-env NODE_ENV=production node index", + "dev": "cross-env NODE_ENV=development && nodemon --legacy-watch", + "test": "cross-env NODE_ENV=test jest --verbose --runInBand", + "test:watch": "cross-env NODE_ENV=test jest --verbose --runInBand --watch", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --write ." + }, + "jest": { + "testEnvironment": "node", + "coveragePathIgnorePatterns": [ + "/node_modules/" + ] + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "axios": "^1.9.0", + "bcrypt": "^5.1.1", + "compression": "^1.7.4", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "crypto": "^1.0.1", + "crypto-js": "^4.2.0", + "dotenv": "^8.2.0", + "express": "^4.18.2", + "express-async-errors": "^3.1.1", + "google-auth-library": "^8.7.0", + "googleapis": "^112.0.0", + "helmet": "^4.4.1", + "joi": "^17.13.3", + "jsonwebtoken": "^8.5.1", + "moment": "^2.29.4", + "morgan": "^1.10.0", + "mqtt": "^5.14.0", + "mssql": "^11.0.1", + "multer": "^1.4.5-lts.2", + "nodemailer": "^6.8.0", + "pg": "^8.8.0", + "pino": "^6.11.3", + "stripe": "^8.138.0", + "svg-captcha": "^1.4.0", + "swagger-ui-express": "^4.6.0", + "uuid": "^11.1.0" + }, + "devDependencies": { + "babel-eslint": "^10.1.0", + "cross-env": "^7.0.3", + "eslint": "^7.32.0", + "eslint-plugin-babel": "^5.3.1", + "eslint-plugin-prettier": "^4.2.1", + "nodemon": "^2.0.20", + "pino-pretty": "^4.8.0", + "prettier": "^2.8.1", + "supertest": "^6.3.3" + } +} diff --git a/utils/logger.js b/utils/logger.js new file mode 100644 index 0000000..b8591e1 --- /dev/null +++ b/utils/logger.js @@ -0,0 +1,9 @@ +const pino = require("pino"); + +// Create a logging instance +const logger = pino({ + level: process.env.NODE_ENV === "production" ? "info" : "debug", + prettyPrint: process.env.NODE_ENV !== "production", +}); + +module.exports.logger = logger;