commit 7fd2f07234b7be2ba9894f48a7d217a9848fef2f Author: yogiedigital Date: Mon Sep 22 10:45:25 2025 +0700 “init” 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 new file mode 100644 index 0000000..ee0f965 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.env +node_modules +.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/readme.md b/readme.md new file mode 100644 index 0000000..9a20603 --- /dev/null +++ b/readme.md @@ -0,0 +1,7 @@ +touch README.md +git init +git checkout -b main +git add README.md +git commit -m "first commit" +git remote add origin https://gitea.idetama.id/yogiedigital/cod-api.git +git push -u origin main \ No newline at end of file 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;