diff --git a/.env.example b/.env.example index 3be6766..8368363 100644 --- a/.env.example +++ b/.env.example @@ -1,18 +1,18 @@ -# # SQL DB Connection Colo -# SQL_HOST=117.102.231.130 +# SQL DB Connection Colo +SQL_HOST=117.102.231.130 +SQL_DATABASE=cod_piu +SQL_USERNAME=sa +SQL_PASSWORD=@R3M4niA. +SQL_PORT=1433 + +# SQL_HOST=203.153.114.226 +# SQL_PORT=1112 # SQL_DATABASE=piu # SQL_USERNAME=sa -# SQL_PASSWORD=@R3M4niA. -# SQL_PORT=1433 - -SQL_HOST=203.153.114.226 -SQL_PORT=1112 -SQL_DATABASE=piu -SQL_USERNAME=sa -SQL_PASSWORD=piu123 +# SQL_PASSWORD=piu123 # Application Port - express server listens on this port (default 9000). -PORT=9528 +PORT=9530 ENDPOINT_WA=http://203.153.114.226:9529/send # ENDPOINT_WA=http://localhost:9529/send ENDPOINT_FE=http://203.153.114.226:9527 diff --git a/.gitignore b/.gitignore index ee0f965..51d619b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules .vscode request.http *.rest +package-lock.json \ No newline at end of file diff --git a/app.js b/app.js index 8674820..c83b7ff 100644 --- a/app.js +++ b/app.js @@ -8,6 +8,7 @@ const helmet = require("helmet"); const compression = require("compression"); const unknownEndpoint = require("./middleware/unKnownEndpoint"); const { handleError } = require("./helpers/error"); +const { checkConnection } = require("./config"); const app = express(); @@ -24,6 +25,25 @@ app.use("/api", routes); app.get("/", (req, res) => res.send("

HAHALO

