Add skeleton

This commit is contained in:
2025-09-17 13:28:36 +07:00
parent e80165c66a
commit f4ab31b436
4 changed files with 544 additions and 0 deletions

204
config/index.js Normal file
View File

@@ -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,
};

View File

@@ -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,
};

View File

@@ -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
};

142
db/user.db.js Normal file
View File

@@ -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,
};