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/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,
+};
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/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;
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/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;
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/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;
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();
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;