") ); + +app.get("/check-db", async (req, res) => { + try { + const isConnected = await checkConnection(); + res.json({ + success: isConnected, + message: isConnected + ? "Koneksi database OK" + : "Koneksi database gagal", + }); + } catch (error) { + res.status(500).json({ + success: false, + message: "Terjadi kesalahan saat cek koneksi database", + error: error.message, + }); + } +}); + app.use(unknownEndpoint); app.use(handleError); diff --git a/config/index.js b/config/index.js index 8ed5673..fe0b713 100644 --- a/config/index.js +++ b/config/index.js @@ -28,6 +28,18 @@ const poolPromise = new sql.ConnectionPool(config) process.exit(1); }); +async function checkConnection() { + try { + const pool = await poolPromise; + await pool.request().query('SELECT 1 AS isConnected'); + console.log('🔍 SQL Server terkoneksi dengan baik'); + return true; + } catch (error) { + console.error('⚠️ Gagal cek koneksi SQL Server:', error); + return false; + } +} + /** * Wrapper query (auto konversi $1 → @p1) */ @@ -46,6 +58,11 @@ async function query(text, params = []) { return request.query(sqlText); } +function isValidDate(dateStr) { + const d = new Date(dateStr); + return !isNaN(d.getTime()); // true kalau valid +} + /** * Build filter query */ @@ -71,10 +88,24 @@ function buildFilterQuery(filterQuery = [], fixedParams = []) { queryParams.push(f.param ? 1 : 0); whereConditions.push(`${f.column} = $${queryParams.length}`); break; + + case 'between': + if (Array.isArray(f.param) && f.param.length === 2) { + const from = f.param[0]; + const to = f.param[1]; + if (isValidDate(from) && isValidDate(to)) { + queryParams.push(from); + queryParams.push(to); + whereConditions.push( + `${f.column} BETWEEN $${queryParams.length - 1} AND $${queryParams.length}` + ); + } + } + break; } }); - return { whereConditions, queryParams }; + return { whereConditions, whereParamAnd: queryParams }; } /** @@ -96,13 +127,17 @@ function buildStringOrIlike(columnParam, criteria, fixedParams = []) { ? `AND (${orStringConditions.join(" OR ")})` : ""; - return { whereOrConditions: whereClause, whereParam: queryParams }; + return { whereOrConditions: whereClause, whereParamOr: queryParams }; } /** * Build dynamic UPDATE */ function buildDynamicUpdate(table, data, where) { + + data.updated_by = data.userId + delete data.userId; + const setParts = []; const values = []; let index = 1; @@ -118,8 +153,8 @@ function buildDynamicUpdate(table, data, where) { throw new Error("Tidak ada kolom untuk diupdate"); } - // updated_at otomatis pakai GETDATE() - setParts.push(`updated_at = GETDATE()`); + // updated_at otomatis pakai CURRENT_TIMESTAMP + setParts.push(`updated_at = CURRENT_TIMESTAMP`); const whereParts = []; for (const [key, value] of Object.entries(where)) { @@ -140,6 +175,11 @@ function buildDynamicUpdate(table, data, where) { * Build dynamic INSERT */ function buildDynamicInsert(table, data) { + + data.created_by = data.userId + data.updated_by = data.userId + delete data.userId; + const columns = []; const placeholders = []; const values = []; @@ -159,7 +199,7 @@ function buildDynamicInsert(table, data) { // created_at & updated_at otomatis columns.push("created_at", "updated_at"); - placeholders.push("GETDATE()", "GETDATE()"); + placeholders.push("CURRENT_TIMESTAMP", "CURRENT_TIMESTAMP"); const query = ` INSERT INTO ${table} (${columns.join(", ")}) @@ -195,6 +235,7 @@ async function generateKode(prefix, tableName, columnName) { } module.exports = { + checkConnection, query, buildFilterQuery, buildStringOrIlike, diff --git a/controllers/auth.controller.js b/controllers/auth.controller.js index 464b3cd..de1a9dd 100644 --- a/controllers/auth.controller.js +++ b/controllers/auth.controller.js @@ -1,26 +1,99 @@ -const authService = require("../services/auth.service"); +const AuthService = require('../services/auth.service'); +const { setResponse, checkValidate } = require('../helpers/utils'); +const { registerSchema, loginSchema } = require('../validate/auth.schema'); +const { createCaptcha } = require('../utils/captcha'); -const loginUser = async (req, res) => { - const { username, password, role, tenant } = req.body; - const { token, refreshToken, user } = await authService.login( - username, - password, - tenant - ); +class AuthController { + // Register + static async register(req, res) { + const { error, value } = await checkValidate(registerSchema, req); - 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, - }); -}; + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } -module.exports = { - loginUser, -}; + // Format nomor HP Indonesia + if (value.user_phone && value.user_phone.startsWith('0')) { + value.user_phone = '+62' + value.user_phone.slice(1); + } + + const results = await AuthService.register(value); + + const response = await setResponse( + { + user: { ...results.user, approved: false }, + }, + 'User registered successfully. Waiting for admin approval.' + ); + + res.status(response.statusCode).json(response); + } + + // Login + static async login(req, res) { + const { error, value } = await checkValidate(loginSchema, req); + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + const results = await AuthService.login(value); + + // Simpan refresh token di cookie + res.cookie('refreshToken', results.tokens.refreshToken, { + httpOnly: true, + secure: false, + sameSite: 'lax', + maxAge: 7 * 24 * 60 * 60 * 1000 + }); + + const response = await setResponse( + { + user: { ...results.user, approved: true }, + accessToken: results.tokens.accessToken + }, + 'Login successful' + ); + + res.status(response.statusCode).json(response); + } + + // Refresh Token + static async refreshToken(req, res) { + const refreshToken = req.cookies?.refreshToken; + + if (!refreshToken) { + return res.status(401).json(setResponse(null, 'Refresh token is required', 401)); + } + + const results = await AuthService.refreshToken(refreshToken); + const response = await setResponse(results, 'Token refreshed successfully'); + + res.status(response.statusCode).json(response); + } + + // Logout + static async logout(req, res) { + res.clearCookie('refreshToken', { + httpOnly: true, + sameSite: 'none', + secure: true + }); + + const response = await setResponse(null, 'Logged out successfully'); + res.status(response.statusCode).json(response); + } + + // Captcha + static async generateCaptcha(req, res) { + const { svg, text } = createCaptcha(); + + // Tampilkan captcha di header untuk dev + res.setHeader('X-Captcha-Text', text); + + const response = await setResponse({ svg, text }, 'Captcha generated'); + res.status(response.statusCode).json(response); + } +} + +module.exports = AuthController; diff --git a/controllers/device.controller.js b/controllers/device.controller.js new file mode 100644 index 0000000..7b2481c --- /dev/null +++ b/controllers/device.controller.js @@ -0,0 +1,71 @@ +const DeviceService = require('../services/device.service'); +const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); +const { insertDeviceSchema, updateDeviceSchema } = require('../validate/device.schema'); + +class DeviceController { + // Get all devices + static async getAll(req, res) { + const queryParams = req.query; + + const results = await DeviceService.getAllDevices(queryParams); + const response = await setResponsePaging(queryParams, results, 'Device found') + + res.status(response.statusCode).json(response); + } + + // Get device by ID + static async getById(req, res) { + const { id } = req.params; + + const results = await DeviceService.getDeviceById(id); + const response = await setResponse(results, 'Device found') + + res.status(response.statusCode).json(response); + } + + // Create device + static async create(req, res) { + const { error, value } = await checkValidate(insertDeviceSchema, req) + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id + + const results = await DeviceService.createDevice(value); + const response = await setResponse(results, 'Device created successfully') + + return res.status(response.statusCode).json(response); + } + + // Update device + static async update(req, res) { + const { id } = req.params; + + const { error, value } = checkValidate(updateDeviceSchema, req) + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id + + const results = await DeviceService.updateDevice(id, value); + const response = await setResponse(results, 'Device updated successfully') + + res.status(response.statusCode).json(response); + } + + // Soft delete device + static async delete(req, res) { + const { id } = req.params; + + const results = await DeviceService.deleteDevice(id, req.user.user_id); + const response = await setResponse(results, 'Device deleted successfully') + + res.status(response.statusCode).json(response); + } +} + +module.exports = DeviceController; diff --git a/controllers/roles.controller.js b/controllers/roles.controller.js new file mode 100644 index 0000000..d7ee09b --- /dev/null +++ b/controllers/roles.controller.js @@ -0,0 +1,71 @@ +const RolesService = require('../services/roles.service'); +const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); +const { updateRolesSchema, insertRolesSchema } = require('../validate/roles.schema'); + +class RolesController { + // Get all Roles + static async getAll(req, res) { + const queryParams = req.query; + + const results = await RolesService.getAllRoles(queryParams); + const response = await setResponsePaging(queryParams, results, 'Roles found') + + res.status(response.statusCode).json(response); + } + + // Get Roles by ID + static async getById(req, res) { + const { id } = req.params; + + const results = await RolesService.getRolesById(id); + const response = await setResponse(results, 'Roles found') + + res.status(response.statusCode).json(response); + } + + // Create Roles + static async create(req, res) { + const { error, value } = await checkValidate(insertRolesSchema, req) + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id + + const results = await RolesService.createRoles(value); + const response = await setResponse(results, 'Roles created successfully') + + return res.status(response.statusCode).json(response); + } + + // Update Roles + static async update(req, res) { + const { id } = req.params; + + const { error, value } = checkValidate(updateRolesSchema, req) + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id + + const results = await RolesService.updateRoles(id, value); + const response = await setResponse(results, 'Roles updated successfully') + + res.status(response.statusCode).json(response); + } + + // Soft delete Roles + static async delete(req, res) { + const { id } = req.params; + + const results = await RolesService.deleteRoles(id, req.user.user_id); + const response = await setResponse(results, 'Roles deleted successfully') + + res.status(response.statusCode).json(response); + } +} + +module.exports = RolesController; diff --git a/controllers/schedule.controller.js b/controllers/schedule.controller.js new file mode 100644 index 0000000..f8798b3 --- /dev/null +++ b/controllers/schedule.controller.js @@ -0,0 +1,71 @@ +const ScheduleService = require('../services/schedule.service'); +const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); +const { updateScheduleSchema, insertScheduleSchema } = require('../validate/schedule.schema'); + +class ScheduleController { + // Get all Schedule + static async getAll(req, res) { + const queryParams = req.query; + + const results = await ScheduleService.getAllSchedule(queryParams); + const response = await setResponsePaging(queryParams, results, 'Schedule found') + + res.status(response.statusCode).json(response); + } + + // Get Schedule by ID + static async getById(req, res) { + const { id } = req.params; + + const results = await ScheduleService.getScheduleById(id); + const response = await setResponse(results, 'Schedule found') + + res.status(response.statusCode).json(response); + } + + // Create Schedule + static async create(req, res) { + const { error, value } = await checkValidate(insertScheduleSchema, req) + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id + + const results = await ScheduleService.insertScheduleDb(value); + const response = await setResponse(results, 'Schedule created successfully') + + return res.status(response.statusCode).json(response); + } + + // Update Schedule + static async update(req, res) { + const { id } = req.params; + + const { error, value } = checkValidate(updateScheduleSchema, req) + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id + + const results = await ScheduleService.updateSchedule(id, value); + const response = await setResponse(results, 'Schedule updated successfully') + + res.status(response.statusCode).json(response); + } + + // Soft delete Schedule + static async delete(req, res) { + const { id } = req.params; + + const results = await ScheduleService.deleteSchedule(id, req.user.user_id); + const response = await setResponse(results, 'Schedule deleted successfully') + + res.status(response.statusCode).json(response); + } +} + +module.exports = ScheduleController; diff --git a/controllers/shift.controller.js b/controllers/shift.controller.js new file mode 100644 index 0000000..ce5ae5c --- /dev/null +++ b/controllers/shift.controller.js @@ -0,0 +1,71 @@ +const ShiftService = require('../services/shift.service'); +const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); +const { updateShiftSchema, insertShiftSchema } = require('../validate/shift.schema'); + +class ShiftController { + // Get all Shift + static async getAll(req, res) { + const queryParams = req.query; + + const results = await ShiftService.getAllShift(queryParams); + const response = await setResponsePaging(queryParams, results, 'Shift found') + + res.status(response.statusCode).json(response); + } + + // Get Shift by ID + static async getById(req, res) { + const { id } = req.params; + + const results = await ShiftService.getShiftById(id); + const response = await setResponse(results, 'Shift found') + + res.status(response.statusCode).json(response); + } + + // Create Shift + static async create(req, res) { + const { error, value } = await checkValidate(insertShiftSchema, req) + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id + + const results = await ShiftService.createShift(value); + const response = await setResponse(results, 'Shift created successfully') + + return res.status(response.statusCode).json(response); + } + + // Update Shift + static async update(req, res) { + const { id } = req.params; + + const { error, value } = checkValidate(updateShiftSchema, req) + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id + + const results = await ShiftService.updateShift(id, value); + const response = await setResponse(results, 'Shift updated successfully') + + res.status(response.statusCode).json(response); + } + + // Soft delete Shift + static async delete(req, res) { + const { id } = req.params; + + const results = await ShiftService.deleteShift(id, req.user.user_id); + const response = await setResponse(results, 'Shift deleted successfully') + + res.status(response.statusCode).json(response); + } +} + +module.exports = ShiftController; diff --git a/controllers/status.controller.js b/controllers/status.controller.js new file mode 100644 index 0000000..859be33 --- /dev/null +++ b/controllers/status.controller.js @@ -0,0 +1,73 @@ +const StatusService = require('../services/status.service'); +const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); +const { insertStatusSchema, updateStatusSchema } = require('../validate/status.schema'); + +class StatusController { + // Get all status + static async getAll(req, res) { + const queryParams = req.query; + + const results = await StatusService.getAllStatus(queryParams); + const response = await setResponsePaging(queryParams, results, 'Status found'); + + res.status(response.statusCode).json(response); + } + + // Get status by ID + static async getById(req, res) { + const { id } = req.params; + + const results = await StatusService.getStatusById(id); + const response = await setResponse(results, 'Status found'); + + res.status(response.statusCode).json(response); + } + + // Create status + static async create(req, res) { + const { error, value } = await checkValidate(insertStatusSchema, req); + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id; + + const results = await StatusService.createStatus(value); + const response = await setResponse(results, 'Status created successfully'); + + return res.status(response.statusCode).json(response); + } + + // Update status + static async update(req, res) { + const { id } = req.params; + + console.log("REQ BODY:", req.body); + + const { error, value } = await checkValidate(updateStatusSchema, req); + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id; + + const results = await StatusService.updateStatus(id, value); + const response = await setResponse(results, 'Status updated successfully'); + + res.status(response.statusCode).json(response); + } + + // Soft delete status + static async delete(req, res) { + const { id } = req.params; + + const results = await StatusService.deleteStatus(id, req.user.user_id); + const response = await setResponse(results, 'Status deleted successfully'); + + res.status(response.statusCode).json(response); + } +} + +module.exports = StatusController; diff --git a/controllers/sub_section.controller.js b/controllers/sub_section.controller.js new file mode 100644 index 0000000..f2a9655 --- /dev/null +++ b/controllers/sub_section.controller.js @@ -0,0 +1,71 @@ +const SubSectionService = require('../services/sub_section.service'); +const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); +const { insertSubSectionSchema, updateSubSectionSchema } = require('../validate/sub_section.schema'); + +class SubSectionController { + // Get all sub sections + static async getAll(req, res) { + const queryParams = req.query; + + const results = await SubSectionService.getAll(queryParams); + const response = await setResponsePaging(queryParams, results, 'Sub section found'); + + res.status(response.statusCode).json(response); + } + + // Get sub section by ID + static async getById(req, res) { + const { id } = req.params; + + const results = await SubSectionService.getById(id); + const response = await setResponse(results, 'Sub section found'); + + res.status(response.statusCode).json(response); + } + + // Create sub section + static async create(req, res) { + const { error, value } = await checkValidate(insertSubSectionSchema, req); + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id; + + const results = await SubSectionService.create(value); + const response = await setResponse(results, 'Sub section created successfully'); + + return res.status(response.statusCode).json(response); + } + + // Update sub section + static async update(req, res) { + const { id } = req.params; + + const { error, value } = checkValidate(updateSubSectionSchema, req); + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id; + + const results = await SubSectionService.update(id, value); + const response = await setResponse(results, 'Sub section updated successfully'); + + res.status(response.statusCode).json(response); + } + + // Soft delete sub section + static async delete(req, res) { + const { id } = req.params; + + const results = await SubSectionService.delete(id, req.user.user_id); + const response = await setResponse(results, 'Sub section deleted successfully'); + + res.status(response.statusCode).json(response); + } +} + +module.exports = SubSectionController; \ No newline at end of file diff --git a/controllers/tags.controller.js b/controllers/tags.controller.js new file mode 100644 index 0000000..fa3dc16 --- /dev/null +++ b/controllers/tags.controller.js @@ -0,0 +1,71 @@ +const TagsService = require('../services/tags.service'); +const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); +const { insertTagsSchema, updateTagsSchema } = require('../validate/tags.schema'); + +class TagsController { + // Get all devices + static async getAll(req, res) { + const queryParams = req.query; + + const results = await TagsService.getAllTags(queryParams); + const response = await setResponsePaging(queryParams, results, 'Tags found') + + res.status(response.statusCode).json(response); + } + + // Get device by ID + static async getById(req, res) { + const { id } = req.params; + + const results = await TagsService.getTagByID(id); + const response = await setResponse(results, 'Tags found') + + res.status(response.statusCode).json(response); + } + + // Create device + static async create(req, res) { + const { error, value } = await checkValidate(insertTagsSchema, req) + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id + + const results = await TagsService.createTags(value); + const response = await setResponse(results, 'Tags created successfully') + + return res.status(response.statusCode).json(response); + } + + // Update device + static async update(req, res) { + const { id } = req.params; + + const { error, value } = checkValidate(updateTagsSchema, req) + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.userId = req.user.user_id + + const results = await TagsService.updateTags(id, value); + const response = await setResponse(results, 'Tags updated successfully') + + res.status(response.statusCode).json(response); + } + + // Soft delete device + static async delete(req, res) { + const { id } = req.params; + + const results = await TagsService.deleteTags(id, req.user.user_id); + const response = await setResponse(results, 'Tags deleted successfully') + + res.status(response.statusCode).json(response); + } +} + +module.exports = TagsController; diff --git a/controllers/unit.controller.js b/controllers/unit.controller.js new file mode 100644 index 0000000..4e323f6 --- /dev/null +++ b/controllers/unit.controller.js @@ -0,0 +1,71 @@ +const UnitService = require('../services/unit.service'); +const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); +const { insertUnitSchema, updateUnitSchema } = require('../validate/unit.schema'); + +class UnitController { + // Get all units + static async getAll(req, res) { + const queryParams = req.query; + + const results = await UnitService.getAllUnits(queryParams); + const response = await setResponsePaging(queryParams, results, 'Unit found'); + + res.status(response.statusCode).json(response); + } + + // Get unit by ID + static async getById(req, res) { + const { id } = req.params; + + const results = await UnitService.getUnitById(id); + const response = await setResponse(results, 'Unit found'); + + res.status(response.statusCode).json(response); + } + + // Create unit + static async create(req, res) { + const { error, value } = await checkValidate(insertUnitSchema, req); + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.created_by = req.user.user_id; + + const results = await UnitService.createUnit(value); + const response = await setResponse(results, 'Unit created successfully'); + + return res.status(response.statusCode).json(response); + } + + // Update unit + static async update(req, res) { + const { id } = req.params; + + const { error, value } = checkValidate(updateUnitSchema, req); + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.updated_by = req.user.user_id; + + const results = await UnitService.updateUnit(id, value); + const response = await setResponse(results, 'Unit updated successfully'); + + res.status(response.statusCode).json(response); + } + + // Soft delete unit + static async delete(req, res) { + const { id } = req.params; + + const results = await UnitService.deleteUnit(id, req.user.user_id); + const response = await setResponse(results, 'Unit deleted successfully'); + + res.status(response.statusCode).json(response); + } +} + +module.exports = UnitController; diff --git a/controllers/users.controller.js b/controllers/users.controller.js index ae9548e..aef8858 100644 --- a/controllers/users.controller.js +++ b/controllers/users.controller.js @@ -1,172 +1,108 @@ -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"); +const UserService = require("../services/user.service"); +const { setResponse, setResponsePaging, checkValidate } = require("../helpers/utils"); +const { userSchema, updateUserSchema, newPasswordSchema } = require("../validate/user.schema"); -// 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() -}); +class UserController { + // Get all users + static async getAll(req, res) { + const queryParams = req.query; -const getAllUsers = async (req, res) => { + const results = await UserService.getAllUsers(queryParams); + const response = await setResponsePaging(queryParams, results, 'Users found'); - const { - page = 1, - limit = 10, - fullname: userFullname, - username: userName, - is_active: isActive, - criteria, - tenantID, - } = req.query + res.status(response.statusCode).json(response); + } - const offset = (page - 1) * limit; + // Get user by ID + static async getById(req, res) { + const { id } = req.params; - 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.getUserById(id); + const response = await setResponse(results, 'User found'); + + res.status(response.statusCode).json(response); + } + + // Create user + static async create(req, res) { + const { error, value } = await checkValidate(userSchema, req); + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); } + + value.approved_by = req.user.user_id; + + const results = await UserService.createUser(value); + const response = await setResponse(results, 'User created successfully'); + + res.status(response.statusCode).json(response); } - const results = await userService.getAllUsers(filterQuery) - const response = await setResponsePaging(results.data, results.total, parseInt(limit), parseInt(page)) + // Update user + static async update(req, res) { + const { id } = req.params; - res.status(response.statusCode).json(response) -}; + const { error, value } = await checkValidate(updateUserSchema, req); -const getAllStatusUsers = async (req, res) => { + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } - const results = await userService.getAllStatusUsers(); - const response = await setResponse(results) + value.userId = req.user.user_id; - res.status(response.statusCode).json(response); -}; + const results = await UserService.updateUser(id, value); + const response = await setResponse(results, 'User updated successfully'); -const createUser = async (req, res) => { + res.status(response.statusCode).json(response); + } + + // Approve user + static async approve(req, res) { + const { id } = req.params; + const approverId = req.user.user_id; + + const updatedUser = await UserService.approveUser(id, approverId); + const response = await setResponse(updatedUser, 'User approved successfully'); - // 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 - }); + // Reject user + static async reject(req, res) { + const { id } = req.params; + const approverId = req.user.user_id; - const response = await setResponse(results); + const updatedUser = await UserService.rejectUser(id, approverId); + const response = await setResponse(updatedUser, 'User rejected successfully'); - 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 - }); + // Soft delete user + static async delete(req, res) { + const { id } = req.params; - const response = await setResponse(results); + const results = await UserService.deleteUser(id, req.user.user_id); + const response = await setResponse(results, 'User deleted successfully'); - res.status(response.statusCode).json(response); -}; + res.status(response.statusCode).json(response); + } -const deleteUser = async (req, res) => { - const { id } = req.params; - const userID = req.userID + // Change password + static async changePassword(req, res) { + const { id } = req.params; + const { error, value } = await checkValidate(newPasswordSchema, req); - const results = await userService.deleteUser(id, userID); - const response = await setResponse(results) + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } - res.status(response.statusCode).json(response); -}; + const results = await UserService.changeUserPassword(id, value.new_password); + const response = await setResponse(results, 'Password changed successfully'); -const getAllRoles = async (req, res) => { - const results = await userService.getAllRoles(req.body.tenantID); - const response = await setResponse(results) + res.status(response.statusCode).json(response); + } +} - res.status(response.statusCode).json(response); -}; - -module.exports = { - getAllUsers, - createUser, - getUserById, - updateUser, - deleteUser, - getUserProfile, - getAllRoles, - getAllStatusUsers -}; +module.exports = UserController; diff --git a/db/brand.db.js b/db/brand.db.js new file mode 100644 index 0000000..6182e1a --- /dev/null +++ b/db/brand.db.js @@ -0,0 +1,116 @@ +const pool = require("../config"); + +// Get all brands +const getAllBrandsDb = async (searchParams = {}) => { + let queryParams = []; + + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1; + queryParams = [Number(searchParams.limit ?? 10), page]; + } + + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + ["b.brand_name"], + searchParams.criteria, + queryParams + ); + + queryParams = whereParamOr ? whereParamOr : queryParams; + + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [ + { column: "b.brand_name", param: searchParams.name, type: "string" }, + { column: "b.created_by", param: searchParams.created_by, type: "number" }, + ], + queryParams + ); + + queryParams = whereParamAnd ? whereParamAnd : queryParams; + + const queryText = ` + SELECT COUNT(*) OVER() AS total_data, b.* + FROM m_brands b + WHERE b.deleted_at IS NULL + ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} + ${whereOrConditions ? whereOrConditions : ""} + ORDER BY b.brand_id ASC + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""}; + `; + + const result = await pool.query(queryText, queryParams); + + const total = result?.recordset.length > 0 ? parseInt(result.recordset[0].total_data, 10) : 0; + + return { data: result.recordset, total }; +}; + +// Get brand by ID +const getBrandByIdDb = async (id) => { + const queryText = ` + SELECT b.* + FROM m_brands b + WHERE b.brand_id = $1 AND b.deleted_at IS NULL + `; + const result = await pool.query(queryText, [id]); + return result.recordset; +}; + +// Get brand by name +const getBrandByNameDb = async (name) => { + const queryText = ` + SELECT b.* + FROM m_brands b + WHERE b.brand_name = $1 AND b.deleted_at IS NULL + `; + const result = await pool.query(queryText, [name]); + return result.recordset[0]; +}; + +// Create brand +const createBrandDb = async (data) => { + const store = { + ...data, + created_at: new Date(), + }; + + const { query: queryText, values } = pool.buildDynamicInsert("m_brands", store); + const result = await pool.query(queryText, values); + const insertedId = result.recordset[0]?.inserted_id; + return insertedId ? await getBrandByIdDb(insertedId) : null; +}; + +// Update brand +const updateBrandDb = async (id, data) => { + const store = { + ...data, + updated_at: new Date(), + }; + + const whereData = { + brand_id: id, + }; + + const { query: queryText, values } = pool.buildDynamicUpdate("m_brands", store, whereData); + await pool.query(`${queryText} AND deleted_at IS NULL`, values); + return getBrandByIdDb(id); +}; + +// Soft delete brand +const deleteBrandDb = async (id, deletedBy) => { + const queryText = ` + UPDATE m_brands + SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 + WHERE brand_id = $2 AND deleted_at IS NULL + `; + await pool.query(queryText, [deletedBy, id]); + return true; +}; + +module.exports = { + getAllBrandsDb, + getBrandByIdDb, + getBrandByNameDb, + createBrandDb, + updateBrandDb, + deleteBrandDb, +}; diff --git a/db/device.db.js b/db/device.db.js new file mode 100644 index 0000000..c28d9a0 --- /dev/null +++ b/db/device.db.js @@ -0,0 +1,133 @@ +const pool = require("../config"); + +// Get all devices +const getAllDevicesDb = async (searchParams = {}) => { + let queryParams = []; + + // Pagination + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1; + queryParams = [Number(searchParams.limit ?? 10), page]; + } + + // Search + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + [ + "a.device_name", + "a.device_code", + "a.device_location", + "a.ip_address", + "b.brand_name", + ], + searchParams.criteria, + queryParams + ); + + queryParams = whereParamOr ? whereParamOr : queryParams; + + // Filter + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [ + { column: "a.device_code", param: searchParams.code, type: "string" }, + { column: "a.device_location", param: searchParams.location, type: "string" }, + { column: "b.brand_name", param: searchParams.brand, type: "string" }, + { column: "a.is_active", param: searchParams.status, type: "string" }, + ], + queryParams + ); + + queryParams = whereParamAnd ? whereParamAnd : queryParams; + + const queryText = ` + SELECT + COUNT(*) OVER() AS total_data, + a.*, + b.brand_name, + COALESCE(a.device_code, '') + ' - ' + COALESCE(a.device_name, '') AS device_code_name + FROM m_device a + LEFT JOIN m_brands b ON a.brand_id = b.brand_id + WHERE a.deleted_at IS NULL + ${whereConditions.length > 0 ? `AND ${whereConditions.join(' AND ')}` : ''} + ${whereOrConditions ? whereOrConditions : ''} + ORDER BY a.device_id ASC + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ''}; + `; + + const result = await pool.query(queryText, queryParams); + + const total = + result?.recordset.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; + + return { data: result.recordset, total }; +}; + + +const getDeviceByIdDb = async (id) => { + const queryText = ` + SELECT + a.*, + b.brand_name, + COALESCE(a.device_code, '') + ' - ' + COALESCE(a.device_name, '') AS device_code_name + FROM m_device a + LEFT JOIN m_brands b ON a.brand_id = b.brand_id + WHERE a.device_id = $1 AND a.deleted_at IS NULL + `; + const result = await pool.query(queryText, [id]); + return result.recordset; +}; + +const createDeviceDb = async (data) => { + const newCode = await pool.generateKode("DVC", "m_device", "device_code"); + + const store = { + ...data, + device_code: newCode, + }; + + const { query: queryText, values } = pool.buildDynamicInsert( + "m_device", + store + ); + const result = await pool.query(queryText, values); + const insertedId = result.recordset[0]?.inserted_id; + return insertedId ? await getDeviceByIdDb(insertedId) : null; +}; + +const updateDeviceDb = async (id, data) => { + const store = { + ...data, + }; + + // Kondisi WHERE + const whereData = { + device_id: id, + }; + + const { query: queryText, values } = pool.buildDynamicUpdate( + "m_device", + store, + whereData + ); + await pool.query(`${queryText} AND deleted_at IS NULL`, values); + return getDeviceByIdDb(id); +}; + +const deleteDeviceDb = async (id, deletedBy) => { + const queryText = ` + UPDATE m_device + SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 + WHERE device_id = $2 AND deleted_at IS NULL + `; + await pool.query(queryText, [deletedBy, id]); + return true; +}; + +module.exports = { + getAllDevicesDb, + getDeviceByIdDb, + createDeviceDb, + updateDeviceDb, + deleteDeviceDb, +}; diff --git a/db/roles.db.js b/db/roles.db.js new file mode 100644 index 0000000..56c42c5 --- /dev/null +++ b/db/roles.db.js @@ -0,0 +1,99 @@ +const pool = require("../config"); + +const getAllRolesDb = async (searchParams = {}) => { + let queryParams = []; + + // Handle pagination + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1; + queryParams = [Number(searchParams.limit ?? 10), page]; + } + + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + ["a.role_name", "a.role_level", "a.role_description"], + searchParams.criteria, + queryParams + ); + if (whereParamOr) queryParams = whereParamOr; + + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [ + { column: "a.role_name", param: searchParams.role_name, type: "string" }, + { column: "a.role_level", param: searchParams.start_time, type: "string" }, + { column: "a.role_description", param: searchParams.role_description, type: "string" }, + ], + queryParams + ); + if (whereParamAnd) queryParams = whereParamAnd; + + const queryText = ` + SELECT + COUNT(*) OVER() AS total_data, + a.* + FROM m_roles a + WHERE a.deleted_at IS NULL + ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} + ${whereOrConditions ? ` ${whereOrConditions}` : ""} + ORDER BY a.role_id ASC + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""} + `; + + const result = await pool.query(queryText, queryParams); + const total = + result?.recordset?.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; + + return { data: result.recordset, total }; +}; + +const getRolesByIdDb = async (id) => { + const queryText = ` + SELECT + a.* + FROM m_roles a + WHERE a.role_id = $1 AND a.deleted_at IS NULL + `; + const result = await pool.query(queryText, [id]); + return result.recordset?.[0] || null; +}; + +const insertRolesDb = async (store) => { + const { query: queryText, values } = pool.buildDynamicInsert("m_roles", store); + const result = await pool.query(queryText, values); + const insertedId = result.recordset?.[0]?.inserted_id; + + return insertedId ? await getRolesByIdDb(insertedId) : null; +}; + +const updateRolesDb = async (id, data) => { + const store = { ...data }; + const whereData = { role_id: id }; + + const { query: queryText, values } = pool.buildDynamicUpdate( + "m_roles", + store, + whereData + ); + + await pool.query(`${queryText} AND deleted_at IS NULL`, values); + return getRolesByIdDb(id); +}; + +const deleteRolesDb = async (id, deletedBy) => { + const queryText = ` + UPDATE m_roles + SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 + WHERE role_id = $2 AND deleted_at IS NULL + `; + await pool.query(queryText, [deletedBy, id]); + return true; +}; + +module.exports = { + getAllRolesDb, + getRolesByIdDb, + insertRolesDb, + updateRolesDb, + deleteRolesDb, +}; diff --git a/db/schedule.db.js b/db/schedule.db.js new file mode 100644 index 0000000..d3a598c --- /dev/null +++ b/db/schedule.db.js @@ -0,0 +1,126 @@ +const pool = require("../config"); +const { formattedDate } = require("../utils/date"); + +// Get all schedules +const getAllScheduleDb = async (searchParams = {}) => { + let queryParams = []; + + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1; + queryParams = [Number(searchParams.limit ?? 10), page]; + } + + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + ["a.schedule_date"], + searchParams.criteria, + queryParams + ); + if (whereParamOr) queryParams = whereParamOr; + + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [{ column: "a.schedule_date", param: searchParams.name, type: "date" }], + queryParams + ); + if (whereParamAnd) queryParams = whereParamAnd; + + const queryText = ` + SELECT + COUNT(*) OVER() AS total_data, + a.*, + b.shift_name, + b.start_time, + b.end_time + FROM schedule a + LEFT JOIN m_shift b ON a.shift_id = b.shift_id + WHERE a.deleted_at IS NULL + ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} + ${whereOrConditions ? ` ${whereOrConditions}` : ""} + ORDER BY a.schedule_id ASC + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""} + `; + + const result = await pool.query(queryText, queryParams); + const total = + result?.recordset?.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; + + return { data: result.recordset, total }; +}; + +const getScheduleByIdDb = async (id) => { + const queryText = ` + SELECT + a.*, + b.shift_name, + b.start_time, + b.end_time + FROM schedule a + LEFT JOIN m_shift b ON a.shift_id = b.shift_id + WHERE a.schedule_id = $1 AND a.deleted_at IS NULL + `; + const result = await pool.query(queryText, [id]); + return result.recordset?.[0] || null; +}; + +const insertScheduleDb = async (store) => { + const nextDays = Number(store.next_day ?? 0); // default 0 kalau tidak diisi + const insertedRecords = []; + + for (let i = 0; i <= nextDays; i++) { + const nextDate = new Date(store.schedule_date); + nextDate.setDate(nextDate.getDate() + i); + + const formatted = formattedDate(nextDate); + + const newStore = { + ...store, + schedule_date: formatted, + }; + delete newStore.next_day; + + const { query: queryText, values } = pool.buildDynamicInsert("schedule", newStore); + const result = await pool.query(queryText, values); + const insertedId = result.recordset?.[0]?.inserted_id; + + if (insertedId) { + const record = await getScheduleByIdDb(insertedId); + insertedRecords.push(record); + } + } + + return insertedRecords; +}; + +const updateScheduleDb = async (id, data) => { + const store = { ...data }; + const whereData = { schedule_id: id }; + + const { query: queryText, values } = pool.buildDynamicUpdate( + "schedule", + store, + whereData + ); + + await pool.query(`${queryText} AND deleted_at IS NULL`, values); + return getScheduleByIdDb(id); +}; + +// Soft delete schedule +const deleteScheduleDb = async (id, deletedBy) => { + const queryText = ` + UPDATE schedule + SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 + WHERE schedule_id = $2 AND deleted_at IS NULL + `; + await pool.query(queryText, [deletedBy, id]); + return true; +}; + +module.exports = { + getAllScheduleDb, + getScheduleByIdDb, + insertScheduleDb, + updateScheduleDb, + deleteScheduleDb, +}; diff --git a/db/shift.db.js b/db/shift.db.js new file mode 100644 index 0000000..4dc4192 --- /dev/null +++ b/db/shift.db.js @@ -0,0 +1,100 @@ +const pool = require("../config"); + +const getAllShiftDb = async (searchParams = {}) => { + let queryParams = []; + + // Handle pagination + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1; + queryParams = [Number(searchParams.limit ?? 10), page]; + } + + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + ["a.shift_name", "a.start_time", "a.end_time"], + searchParams.criteria, + queryParams + ); + if (whereParamOr) queryParams = whereParamOr; + + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [ + { column: "a.shift_name", param: searchParams.name, type: "string" }, + { column: "a.start_time", param: searchParams.start_time, type: "time" }, + { column: "a.end_time", param: searchParams.end_time, type: "time" }, + { column: "a.is_active", param: searchParams.status, type: "string" }, + ], + queryParams + ); + if (whereParamAnd) queryParams = whereParamAnd; + + const queryText = ` + SELECT + COUNT(*) OVER() AS total_data, + a.* + FROM m_shift a + WHERE a.deleted_at IS NULL + ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} + ${whereOrConditions ? ` ${whereOrConditions}` : ""} + ORDER BY a.shift_id ASC + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""} + `; + + const result = await pool.query(queryText, queryParams); + const total = + result?.recordset?.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; + + return { data: result.recordset, total }; +}; + +const getShiftByIdDb = async (id) => { + const queryText = ` + SELECT + a.* + FROM m_shift a + WHERE a.shift_id = $1 AND a.deleted_at IS NULL + `; + const result = await pool.query(queryText, [id]); + return result.recordset?.[0] || null; +}; + +const insertShiftDb = async (store) => { + const { query: queryText, values } = pool.buildDynamicInsert("m_shift", store); + const result = await pool.query(queryText, values); + const insertedId = result.recordset?.[0]?.inserted_id; + + return insertedId ? await getShiftByIdDb(insertedId) : null; +}; + +const updateShiftDb = async (id, data) => { + const store = { ...data }; + const whereData = { shift_id: id }; + + const { query: queryText, values } = pool.buildDynamicUpdate( + "m_shift", + store, + whereData + ); + + await pool.query(`${queryText} AND deleted_at IS NULL`, values); + return getShiftByIdDb(id); +}; + +const deleteShiftDb = async (id, deletedBy) => { + const queryText = ` + UPDATE m_shift + SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 + WHERE shift_id = $2 AND deleted_at IS NULL + `; + await pool.query(queryText, [deletedBy, id]); + return true; +}; + +module.exports = { + getAllShiftDb, + getShiftByIdDb, + insertShiftDb, + updateShiftDb, + deleteShiftDb, +}; diff --git a/db/status.db.js b/db/status.db.js new file mode 100644 index 0000000..37cbd2a --- /dev/null +++ b/db/status.db.js @@ -0,0 +1,117 @@ +const pool = require("../config"); + +// Get all status +const getAllStatusDb = async (searchParams = {}) => { + let queryParams = []; + + // Pagination + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1; + queryParams = [Number(searchParams.limit ?? 10), page]; + } + + // Search + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + ["a.status_name", "a.status_description"], + searchParams.criteria, + queryParams + ); + + queryParams = whereParamOr ? whereParamOr : queryParams; + + // Filter + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [ + { column: "a.status_number", param: searchParams.number, type: "number" }, + { column: "a.is_active", param: searchParams.is_active, type: "boolean" }, + ], + queryParams + ); + + queryParams = whereParamAnd ? whereParamAnd : queryParams; + + const queryText = ` + SELECT + COUNT(*) OVER() AS total_data, + a.* + FROM m_status a + WHERE a.deleted_at IS NULL + ${whereConditions.length > 0 ? `AND ${whereConditions.join(' AND ')}` : ''} + ${whereOrConditions ? whereOrConditions : ''} + ORDER BY a.status_id ASC + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ''}; + `; + + const result = await pool.query(queryText, queryParams); + + const total = + result?.recordset.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; + + return { data: result.recordset, total }; +}; + +const getStatusByIdDb = async (id) => { + const queryText = ` + SELECT * + FROM m_status a + WHERE a.status_id = $1 AND a.deleted_at IS NULL + `; + const result = await pool.query(queryText, [id]); + return result.recordset; +}; + +// Check if status_number already exists +const checkStatusNumberExistsDb = async (status_number) => { + const queryText = ` + SELECT 1 + FROM m_status + WHERE status_number = $1 AND deleted_at IS NULL + `; + const result = await pool.query(queryText, [status_number]); + return result.recordset.length > 0; +}; + + +const createStatusDb = async (data) => { + const { query: queryText, values } = pool.buildDynamicInsert( + "m_status", + data + ); + const result = await pool.query(queryText, values); + const insertedId = result.recordset[0]?.inserted_id; + return insertedId ? await getStatusByIdDb(insertedId) : null; +}; + +const updateStatusDb = async (id, data) => { + const store = { ...data }; + const whereData = { status_id: id }; + + const { query: queryText, values } = pool.buildDynamicUpdate( + "m_status", + store, + whereData + ); + await pool.query(`${queryText} AND deleted_at IS NULL`, values); + return getStatusByIdDb(id); +}; + +const deleteStatusDb = async (id, deletedBy) => { + const queryText = ` + UPDATE m_status + SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 + WHERE status_id = $2 AND deleted_at IS NULL + `; + await pool.query(queryText, [deletedBy, id]); + return true; +}; + +module.exports = { + getAllStatusDb, + getStatusByIdDb, + createStatusDb, + updateStatusDb, + deleteStatusDb, + checkStatusNumberExistsDb, +}; diff --git a/db/sub_section.db.js b/db/sub_section.db.js new file mode 100644 index 0000000..e4358d7 --- /dev/null +++ b/db/sub_section.db.js @@ -0,0 +1,108 @@ +const pool = require("../config"); + +// Get all sub sections +const getAllSubSectionsDb = async (searchParams = {}) => { + let queryParams = []; + + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1; + queryParams = [Number(searchParams.limit ?? 10), page]; + } + + // OR condition (pencarian bebas) + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + ["a.sub_section_code", "a.sub_section_name"], + searchParams.criteria, + queryParams + ); + + queryParams = whereParamOr ?? queryParams; + + // AND condition (filter spesifik) + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [ + { column: "a.sub_section_code", param: searchParams.code, type: "string" }, + { column: "a.sub_section_name", param: searchParams.name, type: "string" }, + ], + queryParams + ); + + queryParams = whereParamAnd ?? queryParams; + + // Query utama + const queryText = ` + SELECT COUNT(*) OVER() AS total_data, a.* + FROM m_plant_sub_section a + WHERE a.deleted_at IS NULL + ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} + ${whereOrConditions ? whereOrConditions : ""} + ORDER BY a.sub_section_id ASC + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""}; + `; + + const result = await pool.query(queryText, queryParams); + const total = + result?.recordset.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; + + return { data: result.recordset, total }; +}; + +// Get sub section by ID +const getSubSectionByIdDb = async (id) => { + const queryText = ` + SELECT a.* + FROM m_plant_sub_section a + WHERE a.sub_section_id = $1 AND a.deleted_at IS NULL + `; + const result = await pool.query(queryText, [id]); + return result.recordset; +}; + +// Create new sub section +const createSubSectionDb = async (data) => { + // Generate kode otomatis + const newCode = await pool.generateKode("SUB", "m_plant_sub_section", "sub_section_code"); + + const store = { + ...data, + sub_section_code: newCode + }; + + const { query: queryText, values } = pool.buildDynamicInsert("m_plant_sub_section", store); + const result = await pool.query(queryText, values); + const insertedId = result.recordset[0]?.inserted_id; + + return insertedId ? await getSubSectionByIdDb(insertedId) : null; +}; + +// Update sub section +const updateSubSectionDb = async (id, data) => { + const store = { ...data }; + const whereData = { sub_section_id: id }; + + const { query: queryText, values } = pool.buildDynamicUpdate("m_plant_sub_section", store, whereData); + await pool.query(`${queryText} AND deleted_at IS NULL`, values); + + return getSubSectionByIdDb(id); +}; + +// Soft delete sub section +const deleteSubSectionDb = async (id, deletedBy) => { + const queryText = ` + UPDATE m_plant_sub_section + SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 + WHERE sub_section_id = $2 AND deleted_at IS NULL + `; + await pool.query(queryText, [deletedBy, id]); + return true; +}; + +module.exports = { + getAllSubSectionsDb, + getSubSectionByIdDb, + createSubSectionDb, + updateSubSectionDb, + deleteSubSectionDb, +}; diff --git a/db/tags.db.js b/db/tags.db.js new file mode 100644 index 0000000..41b61c9 --- /dev/null +++ b/db/tags.db.js @@ -0,0 +1,140 @@ +const pool = require("../config"); + +// Get all tags +const getAllTagsDb = async (searchParams = {}) => { + let queryParams = []; + + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1; + queryParams = [Number(searchParams.limit ?? 10), page]; + } + + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + [ + "a.tag_name", + "a.tag_code", + "a.tag_number", + "a.data_type", + "a.unit", + "b.device_name", + "c.sub_section_name", + ], + searchParams.criteria, + queryParams + ); + + if (whereParamOr) queryParams = whereParamOr; + + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [ + { column: "a.tag_name", param: searchParams.name, type: "string" }, + { column: "a.tag_code", param: searchParams.code, type: "string" }, + { column: "a.data_type", param: searchParams.data, type: "string" }, + { column: "a.unit", param: searchParams.unit, type: "string" }, + { column: "b.device_name", param: searchParams.device, type: "string" }, + { + column: "b.device_code", + param: searchParams.device, + type: "string", + }, + { + column: "c.sub_section_name", + param: searchParams.subsection, + type: "string", + }, + ], + queryParams + ); + + if (whereParamAnd) queryParams = whereParamAnd; + + const queryText = ` + SELECT + COUNT(*) OVER() AS total_data, + a.*, + b.device_name, + b.device_code, + c.sub_section_name + FROM m_tags a + LEFT JOIN m_device b ON a.device_id = b.device_id + LEFT JOIN m_plant_sub_section c ON a.sub_section_id = c.sub_section_id + WHERE a.deleted_at IS NULL + ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} + ${whereOrConditions ? ` ${whereOrConditions}` : ""} + ORDER BY a.tag_id ASC + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""} + `; + + const result = await pool.query(queryText, queryParams); + + const total = + result?.recordset?.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; + + return { data: result.recordset, total }; +}; + +const getTagsByIdDb = async (id) => { + const queryText = ` + SELECT + a.*, + b.device_name, + b.device_code, + c.sub_section_name + FROM m_tags a + LEFT JOIN m_device b ON a.device_id = b.device_id + LEFT JOIN m_plant_sub_section c ON a.sub_section_id = c.sub_section_id + WHERE a.tag_id = $1 AND a.deleted_at IS NULL + `; + const result = await pool.query(queryText, [id]); + return result.recordset; +}; + +const createTagsDb = async (data) => { + const newCode = await pool.generateKode("TAG", "m_tags", "tag_code"); + + const store = { + ...data, + tag_code: newCode, + }; + + const { query: queryText, values } = pool.buildDynamicInsert("m_tags", store); + const result = await pool.query(queryText, values); + const insertedId = result.recordset[0]?.inserted_id; + + return insertedId ? await getTagsByIdDb(insertedId) : null; +}; + +const updateTagsDb = async (id, data) => { + const store = { ...data }; + const whereData = { tag_id: id }; + + const { query: queryText, values } = pool.buildDynamicUpdate( + "m_tags", + store, + whereData + ); + + await pool.query(`${queryText} AND deleted_at IS NULL`, values); + return getTagsByIdDb(id); +}; + +// Soft delete tag +const deleteTagsDb = async (id, deletedBy) => { + const queryText = ` + UPDATE m_tags + SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 + WHERE tag_id = $2 AND deleted_at IS NULL + `; + await pool.query(queryText, [deletedBy, id]); + return true; +}; + +module.exports = { + getAllTagsDb, + getTagsByIdDb, + createTagsDb, + updateTagsDb, + deleteTagsDb, +}; diff --git a/db/unit.db.js b/db/unit.db.js new file mode 100644 index 0000000..ad96fb4 --- /dev/null +++ b/db/unit.db.js @@ -0,0 +1,119 @@ +const pool = require("../config"); + +// Get all units +const getAllUnitsDb = async (searchParams = {}) => { + let queryParams = []; + + // Pagination + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1; + queryParams = [Number(searchParams.limit ?? 10), page]; + } + + // Search + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + ["a.unit_code", "a.unit_name"], + searchParams.criteria, + queryParams + ); + + queryParams = whereParamOr ? whereParamOr : queryParams; + + // Filter + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [ + { column: "a.unit_code", param: searchParams.code, type: "string" }, + { column: "a.unit_name", param: searchParams.name, type: "string" }, + { column: "a.tag_id", param: searchParams.tag, type: "number" }, + { column: "a.is_active", param: searchParams.status, type: "string" }, + ], + queryParams + ); + + queryParams = whereParamAnd ? whereParamAnd : queryParams; + + const queryText = ` + SELECT + COUNT(*) OVER() AS total_data, + a.*, + COALESCE(a.unit_code, '') + ' - ' + COALESCE(a.unit_name, '') AS unit_code_name + FROM m_unit a + WHERE a.deleted_at IS NULL + ${whereConditions.length > 0 ? `AND ${whereConditions.join(" AND ")}` : ""} + ${whereOrConditions ? whereOrConditions : ""} + ORDER BY a.unit_id ASC + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""}; + `; + + const result = await pool.query(queryText, queryParams); + + const total = + result?.recordset.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; + + return { data: result.recordset, total }; +}; + +// Get unit by ID +const getUnitByIdDb = async (id) => { + const queryText = ` + SELECT + a.*, + COALESCE(a.unit_code, '') + ' - ' + COALESCE(a.unit_name, '') AS unit_code_name + FROM m_unit a + WHERE a.unit_id = $1 AND a.deleted_at IS NULL + `; + const result = await pool.query(queryText, [id]); + return result.recordset; +}; + +// Create unit +const createUnitDb = async (data) => { + const newCode = await pool.generateKode("UNT", "m_unit", "unit_code"); + + const store = { + ...data, + unit_code: newCode, + }; + + const { query: queryText, values } = pool.buildDynamicInsert("m_unit", store); + const result = await pool.query(queryText, values); + const insertedId = result.recordset[0]?.inserted_id; + return insertedId ? await getUnitByIdDb(insertedId) : null; +}; + +// Update unit +const updateUnitDb = async (id, data) => { + const store = { ...data }; + const whereData = { unit_id: id }; + + const { query: queryText, values } = pool.buildDynamicUpdate( + "m_unit", + store, + whereData + ); + + await pool.query(`${queryText} AND deleted_at IS NULL`, values); + return getUnitByIdDb(id); +}; + +// Soft delete unit +const deleteUnitDb = async (id, deletedBy) => { + const queryText = ` + UPDATE m_unit + SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 + WHERE unit_id = $2 AND deleted_at IS NULL + `; + await pool.query(queryText, [deletedBy, id]); + return true; +}; + +// Export +module.exports = { + getAllUnitsDb, + getUnitByIdDb, + createUnitDb, + updateUnitDb, + deleteUnitDb, +}; \ No newline at end of file diff --git a/db/user.db.js b/db/user.db.js index 46471bc..c8772b6 100644 --- a/db/user.db.js +++ b/db/user.db.js @@ -1,142 +1,204 @@ const pool = require("../config"); -const getAllUsersDb = async (param) => { - // limit & offset masuk fixed param - let fixedParams = [param.fixed.limit, param.fixed.offset, param.fixed.tenantID]; +// Get all users +const getAllUsersDb = async (searchParams = {}) => { + let queryParams = []; - const { whereOrConditions, whereParam } = pool.buildStringOrIlike( - param.filterCriteria.column, - param.filterCriteria.criteria, - fixedParams + // Pagination + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1; + queryParams = [Number(searchParams.limit ?? 10), page]; + } + + // Search + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( + [ + "u.user_fullname", + "u.user_name", + "u.user_email", + "r.role_name" + ], + searchParams.criteria, + queryParams ); - 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 + queryParams = whereParamOr ? whereParamOr : queryParams; + + // Filter + const { whereConditions, whereParamAnd } = pool.buildFilterQuery( + [ + { column: "u.user_fullname", param: searchParams.fullname, type: "string" }, + { column: "u.user_name", param: searchParams.username, type: "string" }, + { column: "u.user_email", param: searchParams.email, type: "string" }, + { column: "r.role_name", param: searchParams.role, type: "string" }, + ], + queryParams + ); + + queryParams = whereParamAnd ? whereParamAnd : queryParams; + + const queryText = ` + SELECT + COUNT(*) OVER() AS total_data, + u.user_id, u.user_fullname, u.user_name, u.user_email, u.user_phone, + u.is_active, u.is_sa, u.is_approve, u.approved_by, + approver.user_fullname AS approved_by_name, + u.approved_at, u.created_at, u.updated_at, u.deleted_at, + u.updated_by, u.deleted_by, + r.role_id, r.role_name, r.role_description, r.role_level + FROM m_users u + LEFT JOIN m_roles r ON u.role_id = r.role_id + LEFT JOIN m_users approver ON u.approved_by = approver.user_id + WHERE u.deleted_at IS NULL + ${whereConditions.length > 0 ? ` AND ${whereConditions.join(' AND ')}` : ''} + ${whereOrConditions ? whereOrConditions : ''} + ORDER BY u.user_id ASC + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ''}; `; - const result = await pool.query(query, queryParams); - const rows = result.recordset; + const result = await pool.query(queryText, queryParams); - 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 total = + result?.recordset.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; + + return { data: result.recordset, total }; }; +// Get user by ID const getUserByIdDb = async (id) => { - const query = ` - SELECT mut.* - FROM m_users mut - WHERE mut.user_id = $1 + const queryText = ` + SELECT + u.user_id, u.user_fullname, u.user_name, u.user_email, u.user_phone, + u.is_active, u.is_sa, u.is_approve, u.approved_by, + approver.user_fullname AS approved_by_name, + u.approved_at, u.created_at, u.updated_at, u.deleted_at, + u.updated_by, u.deleted_by, + r.role_id, r.role_name, r.role_description, r.role_level + FROM m_users u + LEFT JOIN m_roles r ON u.role_id = r.role_id + LEFT JOIN m_users approver ON u.approved_by = approver.user_id + WHERE u.user_id = $1 AND u.deleted_at IS NULL `; - const result = await pool.query(query, [id]); + const result = await pool.query(queryText, [id]); return result.recordset[0]; }; +// Get user by email +const getUserByUserEmailDb = async (email) => { + const queryText = ` + SELECT + u.user_id, u.user_fullname, u.user_name, u.user_email, u.user_phone, + u.user_password, u.is_active, u.is_sa, u.is_approve, u.role_id, + r.role_name, r.role_description, r.role_level + FROM m_users u + LEFT JOIN m_roles r ON u.role_id = r.role_id + WHERE u.user_email = $1 AND u.deleted_at IS NULL + `; + const result = await pool.query(queryText, [email]); + return result.recordset[0]; +}; + +// Get user by username const getUserByUsernameDb = async (username) => { - const query = ` - SELECT mut.* - FROM m_users mut - WHERE LOWER(mut.username) = LOWER($1) + const queryText = ` + SELECT + u.user_id, u.user_fullname, u.user_name, u.user_email, u.user_phone, + u.user_password, u.is_active, u.is_sa, u.is_approve, u.role_id, + r.role_name, r.role_description, r.role_level + FROM m_users u + LEFT JOIN m_roles r ON u.role_id = r.role_id + WHERE u.user_name = $1 AND u.deleted_at IS NULL `; - const result = await pool.query(query, [username]); + const result = await pool.query(queryText, [username]); return result.recordset[0]; }; -const getUserByUserEmailDb = async (userEmail) => { - const query = ` - SELECT mut.* - FROM m_users mut - WHERE LOWER(mut.user_email) = LOWER($1) - `; - const result = await pool.query(query, [userEmail]); - return result.recordset[0]; +// Create user +const createUserDb = async (data) => { + const { query: queryText, values } = pool.buildDynamicInsert("m_users", data); + const result = await pool.query(queryText, values); + const insertedId = result.recordset[0]?.inserted_id; + return insertedId ? await getUserByIdDb(insertedId) : null; }; -const updateUserDb = async (param) => { - const updateData = { - tenant_id: param.tenantID, - user_fullname: param.userFullname, - user_name: param.userName, - user_email: param.userEmail ?? null, - user_password: param.userPassword, - role_id: param.roleId ?? null, - is_active: param.isActive ? 1 : 0, - updated_by: param.userID, - }; - - const whereData = { user_id: param.id }; - - const { query, values } = pool.buildDynamicUpdate("m_users", updateData, whereData); - - const result = await pool.query(query, values); - return result.recordset[0]; +// Update user +const updateUserDb = async (userId, data) => { + const { query: queryText, values } = pool.buildDynamicUpdate("m_users", data, { + user_id: userId, + }); + await pool.query(`${queryText} AND deleted_at IS NULL`, values); + return getUserByIdDb(userId); }; -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 +// Approve user +const approveUserDb = async (userId, approverId) => { + const queryText = ` + UPDATE m_users + SET + is_approve = 2, + approved_by = $1, + approved_at = CURRENT_TIMESTAMP, + updated_by = $1, + updated_at = CURRENT_TIMESTAMP + WHERE user_id = $2 AND deleted_at IS NULL `; - const result = await pool.query(query, [userID, id]); - return result.recordset[0]; + await pool.query(queryText, [approverId, userId]); + return true; }; -const changeUserPasswordDb = async (hashedPassword, userEmail, tenantId) => { - const query = ` - UPDATE m_users - SET user_password = $1 - WHERE user_email = $2 AND tenant_id = $3 +// Reject user +const rejectUserDb = async (userId, approverId) => { + const queryText = ` + UPDATE m_users + SET + is_approve = 0, + approved_by = $1, + approved_at = CURRENT_TIMESTAMP, + updated_by = $1, + updated_at = CURRENT_TIMESTAMP + WHERE user_id = $2 AND deleted_at IS NULL `; - return pool.query(query, [hashedPassword, userEmail, tenantId]); + await pool.query(queryText, [approverId, userId]); + return true; +} + +// Change user password +const changeUserPasswordDb = async (userId, newPassword) => { + const queryText = ` + UPDATE m_users + SET user_password = $1, updated_at = CURRENT_TIMESTAMP + WHERE user_id = $2 AND deleted_at IS NULL + `; + await pool.query(queryText, [newPassword, userId]); + return true; }; -const getAllRoleDb = async (tenantId) => { - const query = ` - SELECT * - FROM system.role_tenant - WHERE deleted_at IS NULL AND tenant_id = $1 +// Soft delete user +const deleteUserDb = async (userId, deletedBy) => { + const queryText = ` + UPDATE m_users + SET + deleted_at = CURRENT_TIMESTAMP, + deleted_by = $1, + is_active = 0 + WHERE user_id = $2 + AND deleted_at IS NULL `; - const result = await pool.query(query, [tenantId]); - return result.recordset; + await pool.query(queryText, [deletedBy, userId]); + return true; }; module.exports = { getAllUsersDb, getUserByIdDb, getUserByUserEmailDb, - updateUserDb, - createUserDb, - deleteUserDb, getUserByUsernameDb, + createUserDb, + updateUserDb, + approveUserDb, + rejectUserDb, changeUserPasswordDb, - getAllRoleDb, + deleteUserDb, }; diff --git a/helpers/utils.js b/helpers/utils.js index 41c60ed..7ed4a3e 100644 --- a/helpers/utils.js +++ b/helpers/utils.js @@ -1,43 +1,29 @@ -const setResponse = async (data = [], message = "success", statusCode = 200) => { - const response = { - data, - total: data.length, - message, - statusCode - } +const setResponse = (data = null, message = "success", statusCode = 200) => { + const total = Array.isArray(data) ? data.length : null; - return response + return { + message, + statusCode, + rows: total, + data, + }; }; -const setResponsePaging = async (data = [], total, limit, page, message = "success", statusCode = 200) => { +const setResponsePaging = async (queryParam, data = [], message = "success", statusCode = 200) => { - const totalPages = Math.ceil(total / limit); + const totalPages = Math.ceil(data?.total / Number(queryParam.limit ?? 0)); const response = { message, statusCode, - data, - total: data.length, + rows: data?.data?.length, paging: { - total, - limit, - page, - page_total: totalPages - } - } - - return response -}; - -const setPaging = async (total, limit, page) => { - - const totalPages = Math.ceil(total / limit); - - const response = { - total, - limit, - page, - page_total: totalPages + current_limit: Number(queryParam.limit ?? 0), + current_page: Number(queryParam.page ?? 0), + total_limit: data?.total, + total_page: totalPages + }, + data: data?.data ?? [] } return response @@ -86,4 +72,19 @@ function orderByClauseQuery(orderParams) { return orderByClause } -module.exports = { setResponse, setResponsePaging, setPaging, convertId, formatToYYYYMMDD, orderByClauseQuery }; +const checkValidate = (validateSchema, req) => { + const { error, value } = validateSchema.validate(req.body || {}, { abortEarly: false }); + if (error) { + const errors = error.details.reduce((acc, cur) => { + const field = Array.isArray(cur.path) ? cur.path.join('.') : String(cur.path); + if (!acc[field]) acc[field] = []; + acc[field].push(cur.message); + return acc; + }, {}); + return { error: errors, value } + } + + return { error, value } +} + +module.exports = { setResponse, setResponsePaging, convertId, formatToYYYYMMDD, orderByClauseQuery, checkValidate }; diff --git a/helpers/validateUser.js b/helpers/validateUser.js deleted file mode 100644 index c904bb4..0000000 --- a/helpers/validateUser.js +++ /dev/null @@ -1,9 +0,0 @@ -const validateUser = (email, password) => { - const validEmail = typeof email === "string" && email.trim() !== ""; - const validPassword = - typeof password === "string" && password.trim().length >= 6; - - return validEmail && validPassword; -}; - -module.exports = validateUser; diff --git a/index.js b/index.js index 552b3a5..0f5b6e1 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,6 @@ const { logger } = require("./utils/logger"); const server = http.createServer(app); -const PORT = process.env.PORT || 9524; +const PORT = process.env.PORT || 9533; server.listen(PORT, () => logger.info(`Magic happening on port: ${PORT}`)); diff --git a/middleware/verifyAccess.js b/middleware/verifyAccess.js new file mode 100644 index 0000000..e62fee0 --- /dev/null +++ b/middleware/verifyAccess.js @@ -0,0 +1,38 @@ +const { ErrorHandler } = require("../helpers/error"); +const { getUserByIdDb } = require("../db/user.db"); + +const verifyAccess = (minLevel = 1, allowUnapprovedReadOnly = false) => { + return async (req, res, next) => { + try { + const user = req.user; + + if (!user) throw new ErrorHandler(401, "Unauthorized: User not found"); + + // Super Admin bypass semua + if (user.is_sa) return next(); + + const fullUser = await getUserByIdDb(user.user_id); + if (!fullUser) throw new ErrorHandler(403, "Forbidden: User not found"); + + if (!fullUser.is_approve) { + if (req.method !== "GET") { + throw new ErrorHandler(403, "Account not approved — read-only access"); + } + + if (allowUnapprovedReadOnly) return next(); + + throw new ErrorHandler(403, "Account not approved"); + } + + if (!fullUser.role_level || fullUser.role_level < minLevel) { + throw new ErrorHandler(403, "Forbidden: Insufficient role level"); + } + + next(); + } catch (err) { + next(err); + } + }; +}; + +module.exports = verifyAccess; diff --git a/middleware/verifyAdmin.js b/middleware/verifyAdmin.js deleted file mode 100644 index 4a04109..0000000 --- a/middleware/verifyAdmin.js +++ /dev/null @@ -1,14 +0,0 @@ -const { ErrorHandler } = require("../helpers/error"); - -module.exports = (req, res, next) => { - const { roles } = req.user; - if (roles && roles.includes("admin")) { - req.user = { - ...req.user, - roles, - }; - return next(); - } else { - throw new ErrorHandler(401, "require admin role"); - } -}; diff --git a/middleware/verifyToken.js b/middleware/verifyToken.js index 58d4352..020e967 100644 --- a/middleware/verifyToken.js +++ b/middleware/verifyToken.js @@ -1,47 +1,48 @@ -const jwt = require("jsonwebtoken"); -const { ErrorHandler } = require("../helpers/error"); +const JWTService = require('../utils/jwt'); +const { ErrorHandler } = require('../helpers/error'); -const verifyToken = (req, res, next) => { - const authHeader = req.header("Authorization"); - // console.log("authHeader", authHeader) - - // Pastikan header Authorization ada dan berisi token - if (!authHeader || !authHeader.startsWith("Bearer ")) { - throw new ErrorHandler(401, "Token missing or invalid"); - } - - // Ambil token dari header Authorization - const token = authHeader.split(" ")[1]; +function setUser(req, decoded) { + req.user = { + userId: decoded.user_id, + fullname: decoded.user_fullname, + username: decoded.user_name, + email: decoded.user_email, + roleId: decoded.role_id, + roleName: decoded.role_name, + is_sa: decoded.is_sa + }; +} +function verifyAccessToken(req, res, next) { try { - // const decoded = jwt.decode(token, { complete: true }); - // console.log("decoded", decoded) - // console.log("==============================") - // console.log("token", token) - // console.log("process.env.SECRET", process.env.SECRET) - // // console.log("==============================> ", jwt.verify(token, process.env.SECRET)) - // jwt.verify(token, process.env.SECRET, (err, decoded) => { - // if (err) { - // console.error('Error verifying token: ==============================>', err.message); - // } else { - // console.log('Decoded payload: ==============================>', decoded); - // } - // }); + let token = req.cookies?.accessToken; + + if (!token) { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer')) { + throw new ErrorHandler(401, 'Access Token is required'); + } + token = authHeader.split(' ')[1]; + } + + const decoded = JWTService.verifyToken(token); + + req.user = 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"); - } -}; + if (error.name === 'TokenExpiredError') { + return next(new ErrorHandler(401, 'Access token expired')); + } -module.exports = verifyToken; + if (error.name === 'JsonWebTokenError') { + return next(new ErrorHandler(401, 'Invalid access token')); + } + + return next(new ErrorHandler(500, 'Internal authentication error')); + } +} + +module.exports = { + verifyAccessToken +}; \ No newline at end of file diff --git a/routes/auth.js b/routes/auth.js deleted file mode 100644 index 1c738f0..0000000 --- a/routes/auth.js +++ /dev/null @@ -1,8 +0,0 @@ -const router = require("express").Router(); -const { - loginUser, -} = require("../controllers/auth.controller"); - -router.post("/login", loginUser); - -module.exports = router; diff --git a/routes/auth.route.js b/routes/auth.route.js new file mode 100644 index 0000000..166d68d --- /dev/null +++ b/routes/auth.route.js @@ -0,0 +1,11 @@ +const express = require('express'); +const AuthController = require("../controllers/auth.controller"); + +const router = express.Router(); + +router.post('/login', AuthController.login); +router.post('/register', AuthController.register); +router.get('/generate-captcha', AuthController.generateCaptcha); +router.post('/refresh-token', AuthController.refreshToken); + +module.exports = router; \ No newline at end of file diff --git a/routes/device.route.js b/routes/device.route.js new file mode 100644 index 0000000..5c74061 --- /dev/null +++ b/routes/device.route.js @@ -0,0 +1,17 @@ +const express = require('express'); +const DeviceController = require('../controllers/device.controller'); +const verifyToken = require("../middleware/verifyToken") +const verifyAccess = require("../middleware/verifyAccess") + +const router = express.Router(); + +router.route("/") + .get(verifyToken.verifyAccessToken, DeviceController.getAll) + .post(verifyToken.verifyAccessToken, verifyAccess(), DeviceController.create); + +router.route("/:id") + .get(verifyToken.verifyAccessToken, DeviceController.getById) + .put(verifyToken.verifyAccessToken, verifyAccess(), DeviceController.update) + .delete(verifyToken.verifyAccessToken, verifyAccess(), DeviceController.delete); + +module.exports = router; \ No newline at end of file diff --git a/routes/index.js b/routes/index.js index 92d5909..cc2c2c5 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,8 +1,25 @@ const router = require("express").Router(); -const auth = require("./auth"); -const users = require("./users"); +const auth = require("./auth.route"); +const users = require("./users.route"); +const device = require('./device.route'); +const roles = require('./roles.route'); +const tags = require("./tags.route"); +const subSection = require("./sub_section.route"); +const shift = require("./shift.route"); +const schedule = require("./schedule.route"); +const status = require("./status.route"); +const unit = require("./unit.route") router.use("/auth", auth); -router.use("/users", users); +router.use("/user", users); +router.use("/device", device); +router.use("/roles", roles); +router.use("/tags", tags); +router.use("/plant-sub-section", subSection); +router.use("/shift", shift); +router.use("/schedule", schedule); +router.use("/status", status); +router.use("/unit", unit); + module.exports = router; diff --git a/routes/roles.route.js b/routes/roles.route.js new file mode 100644 index 0000000..0ce2d4a --- /dev/null +++ b/routes/roles.route.js @@ -0,0 +1,17 @@ +const express = require('express'); +const RolesController = require('../controllers/roles.controller'); +const verifyToken = require("../middleware/verifyToken") +const verifyAccess = require("../middleware/verifyAccess") + +const router = express.Router(); + +router.route("/") + .get(verifyToken.verifyAccessToken, RolesController.getAll) + .post(verifyToken.verifyAccessToken, verifyAccess(), RolesController.create); + +router.route("/:id") + .get(verifyToken.verifyAccessToken, RolesController.getById) + .put(verifyToken.verifyAccessToken, verifyAccess(), RolesController.update) + .delete(verifyToken.verifyAccessToken, verifyAccess(), RolesController.delete); + +module.exports = router; \ No newline at end of file diff --git a/routes/schedule.route.js b/routes/schedule.route.js new file mode 100644 index 0000000..4c5e3e2 --- /dev/null +++ b/routes/schedule.route.js @@ -0,0 +1,17 @@ +const express = require('express'); +const ScheduleController = require('../controllers/schedule.controller'); +const verifyToken = require("../middleware/verifyToken") +const verifyAccess = require("../middleware/verifyAccess") + +const router = express.Router(); + +router.route("/") + .get(verifyToken.verifyAccessToken, ScheduleController.getAll) + .post(verifyToken.verifyAccessToken, verifyAccess(), ScheduleController.create); + +router.route("/:id") + .get(verifyToken.verifyAccessToken, ScheduleController.getById) + .put(verifyToken.verifyAccessToken, verifyAccess(), ScheduleController.update) + .delete(verifyToken.verifyAccessToken, verifyAccess(), ScheduleController.delete); + +module.exports = router; \ No newline at end of file diff --git a/routes/shift.route.js b/routes/shift.route.js new file mode 100644 index 0000000..322bb81 --- /dev/null +++ b/routes/shift.route.js @@ -0,0 +1,17 @@ +const express = require('express'); +const ShiftController = require('../controllers/shift.controller'); +const verifyToken = require("../middleware/verifyToken") +const verifyAccess = require("../middleware/verifyAccess") + +const router = express.Router(); + +router.route("/") + .get(verifyToken.verifyAccessToken, ShiftController.getAll) + .post(verifyToken.verifyAccessToken, verifyAccess(), ShiftController.create); + +router.route("/:id") + .get(verifyToken.verifyAccessToken, ShiftController.getById) + .put(verifyToken.verifyAccessToken, verifyAccess(), ShiftController.update) + .delete(verifyToken.verifyAccessToken, verifyAccess(), ShiftController.delete); + +module.exports = router; \ No newline at end of file diff --git a/routes/status.route.js b/routes/status.route.js new file mode 100644 index 0000000..eb4995f --- /dev/null +++ b/routes/status.route.js @@ -0,0 +1,17 @@ +const express = require('express'); +const StatusController = require('../controllers/status.controller'); +const verifyToken = require("../middleware/verifyToken"); +const verifyAccess = require("../middleware/verifyAccess"); + +const router = express.Router(); + +router.route("/") + .get(verifyToken.verifyAccessToken, StatusController.getAll) + .post(verifyToken.verifyAccessToken, verifyAccess(), StatusController.create); + +router.route("/:id") + .get(verifyToken.verifyAccessToken, StatusController.getById) + .put(verifyToken.verifyAccessToken, verifyAccess(), StatusController.update) + .delete(verifyToken.verifyAccessToken, verifyAccess(), StatusController.delete); + +module.exports = router; diff --git a/routes/sub_section.route.js b/routes/sub_section.route.js new file mode 100644 index 0000000..58adde9 --- /dev/null +++ b/routes/sub_section.route.js @@ -0,0 +1,17 @@ +const express = require('express'); +const PlantSubSectionController = require('../controllers/sub_section.controller'); +const verifyToken = require('../middleware/verifyToken'); +const verifyAccess = require('../middleware/verifyAccess'); + +const router = express.Router(); + +router.route('/') + .get(verifyToken.verifyAccessToken, PlantSubSectionController.getAll) + .post(verifyToken.verifyAccessToken, verifyAccess(), PlantSubSectionController.create); + +router.route('/:id') + .get(verifyToken.verifyAccessToken, PlantSubSectionController.getById) + .put(verifyToken.verifyAccessToken, verifyAccess(), PlantSubSectionController.update) + .delete(verifyToken.verifyAccessToken, verifyAccess(), PlantSubSectionController.delete); + +module.exports = router; \ No newline at end of file diff --git a/routes/tags.route.js b/routes/tags.route.js new file mode 100644 index 0000000..4160284 --- /dev/null +++ b/routes/tags.route.js @@ -0,0 +1,17 @@ +const express = require('express'); +const TagsController = require('../controllers/tags.controller'); +const verifyToken = require("../middleware/verifyToken") +const verifyAccess = require("../middleware/verifyAccess") + +const router = express.Router(); + +router.route("/") + .get(verifyToken.verifyAccessToken, TagsController.getAll) + .post(verifyToken.verifyAccessToken, verifyAccess(), TagsController.create); + +router.route("/:id") + .get(verifyToken.verifyAccessToken, TagsController.getById) + .put(verifyToken.verifyAccessToken, verifyAccess(), TagsController.update) + .delete(verifyToken.verifyAccessToken, verifyAccess(), TagsController.delete); + +module.exports = router; \ No newline at end of file diff --git a/routes/unit.route.js b/routes/unit.route.js new file mode 100644 index 0000000..24a3c77 --- /dev/null +++ b/routes/unit.route.js @@ -0,0 +1,17 @@ +const express = require('express'); +const UnitController = require('../controllers/unit.controller'); +const verifyToken = require('../middleware/verifyToken'); +const verifyAccess = require('../middleware/verifyAccess'); + +const router = express.Router(); + +router.route('/') + .get(verifyToken.verifyAccessToken, UnitController.getAll) + .post(verifyToken.verifyAccessToken, verifyAccess(), UnitController.create); + +router.route('/:id') + .get(verifyToken.verifyAccessToken, UnitController.getById) + .put(verifyToken.verifyAccessToken, verifyAccess(), UnitController.update) + .delete(verifyToken.verifyAccessToken, verifyAccess(), UnitController.delete); + +module.exports = router; diff --git a/routes/users.js b/routes/users.js deleted file mode 100644 index 3cbf210..0000000 --- a/routes/users.js +++ /dev/null @@ -1,33 +0,0 @@ -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/routes/users.route.js b/routes/users.route.js new file mode 100644 index 0000000..2b7abbb --- /dev/null +++ b/routes/users.route.js @@ -0,0 +1,26 @@ +const express = require('express'); +const UserController = require('../controllers/users.controller'); +const verifyToken = require('../middleware/verifyToken'); +const verifyAccess = require('../middleware/verifyAccess'); + +const router = express.Router(); + +router.route('/') + .get(verifyToken.verifyAccessToken, UserController.getAll) + .post(verifyToken.verifyAccessToken, verifyAccess(), UserController.create); + +router.route('/:id') + .get(verifyToken.verifyAccessToken, UserController.getById) + .put(verifyToken.verifyAccessToken, verifyAccess(), UserController.update) + .delete(verifyToken.verifyAccessToken, verifyAccess(), UserController.delete); + +router.route('/change-password/:id') + .put(verifyToken.verifyAccessToken, verifyAccess(), UserController.changePassword); + +router.route('/:id/approve') + .put(verifyToken.verifyAccessToken, verifyAccess(), UserController.approve); + +router.route('/:id/reject') + .put(verifyToken.verifyAccessToken, verifyAccess(), UserController.reject); + +module.exports = router; diff --git a/services/auth.service.js b/services/auth.service.js index 583d300..44b5948 100644 --- a/services/auth.service.js +++ b/services/auth.service.js @@ -1,77 +1,130 @@ -const bcrypt = require("bcrypt"); -const jwt = require("jsonwebtoken"); -const validateUser = require("../helpers/validateUser"); -const { ErrorHandler } = require("../helpers/error"); const { + getUserByUserEmailDb, + createUserDb, getUserByUsernameDb -} = require("../db/user.db"); -const { logger } = require("../utils/logger"); +} = require('../db/user.db'); +const { hashPassword, comparePassword } = require('../helpers/hashPassword'); +const { ErrorHandler } = require('../helpers/error'); +const JWTService = require('../utils/jwt'); class AuthService { - - async login(username, password, tenantId) { + // Register + static async register(data) { try { - // if (!validateUser(username, password)) { - // throw new ErrorHandler(403, "Invalid login"); - // } + const existingEmail = await getUserByUserEmailDb(data.user_email); + const existingUsername = await getUserByUsernameDb(data.user_name); - const user = await getUserByUsernameDb(username, tenantId); - console.log(user); - - if (!user) { - throw new ErrorHandler(403, "Username not found."); + if (existingUsername) { + throw new ErrorHandler(400, 'Username is already taken'); + } + if (existingEmail) { + throw new ErrorHandler(400, 'Email is already taken'); } - const isCorrectPassword = password === user.password - if (!isCorrectPassword) { - throw new ErrorHandler(403, "Username or password incorrect."); - } + const hashedPassword = await hashPassword(data.user_password); - const dataToken = { - tenant_id: tenantId, - user_id: user.user_id, - username, - fullname: user.full_name, - role_id: user.role_id - } + const userId = await createUserDb({ + user_fullname: data.user_fullname, + user_name: data.user_name, + user_email: data.user_email, + user_phone: data.user_phone, + user_password: hashedPassword, + is_sa: 0, + is_active: 1, + is_approve: 1, + }); - 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, - }, + const newUser = { + user_id: userId, + user_fullname: data.user_fullname, + user_name: data.user_name, + user_email: data.user_email, + user_phone: data.user_phone }; + + return { user: newUser }; } catch (error) { throw new ErrorHandler(error.statusCode, error.message); } } - async signToken(data) { + // Login + static async login(data) { try { - // console.log("signToken process.env.SECRET", process.env.SECRET) - return jwt.sign(data, process.env.SECRET, { expiresIn: "23h" }); + const { identifier, password, captcha, captchaText } = data; + + if (!captcha || captcha.toLowerCase() !== captchaText.toLowerCase()) { + throw new ErrorHandler(400, 'Invalid captcha'); + } + + let user; + if (identifier.includes('@')) { + user = await getUserByUserEmailDb(identifier); + } else { + user = await getUserByUsernameDb(identifier); + } + + if (!user) throw new ErrorHandler(401, 'Invalid credentials'); + + const passwordMatch = await comparePassword(password, user.user_password); + if (!passwordMatch) throw new ErrorHandler(401, 'Invalid credentials'); + + if (!user.is_active) throw new ErrorHandler(403, 'User is inactive'); + if (!user.is_approve) throw new ErrorHandler(403, 'Your account has not been approved by admin yet.'); + + const payload = { + user_id: user.user_id, + user_fullname: user.user_fullname, + user_name: user.user_name, + user_email: user.user_email, + user_phone: user.user_phone, + role_id: user.role_id, + role_name: user.role_name, + is_sa: user.is_sa + }; + + const tokens = JWTService.generateTokenPair(payload); + return { user: payload, tokens }; } catch (error) { - logger.error(error); - throw new ErrorHandler(500, "An error occurred"); + throw new ErrorHandler(error.statusCode, error.message); } } - async signRefreshToken(data) { + // Refresh Token + static async refreshToken(refreshToken) { try { - return jwt.sign(data, process.env.REFRESH_SECRET, { expiresIn: "23h" }); + let decoded; + try { + decoded = JWTService.verifyRefreshToken(refreshToken); + } catch (err) { + if (err.message.includes('expired')) { + throw new ErrorHandler(401, 'Refresh token expired'); + } + throw new ErrorHandler(401, 'Invalid refresh token'); + } + + const payload = { + user_id: decoded.user_id, + user_fullname: decoded.user_fullname, + user_name: decoded.user_name, + user_email: decoded.user_email, + user_phone: decoded.user_phone, + role_id: decoded.role_id, + role_name: decoded.role_name, + is_sa: decoded.is_sa + }; + + const accessToken = JWTService.generateAccessToken(payload); + + return { + accessToken, + tokenType: 'Bearer', + expiresIn: 900 + }; } catch (error) { - logger.error(error); - throw new ErrorHandler(500, error.message); + throw new ErrorHandler(error.statusCode, error.message); } } } -module.exports = new AuthService(); +module.exports = AuthService; diff --git a/services/device.service.js b/services/device.service.js new file mode 100644 index 0000000..978b336 --- /dev/null +++ b/services/device.service.js @@ -0,0 +1,88 @@ +const { + getAllDevicesDb, + getDeviceByIdDb, + createDeviceDb, + updateDeviceDb, + deleteDeviceDb +} = require('../db/device.db'); +const { ErrorHandler } = require('../helpers/error'); + +class DeviceService { + // Get all devices + static async getAllDevices(param) { + try { + const results = await getAllDevicesDb(param); + + results.data.map(element => { + }); + + return results + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Get device by ID + static async getDeviceById(id) { + try { + const result = await getDeviceByIdDb(id); + + if (result.length < 1) throw new ErrorHandler(404, 'Device not found'); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Create device + static async createDevice(data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const result = await createDeviceDb(data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Update device + static async updateDevice(id, data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const dataExist = await getDeviceByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Device not found'); + } + + const result = await updateDeviceDb(id, data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Soft delete device + static async deleteDevice(id, userId) { + try { + const dataExist = await getDeviceByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Device not found'); + } + + const result = await deleteDeviceDb(id, userId); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } +} + +module.exports = DeviceService; diff --git a/services/roles.service.js b/services/roles.service.js new file mode 100644 index 0000000..863ae80 --- /dev/null +++ b/services/roles.service.js @@ -0,0 +1,88 @@ +const { + getAllRolesDb, + getRolesByIdDb, + insertRolesDb, + updateRolesDb, + deleteRolesDb +} = require('../db/roles.db'); +const { ErrorHandler } = require('../helpers/error'); + +class RolesService { + // Get all Roles + static async getAllRoles(param) { + try { + const results = await getAllRolesDb(param); + + results.data.map(element => { + }); + + return results + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Get Roles by ID + static async getRolesById(id) { + try { + const result = await getRolesByIdDb(id); + + if (result.length < 1) throw new ErrorHandler(404, 'Roles not found'); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Create Roles + static async createRoles(data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const result = await insertRolesDb(data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Update Roles + static async updateRoles(id, data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const dataExist = await getRolesByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Roles not found'); + } + + const result = await updateRolesDb(id, data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Soft delete Roles + static async deleteRoles(id, userId) { + try { + const dataExist = await getRolesByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Roles not found'); + } + + const result = await deleteRolesDb(id, userId); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } +} + +module.exports = RolesService; diff --git a/services/schedule.service.js b/services/schedule.service.js new file mode 100644 index 0000000..ea33e1d --- /dev/null +++ b/services/schedule.service.js @@ -0,0 +1,88 @@ +const { + getAllScheduleDb, + getScheduleByIdDb, + insertScheduleDb, + updateScheduleDb, + deleteScheduleDb +} = require('../db/schedule.db'); +const { ErrorHandler } = require('../helpers/error'); + +class ScheduleService { + // Get all Schedule + static async getAllSchedule(param) { + try { + const results = await getAllScheduleDb(param); + + results.data.map(element => { + }); + + return results + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Get Schedule by ID + static async getScheduleById(id) { + try { + const result = await getScheduleByIdDb(id); + + if (result.length < 1) throw new ErrorHandler(404, 'Schedule not found'); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Create Schedule + static async insertScheduleDb(data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const result = await insertScheduleDb(data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Update Schedule + static async updateSchedule(id, data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const dataExist = await getScheduleByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Schedule not found'); + } + + const result = await updateScheduleDb(id, data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Soft delete Schedule + static async deleteSchedule(id, userId) { + try { + const dataExist = await getScheduleByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Schedule not found'); + } + + const result = await deleteScheduleDb(id, userId); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } +} + +module.exports = ScheduleService; diff --git a/services/shift.service.js b/services/shift.service.js new file mode 100644 index 0000000..8706771 --- /dev/null +++ b/services/shift.service.js @@ -0,0 +1,88 @@ +const { + getAllShiftDb, + getShiftByIdDb, + insertShiftDb, + updateShiftDb, + deleteShiftDb +} = require('../db/shift.db'); +const { ErrorHandler } = require('../helpers/error'); + +class ShiftService { + // Get all Shift + static async getAllShift(param) { + try { + const results = await getAllShiftDb(param); + + results.data.map(element => { + }); + + return results + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Get Shift by ID + static async getShiftById(id) { + try { + const result = await getShiftByIdDb(id); + + if (result.length < 1) throw new ErrorHandler(404, 'Shift not found'); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Create Shift + static async createShift(data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const result = await insertShiftDb(data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Update Shift + static async updateShift(id, data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const dataExist = await getShiftByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Shift not found'); + } + + const result = await updateShiftDb(id, data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Soft delete Shift + static async deleteShift(id, userId) { + try { + const dataExist = await getShiftByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Shift not found'); + } + + const result = await deleteShiftDb(id, userId); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } +} + +module.exports = ShiftService; diff --git a/services/status.service.js b/services/status.service.js new file mode 100644 index 0000000..cfc3113 --- /dev/null +++ b/services/status.service.js @@ -0,0 +1,92 @@ +const { + getAllStatusDb, + getStatusByIdDb, + createStatusDb, + updateStatusDb, + deleteStatusDb, + checkStatusNumberExistsDb +} = require('../db/status.db'); +const { ErrorHandler } = require('../helpers/error'); + +class StatusService { + // Get all status + static async getAllStatus(param) { + try { + const results = await getAllStatusDb(param); + + results.data.map(element => { + }); + + return results; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Get status by ID + static async getStatusById(id) { + try { + const result = await getStatusByIdDb(id); + + if (result.length < 1) throw new ErrorHandler(404, 'Status not found'); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + static async createStatus(data) { + try { + if (!data || typeof data !== 'object') data = {}; + + if (data.status_number) { + const exists = await checkStatusNumberExistsDb(data.status_number); + if (exists) throw new ErrorHandler(400, 'Status number already exists'); + } + + const result = await createStatusDb(data); + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode || 500, error.message); + } + } + + // Update status + static async updateStatus(id, data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const dataExist = await getStatusByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Status not found'); + } + + const result = await updateStatusDb(id, data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Soft delete status + static async deleteStatus(id, userId) { + try { + const dataExist = await getStatusByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Status not found'); + } + + const result = await deleteStatusDb(id, userId); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } +} + +module.exports = StatusService; diff --git a/services/sub_section.service.js b/services/sub_section.service.js new file mode 100644 index 0000000..8860bfd --- /dev/null +++ b/services/sub_section.service.js @@ -0,0 +1,87 @@ +const { + getAllSubSectionsDb, + getSubSectionByIdDb, + createSubSectionDb, + updateSubSectionDb, + deleteSubSectionDb +} = require('../db/sub_section.db'); +const { ErrorHandler } = require('../helpers/error'); + +class SubSectionService { + // Get all sub sections + static async getAll(param) { + try { + const results = await getAllSubSectionsDb(param); + + results.data.map(el => {}); + + return results; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Get sub section by ID + static async getById(id) { + try { + const result = await getSubSectionByIdDb(id); + + if (result.length < 1) throw new ErrorHandler(404, 'Sub section not found'); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Create sub section + static async create(data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const result = await createSubSectionDb(data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Update sub section + static async update(id, data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const dataExist = await getSubSectionByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Sub section not found'); + } + + const result = await updateSubSectionDb(id, data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Soft delete sub section + static async delete(id, userId) { + try { + const dataExist = await getSubSectionByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Sub section not found'); + } + + const result = await deleteSubSectionDb(id, userId); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } +} + +module.exports = SubSectionService; diff --git a/services/tags.service.js b/services/tags.service.js new file mode 100644 index 0000000..9f180ae --- /dev/null +++ b/services/tags.service.js @@ -0,0 +1,89 @@ +const { + getAllTagsDb, + getTagsByIdDb, + createTagsDb, + updateTagsDb, + deleteTagsDb + } = require('../db/tags.db'); + const { ErrorHandler } = require('../helpers/error'); + + class TagsService { + // Get all devices + static async getAllTags(param) { + try { + const results = await getAllTagsDb(param); + + results.data.map(element => { + }); + + return results + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Get device by ID + static async getTagByID(id) { + try { + const result = await getTagsByIdDb(id); + + if (result.length < 1) throw new ErrorHandler(404, 'Tags not found'); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Create device + static async createTags(data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const result = await createTagsDb(data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Update device + static async updateTags(id, data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const dataExist = await getTagsByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Tags not found'); + } + + const result = await updateTagsDb(id, data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Soft delete device + static async deleteTags(id, userId) { + try { + const dataExist = await getTagsByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Tags not found'); + } + + const result = await deleteTagsDb(id, userId); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + } + + module.exports = TagsService; + \ No newline at end of file diff --git a/services/unit.service.js b/services/unit.service.js new file mode 100644 index 0000000..c6d6624 --- /dev/null +++ b/services/unit.service.js @@ -0,0 +1,88 @@ +const { + getAllUnitsDb, + getUnitByIdDb, + createUnitDb, + updateUnitDb, + deleteUnitDb +} = require('../db/unit.db'); +const { ErrorHandler } = require('../helpers/error'); + +class UnitService { + // Get all units + static async getAllUnits(param) { + try { + const results = await getAllUnitsDb(param); + + results.data.map(element => { + }); + + return results; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Get unit by ID + static async getUnitById(id) { + try { + const result = await getUnitByIdDb(id); + + if (result.length < 1) throw new ErrorHandler(404, 'Unit not found'); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Create unit + static async createUnit(data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const result = await createUnitDb(data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Update unit + static async updateUnit(id, data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const dataExist = await getUnitByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Unit not found'); + } + + const result = await updateUnitDb(id, data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Soft delete unit + static async deleteUnit(id, userId) { + try { + const dataExist = await getUnitByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Unit not found'); + } + + const result = await deleteUnitDb(id, userId); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } +} + +module.exports = UnitService; \ No newline at end of file diff --git a/services/user.service.js b/services/user.service.js index 2e95fda..d2688b3 100644 --- a/services/user.service.js +++ b/services/user.service.js @@ -1,124 +1,188 @@ const { - createUserDb, - changeUserPasswordDb, - getUserByIdDb, - updateUserDb, - deleteUserDb, getAllUsersDb, + getUserByIdDb, + getUserByUserEmailDb, 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" - } -]; + createUserDb, + updateUserDb, + approveUserDb, + rejectUserDb, + deleteUserDb, + changeUserPasswordDb +} = require('../db/user.db'); +const { hashPassword } = require('../helpers/hashPassword'); +const { ErrorHandler } = require('../helpers/error'); class UserService { - getAllStatusUsers = async () => { - try { - return statusName; - } catch (error) { - throw new ErrorHandler(error.statusCode, error.message); - } - }; - - getAllUsers = async (param) => { + // Get all users + static async getAllUsers(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 + return results; } catch (error) { throw new ErrorHandler(error.statusCode, error.message); } - }; + } - createUser = async (param) => { + // Get user by ID + static async getUserById(id) { try { - const userByUsername = await getUserByUsernameDb(param.userName, param.tenantID); + const result = await getUserByIdDb(id); - if (userByUsername) { - throw new ErrorHandler(401, "username taken already"); + if (!result) throw new ErrorHandler(404, 'User not found'); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Create user + static async createUser(data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const creatorId = data.userId || null; + + // cek duplikasi username & email + const [existingUsername, existingEmail] = await Promise.all([ + getUserByUsernameDb(data.user_name), + getUserByUserEmailDb(data.user_email) + ]); + + if (existingUsername) throw new ErrorHandler(400, 'Username is already taken'); + if (existingEmail) throw new ErrorHandler(400, 'Email is already taken'); + + // hash password + const hashedPassword = await hashPassword(data.user_password); + + // siapkan data untuk insert + const userData = { + ...data, + user_password: hashedPassword, + is_approve: 2, + approved_by: creatorId, + created_by: creatorId, + updated_by: creatorId, + is_sa: 0, + is_active: 1 + }; + + delete userData.userId; + + const result = await createUserDb(userData); + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode || 500, error.message); + } + } + + // Update user + static async updateUser(id, data) { + try { + if (!data || typeof data !== 'object') data = {}; + + const existingEmail = await getUserByUserEmailDb(data.user_email); + const existingUsername = await getUserByUsernameDb(data.user_name); + + if (existingUsername) { + throw new ErrorHandler(400, 'Username is already taken'); + } + if (existingEmail) { + throw new ErrorHandler(400, 'Email is already taken') } - return await createUserDb(param); + const userExist = await getUserByIdDb(id); + if (!userExist) throw new ErrorHandler(404, 'User not found'); + + const result = await updateUserDb(id, data); + return result; } catch (error) { throw new ErrorHandler(error.statusCode, error.message); } - }; + } - getUserById = async (id) => { + // Approve user + static async approveUser(userId, approverId) { 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 (!userId) { + throw new ErrorHandler(400, 'User ID is required'); } - if (Object.keys(errors).length > 0) { - throw new ErrorHandler(403, errors); + const existingUser = await getUserByIdDb(userId); + if (!existingUser) { + throw new ErrorHandler(404, 'User not found'); } - return await updateUserDb(param); - } catch (error) { - throw new ErrorHandler(error.statusCode, error.message); - } - }; + if (existingUser.is_approve === 2) { + throw new ErrorHandler(400, 'User is already approved'); + } - deleteUser = async (id, userID) => { - try { - return await deleteUserDb(id, userID); - } catch (error) { - throw new ErrorHandler(error.statusCode, error.message); - } - }; + if (existingUser.is_approve === 0) { + throw new ErrorHandler(400, 'User is already rejected'); + } - getAllRoles = async (tenantID) => { + const updatedUser = await approveUserDb(userId, approverId); + return updatedUser; + } catch (error) { + throw new ErrorHandler(error.statusCode || 500, error.message); + } + } + + // Reject user + static async rejectUser(userId, approverId) { try { - return await getAllRoleDb(tenantID); + if (!userId) { + throw new ErrorHandler(400, 'User ID is required'); + } + + const existingUser = await getUserByIdDb(userId); + if (!existingUser) { + throw new ErrorHandler(404, 'User not found'); + } + + if (existingUser.is_approve === 2) { + throw new ErrorHandler(400, 'User is already approved'); + } + + if (existingUser.is_approve === 0) { + throw new ErrorHandler(400, 'User is already rejected'); + } + + const updatedUser = await rejectUserDb(userId, approverId); + return updatedUser; + } catch (error) { + throw new ErrorHandler(error.statusCode || 500, error.message); + } + } + + // Soft delete user + static async deleteUser(id, userId) { + try { + const userExist = await getUserByIdDb(id); + if (!userExist) throw new ErrorHandler(404, 'User not found'); + + const result = await deleteUserDb(id, userId); + return result; } catch (error) { throw new ErrorHandler(error.statusCode, error.message); } - }; + } + + // Change password + static async changeUserPassword(id, newPassword) { + try { + const userExist = await getUserByIdDb(id); + if (!userExist) throw new ErrorHandler(404, 'User not found'); + + const hashedPassword = await hashPassword(newPassword); + const result = await changeUserPasswordDb(id, hashedPassword); + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } } -module.exports = new UserService(); +module.exports = UserService; diff --git a/utils/captcha.js b/utils/captcha.js new file mode 100644 index 0000000..d14d31c --- /dev/null +++ b/utils/captcha.js @@ -0,0 +1,8 @@ +const svgCaptcha = require('svg-captcha'); + +function createCaptcha() { + const captcha = svgCaptcha.create({ size: 5, noise: 7, color: true }); + return { svg: captcha.data, text: captcha.text }; +} + +module.exports = { createCaptcha }; \ No newline at end of file diff --git a/utils/date.js b/utils/date.js new file mode 100644 index 0000000..400d9ce --- /dev/null +++ b/utils/date.js @@ -0,0 +1,8 @@ +module.exports = { + formattedDate: (timestamp) => { + let date = new Date(timestamp); + let options = { day: "numeric", month: "long", year: "numeric" }; + let formattedDate = date.toISOString("id-ID", options); + return formattedDate; + }, + }; \ No newline at end of file diff --git a/utils/jwt.js b/utils/jwt.js new file mode 100644 index 0000000..4d5aaa4 --- /dev/null +++ b/utils/jwt.js @@ -0,0 +1,73 @@ +const jwt = require('jsonwebtoken'); +const crypto = require('crypto'); + +const tokenSettings = { + access: { + expiresIn: '15m', + type: 'access', + secret: process.env.SECRET + }, + refresh: { + expiresIn: '7d', + type: 'refresh', + secret: process.env.REFRESH_SECRET + } +}; + +function generateTokenId() { + return crypto.randomBytes(32).toString('hex'); +} + +function generateToken(payload, type) { + const settings = tokenSettings[type]; + if (!settings) throw new Error(`Invalid token type: ${type}`); + + const tokenPayload = { ...payload, type: settings.type }; + + return jwt.sign(tokenPayload, settings.secret, { + expiresIn: settings.expiresIn, + jwtid: generateTokenId() + }); +} + +function verifyTokenType(token, type) { + const settings = tokenSettings[type]; + const decoded = jwt.verify(token, settings.secret); + if (decoded.type !== type) throw new Error('Invalid token type'); + return decoded; +} + +function generateAccessToken(payload) { + return generateToken(payload, 'access'); +} + +function generateRefreshToken(payload) { + return generateToken(payload, 'refresh'); +} + +function verifyToken(token) { + return verifyTokenType(token, 'access'); +} + +function verifyRefreshToken(token) { + return verifyTokenType(token, 'refresh'); +} + +function generateTokenPair(payload) { + const accessToken = generateAccessToken(payload); + const refreshToken = generateRefreshToken(payload); + + return { + accessToken, + refreshToken, + tokenType: 'Bearer', + }; +} + +module.exports = { + generateAccessToken, + generateRefreshToken, + verifyToken, + verifyRefreshToken, + generateTokenPair, +}; diff --git a/validate/auth.schema.js b/validate/auth.schema.js new file mode 100644 index 0000000..bba7312 --- /dev/null +++ b/validate/auth.schema.js @@ -0,0 +1,40 @@ +const Joi = require("joi"); + +// ======================== +// Auth Validation +// ======================== +const registerSchema = Joi.object({ + user_fullname: Joi.string().min(3).max(100).required(), + user_name: Joi.string().alphanum().min(3).max(50).required(), + user_email: Joi.string().email().required(), + user_phone: Joi.string() + .pattern(/^(?:\+62|0)8\d{7,10}$/) + .required() + .messages({ + 'string.pattern.base': + 'Phone number must be a valid Indonesian number in format +628XXXXXXXXX' + }), + user_password: Joi.string() + .min(8) + .pattern(/[A-Z]/, 'uppercase letter') + .pattern(/[a-z]/, 'lowercase letter') + .pattern(/\d/, 'number') + .pattern(/[!@#$%^&*(),.?":{}|<>]/, 'special character') + .required() + .messages({ + 'string.min': 'Password must be at least 8 characters long', + 'string.pattern.name': 'Password must contain at least one {#name}' + }) +}); + +const loginSchema = Joi.object({ + identifier: Joi.string().required(), + password: Joi.string().required(), + captcha: Joi.string().required(), + captchaText: Joi.string().required() +}); + +module.exports = { + registerSchema, + loginSchema, +}; \ No newline at end of file diff --git a/validate/device.schema.js b/validate/device.schema.js new file mode 100644 index 0000000..4d1f65e --- /dev/null +++ b/validate/device.schema.js @@ -0,0 +1,36 @@ +// ======================== +// Device Validation + +const Joi = require("joi"); + +// ======================== +const insertDeviceSchema = Joi.object({ + device_name: Joi.string().max(100).required(), + is_active: Joi.boolean().required(), + device_location: Joi.string().max(100).required(), + device_description: Joi.string().required(), + ip_address: Joi.string() + .ip({ version: ['ipv4', 'ipv6'] }) + .required() + .messages({ + 'string.ip': 'IP address must be a valid IPv4 or IPv6 address' + }) +}); + +const updateDeviceSchema = Joi.object({ + device_name: Joi.string().max(100), + is_active: Joi.boolean(), + device_location: Joi.string().max(100), + device_description: Joi.string(), + ip_address: Joi.string() + .ip({ version: ['ipv4', 'ipv6'] }) + .messages({ + 'string.ip': 'IP address must be a valid IPv4 or IPv6 address' + }) +}).min(1); + + +// ✅ Export dengan CommonJS +module.exports = { + insertDeviceSchema, updateDeviceSchema +}; \ No newline at end of file diff --git a/validate/roles.schema.js b/validate/roles.schema.js new file mode 100644 index 0000000..d23436a --- /dev/null +++ b/validate/roles.schema.js @@ -0,0 +1,25 @@ +// ======================== +// Device Validation + +const Joi = require("joi"); + +// ======================== +const insertRolesSchema = Joi.object({ + role_name: Joi.string().max(100).required(), + role_level: Joi.number().required(), + role_description: Joi.string().max(100).required(), + is_active: Joi.boolean().required(), +}); + +const updateRolesSchema = Joi.object({ + role_name: Joi.string().max(100).optional(), + role_level: Joi.number().optional(), + role_description: Joi.string().max(100).optional(), + is_active: Joi.boolean().optional() +}).min(1); + + +// ✅ Export dengan CommonJS +module.exports = { + insertRolesSchema, updateRolesSchema +}; \ No newline at end of file diff --git a/validate/schedule.schema.js b/validate/schedule.schema.js new file mode 100644 index 0000000..ee0e37d --- /dev/null +++ b/validate/schedule.schema.js @@ -0,0 +1,29 @@ +// ======================== +// Schedule Validation + +const Joi = require("joi"); + +const datePattern = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/; + +const insertScheduleSchema = Joi.object({ + schedule_date: Joi.string().pattern(datePattern).required().messages({ + "string.pattern.base": "schedule_date harus dalam format YYYY-MM-DD", + "any.required": "schedule_date wajib diisi", + }), + is_active: Joi.boolean().required(), + shift_id: Joi.number(), + next_day: Joi.number().required(), +}); + +const updateScheduleSchema = Joi.object({ + schedule_date: Joi.string().pattern(datePattern).messages({ + "string.pattern.base": "schedule_date harus dalam format YYYY-MM-DD", + }), + is_active: Joi.boolean(), + shift_id: Joi.number(), +}).min(1); + +module.exports = { + insertScheduleSchema, + updateScheduleSchema, +}; diff --git a/validate/shift.schema.js b/validate/shift.schema.js new file mode 100644 index 0000000..4751778 --- /dev/null +++ b/validate/shift.schema.js @@ -0,0 +1,44 @@ +// ======================== +// Device Validation + +const Joi = require("joi"); + +// ======================== +const timePattern = /^([01]\d|2[0-3]):([0-5]\d)(:[0-5]\d)?$/; + +const insertShiftSchema = Joi.object({ + shift_name: Joi.string().max(100).required(), + is_active:Joi.boolean().required(), + start_time: Joi.string() + .pattern(timePattern) + .required() + .messages({ + "string.pattern.base": "start_time harus dalam format HH:mm atau HH:mm:ss", + }), + end_time: Joi.string() + .pattern(timePattern) + .required() + .messages({ + "string.pattern.base": "end_time harus dalam format HH:mm atau HH:mm:ss", + }), +}); + +const updateShiftSchema = Joi.object({ + shift_name: Joi.string().max(100).optional(), + is_active:Joi.boolean().optional(), + start_time: Joi.string() + .pattern(timePattern) + .messages({ + "string.pattern.base": "start_time harus dalam format HH:mm atau HH:mm:ss", + }).optional(), + end_time: Joi.string() + .pattern(timePattern) + .messages({ + "string.pattern.base": "end_time harus dalam format HH:mm atau HH:mm:ss", + }), +}).min(1); + +module.exports = { + insertShiftSchema, + updateShiftSchema, +}; diff --git a/validate/status.schema.js b/validate/status.schema.js new file mode 100644 index 0000000..9b79b65 --- /dev/null +++ b/validate/status.schema.js @@ -0,0 +1,26 @@ +const Joi = require("joi"); + +// ======================== +// Status Validation +// ======================== +const insertStatusSchema = Joi.object({ + status_number: Joi.number().integer().required(), + status_name: Joi.string().max(200).required(), + status_color: Joi.string().max(200).required(), + status_description: Joi.string().allow('', null), + is_active: Joi.boolean().required(), +}); + +const updateStatusSchema = Joi.object({ + status_number: Joi.number().integer().optional(), + status_name: Joi.string().max(200).optional(), + status_color: Joi.string().max(200).optional(), + status_description: Joi.string().allow('', null).optional(), + is_active: Joi.boolean().optional() +}).min(1); + +// ✅ Export dengan CommonJS +module.exports = { + insertStatusSchema, + updateStatusSchema +}; diff --git a/validate/sub_section.schema.js b/validate/sub_section.schema.js new file mode 100644 index 0000000..fe1e570 --- /dev/null +++ b/validate/sub_section.schema.js @@ -0,0 +1,33 @@ +const Joi = require("joi"); + +// ======================== +// Plant Sub Section Validation +// ======================== +const insertSubSectionSchema = Joi.object({ + sub_section_name: Joi.string() + .max(200) + .required() + .messages({ + "string.base": "Sub section name must be a string", + "string.max": "Sub section name cannot exceed 200 characters", + "any.required": "Sub section name is required" + }).required(), + is_active: Joi.boolean().required(), +}); + +const updateSubSectionSchema = Joi.object({ + sub_section_name: Joi.string() + .max(200) + .messages({ + "string.base": "Sub section name must be a string", + "string.max": "Sub section name cannot exceed 200 characters", + }).optional(), + is_active: Joi.boolean().optional(), +}).min(1).messages({ + "object.min": "At least one field must be provided to update", +}); + +module.exports = { + insertSubSectionSchema, + updateSubSectionSchema +}; diff --git a/validate/tags.schema.js b/validate/tags.schema.js new file mode 100644 index 0000000..29563f1 --- /dev/null +++ b/validate/tags.schema.js @@ -0,0 +1,33 @@ +// ======================== +// Device Validation + +const Joi = require("joi"); + +// ======================== +const insertTagsSchema = Joi.object({ + device_id: Joi.number().optional(), + tag_name: Joi.string().max(200).required(), + tag_number: Joi.number().required(), + is_active: Joi.boolean().required(), + data_type: Joi.string().max(50).required(), + unit: Joi.string().max(50).required(), + sub_section_id: Joi.number().optional(), + is_alarm: Joi.boolean().required() +}); + +const updateTagsSchema = Joi.object({ + device_id: Joi.number(), + tag_name: Joi.string().max(200), + tag_number: Joi.number(), + is_active: Joi.boolean(), + data_type: Joi.string().max(50), + unit: Joi.string().max(50), + is_alarm: Joi.boolean().optional(), + sub_section_id: Joi.number().optional(), +}).min(1); + +// ✅ Export dengan CommonJS +module.exports = { + insertTagsSchema, + updateTagsSchema, +}; diff --git a/validate/unit.schema.js b/validate/unit.schema.js new file mode 100644 index 0000000..058e91c --- /dev/null +++ b/validate/unit.schema.js @@ -0,0 +1,21 @@ +const Joi = require("joi"); + +// ======================== +// Unit Validation +// ======================== +const insertUnitSchema = Joi.object({ + unit_name: Joi.string().max(100).required(), + tag_id: Joi.number().integer().optional(), + is_active: Joi.boolean().required(), +}); + +const updateUnitSchema = Joi.object({ + unit_name: Joi.string().max(100).optional(), + tag_id: Joi.number().integer().optional(), + is_active: Joi.boolean().optional() +}).min(1); + +module.exports = { + insertUnitSchema, + updateUnitSchema +}; diff --git a/validate/user.schema.js b/validate/user.schema.js new file mode 100644 index 0000000..19b4be9 --- /dev/null +++ b/validate/user.schema.js @@ -0,0 +1,61 @@ +const Joi = require("joi"); + +// ======================== +// Users Validation +// ======================== +const userSchema = Joi.object({ + user_fullname: Joi.string().min(3).max(100).required(), + user_name: Joi.string().alphanum().min(3).max(50).required(), + user_email: Joi.string().email().required(), + user_phone: Joi.string() + .pattern(/^(?:\+62|0)8\d{7,10}$/) + .required() + .messages({ + 'string.pattern.base': + 'Phone number must be a valid Indonesian number in format +628XXXXXXXXX' + }), + user_password: Joi.string() + .min(8) + .pattern(/[A-Z]/, 'uppercase letter') + .pattern(/[a-z]/, 'lowercase letter') + .pattern(/\d/, 'number') + .pattern(/[!@#$%^&*(),.?":{}|<>]/, 'special character') + .required() + .messages({ + 'string.min': 'Password must be at least 8 characters long', + 'string.pattern.name': 'Password must contain at least one {#name}' + }), + role_id: Joi.number().integer().min(1) +}); + +const updateUserSchema = Joi.object({ + user_fullname: Joi.string().min(3).max(100).optional(), + user_name: Joi.string().alphanum().min(3).max(50).optional(), + user_email: Joi.string().email().optional(), + user_phone: Joi.string() + .pattern(/^(?:\+62|0)8\d{7,10}$/) + .message('Phone number must be a valid Indonesian number in format +628XXXXXXXXX') + .optional(), + role_id: Joi.number().integer().min(1).optional(), + is_active: Joi.boolean().optional() +}).min(1); + +const newPasswordSchema = Joi.object({ + new_password: Joi.string() + .min(8) + .pattern(/[A-Z]/, 'uppercase letter') + .pattern(/[a-z]/, 'lowercase letter') + .pattern(/\d/, 'number') + .pattern(/[!@#$%^&*(),.?":{}|<>]/, 'special character') + .required() + .messages({ + 'string.min': 'Password must be at least 8 characters long', + 'string.pattern.name': 'Password must contain at least one {#name}' + }) +}); + +module.exports = { + userSchema, + newPasswordSchema, + updateUserSchema +}; \ No newline at end of file