commit 96d206d892e40b35e2a83582f27b38d339a4b8a6 Author: Tirta Aditya Date: Tue Oct 14 14:08:11 2025 +0700 created API For Aplication Absensi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e338d1f --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# Node modules +node_modules/ + +# Environment variables +.env +.env.* + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# OS +.DS_Store +Thumbs.db + +# Editor/IDE +.vscode/ +.idea/ +*.sublime-workspace +*.sublime-project + +# Coverage/testing +coverage/ +.nyc_output/ +coverage-final.json + +# Build/output folders +dist/ +build/ +tmp/ +temp/ + +# Optional: PM2 process logs +pids/ +*.pid +*.seed + +# Optional: TypeScript cache +*.tsbuildinfo + +# Optional: dotenv-expand cache +.env.local + +# Ignore uploaded files +/public/uploads/* +!/public/uploads/.gitkeep diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..8eabdb3 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "models"] + path = models + url = git@git.oxinos.io:max/models.git diff --git a/app/config/config.json b/app/config/config.json new file mode 100644 index 0000000..06ac2b7 --- /dev/null +++ b/app/config/config.json @@ -0,0 +1,13 @@ +{ + "development": { + "username": "oxinos", + "password": "Z9jUA33GwblqN1Vk06", + "database": "absens", + "host": "31.97.110.178", + "dialect": "mysql" + } + } + + + + \ No newline at end of file diff --git a/app/config/db.config.js b/app/config/db.config.js new file mode 100644 index 0000000..cb21caa --- /dev/null +++ b/app/config/db.config.js @@ -0,0 +1,13 @@ +require('dotenv').config(); +const { Sequelize } = require('sequelize'); + +const config = { + username: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE, + host: process.env.DB_HOST, + port : process.env.DB_PORT, + dialect: process.env.DB_CONNECTION || 'mysql' +}; + +module.exports = config; \ No newline at end of file diff --git a/app/config/mail.config.js b/app/config/mail.config.js new file mode 100644 index 0000000..5d9dfe0 --- /dev/null +++ b/app/config/mail.config.js @@ -0,0 +1,34 @@ +const nodemailer = require('nodemailer') +require('dotenv').config() + +const transporter = nodemailer.createTransport({ + host: process.env.MAIL_HOST, + port: process.env.MAIL_PORT, + secure: false, // true for 465, false for other ports + auth: { + user: process.env.MAIL_USERNAME, // Perbaikan di sini + pass: process.env.MAIL_PASSWORD // Perbaikan di sini + }, + tls: { + rejectUnauthorized: false // Untuk development, hati-hati di production + } +}) + +const sendMail = async ({to, subject, html}) => { // Diubah ke object destructuring + try { + await transporter.sendMail({ + from: `"${process.env.MAIL_FROM_NAME}" <${process.env.MAIL_FROM_ADDRESS}>`, // Ditambahkan > + to, + subject, + html + }) + console.log('Email sent successfully') + } catch (error) { + console.error('Error sending email:', error) + throw error + } +} + +module.exports = { + sendMail +} \ No newline at end of file diff --git a/app/config/rabbitmq.config.js b/app/config/rabbitmq.config.js new file mode 100644 index 0000000..61d9165 --- /dev/null +++ b/app/config/rabbitmq.config.js @@ -0,0 +1,42 @@ +require('dotenv').config(); + +const parseNumber = (value, fallback) => { + const parsed = Number(value); + return Number.isNaN(parsed) ? fallback : parsed; +}; + +const retryDelayMs = parseNumber(process.env.RABBITMQ_RETRY_DELAY_MS, 6000); + +const exchangeName = process.env.RABBITMQ_EXCHANGE || 'ai.story.exchange'; + +const config = { + url: process.env.RABBITMQ_URL || 'amqp://guest:guest@localhost:5672', + prefetch: parseNumber(process.env.RABBITMQ_PREFETCH, 1), + retry: { + maxAttempts: parseNumber(process.env.RABBITMQ_MAX_RETRY_ATTEMPTS, 5), + delayMs: retryDelayMs, + }, + exchange: { + name: exchangeName, + type: process.env.RABBITMQ_EXCHANGE_TYPE || 'direct', + routingKeys: { + story: process.env.RABBITMQ_STORY_ROUTING_KEY || 'story.generate', + retry: process.env.RABBITMQ_RETRY_ROUTING_KEY || 'story.generate.retry', + deadLetter: process.env.RABBITMQ_DEAD_LETTER_ROUTING_KEY || 'story.generate.dead', + }, + }, + queues: { + storyGeneration: { + name: process.env.RABBITMQ_QUEUE || 'ai.story.queue', + }, + retry: { + name: process.env.RABBITMQ_RETRY_QUEUE || 'ai.story.retry.queue', + messageTtl: retryDelayMs, + }, + deadLetter: { + name: process.env.RABBITMQ_DEAD_LETTER_QUEUE || 'ai.story.dlq', + }, + }, +}; + +module.exports = config; diff --git a/app/constants/api_constans.js b/app/constants/api_constans.js new file mode 100644 index 0000000..05d8b1f --- /dev/null +++ b/app/constants/api_constans.js @@ -0,0 +1,87 @@ +// 13XX Session Errors +const API_URL = "/api/"; + +const errorCodes = [ + { code: 1001, httpCode: 500, message: "Invalid request format" }, + { code: 1002, httpCode: 500, message: "Request data missing" }, + { code: 1003, httpCode: 500, message: "Invalid request parameter" }, + { code: 1004, httpCode: 500, message: "Request parameter missing" }, + { code: 1005, httpCode: 500, message: "Request limit exceeded" }, + { code: 1101, httpCode: 500, message: "Authentication required" }, + { code: 1102, httpCode: 500, message: "Invalid authentication credentials" }, + { code: 1103, httpCode: 500, message: "Authentication token expired" }, + { code: 1104, httpCode: 500, message: "Authentication token invalid" }, + { code: 1105, httpCode: 500, message: "Invalid username or password" }, + { code: 1201, httpCode: 500, message: "Resource not found" }, + { code: 1202, httpCode: 500, message: "Resource already exists" }, + { code: 1203, httpCode: 500, message: "Operation not permitted" }, + { code: 1204, httpCode: 500, message: "Access denied" }, + { code: 401, httpCode: 500, message: "Unauthorized access, please login" }, + { code: 1301, httpCode: 500, message: "Invalid input data" }, + { code: 1302, httpCode: 500, message: "Invalid data format" }, + { code: 1303, httpCode: 500, message: "Data not found" }, + { code: 1304, httpCode: 500, message: "Data already exists" }, + { code: 1305, httpCode: 500, message: "Invalid token, please login again" }, + { + code: "1306", + httpCode: 500, + message: + "The provided token is invalid or has expired. Please request a new token", + }, + { code: 1307, httpCode: 500, message: "Email not found" }, + { + code: 1308, + httpCode: 500, + message: "Invalid email or password. Please try again", + }, + { + code: 1309, + httpCode: 500, + message: "Invalid phone or password. Please try again.", + }, + { code: 1310, httpCode: 500, message: "Invalid username format" }, + { code: 1311, httpCode: 500, message: "Username already taken" }, + { code: 1312, httpCode: 500, message: "Invalid password format" }, + { code: 1313, httpCode: 500, message: "Password too weak" }, + { code: 1314, httpCode: 500, message: "Password reset failed" }, + { code: 1315, httpCode: 500, message: "Invalid verification code" }, + { code: 1316, httpCode: 500, message: "Verification code expired" }, + { code: 1317, httpCode: 500, message: "Phone number not found" }, + { code: 1318, httpCode: 500, message: "Phone number already registered" }, + { code: 1319, httpCode: 500, message: "Invalid phone number format" }, + { code: 1320, httpCode: 500, message: "Phone number verification failed" }, + { code: 1321, httpCode: 500, message: "Email already registered" }, + { code: 1322, httpCode: 500, message: "Invalid email format" }, + { code: 1323, httpCode: 500, message: "Email verification failed" }, + { code: 1401, httpCode: 500, message: "Unauthorized" }, + { code: 1402, httpCode: 500, message: "Service unavailable" }, + { code: 1403, httpCode: 500, message: "Server overloaded" }, + { code: 1404, httpCode: 500, message: "Server timeout" }, + { code: 1405, httpCode: 500, message: "Request timeout" }, + { code: 1406, httpCode: 500, message: "Request canceled" }, + { code: 1407, httpCode: 500, message: "Server not responding" }, + { code: 1408, httpCode: 500, message: "API Key Is missing" }, + { code: 1501, httpCode: 500, message: "Database error" }, + { code: 1502, httpCode: 500, message: "Transaction failed" }, + { code: 1503, httpCode: 500, message: "Data inconsistency" }, + { code: 1504, httpCode: 500, message: "Lock wait timeout exceeded" }, + { code: 1505, httpCode: 500, message: "Deadlock detected" }, + { code: 500, httpCode: 500, message: "Internal Server Error" }, +]; + +function findErrorByCode(errorCode) { + const foundError = errorCodes.find((error) => error.code === errorCode); + console.log(`ERROR ${foundError}`); + return foundError != null + ? foundError + : { code: 500, message: "Internal server error", httpCode: 500 }; +} + +const getErrorMessage = (code) => { + const error = findErrorByCode(code); + return error; +}; + +module.exports = { + getErrorMessage, +}; diff --git a/app/core/controllers/auth.controller.js b/app/core/controllers/auth.controller.js new file mode 100644 index 0000000..fcf8975 --- /dev/null +++ b/app/core/controllers/auth.controller.js @@ -0,0 +1,49 @@ +const service = require('../services/auth.service') +const servicegoogle = require('../services/authGoogle.service') + +exports.signIn = async (req, res) => { + const response = await service.signIn(req,res) + return response +} + +exports.sendOtp = async (req, res) => { + const response = await service.sendOtp(req, res) + return response +} + + exports.checkOtp = async (req, res) => { + const response = await service.checkOtp(req, res) + return response +} + + exports.signUp = async (req, res) => { + const response = await service.signUp(req, res) + return response +} + +exports.getUserlogin = async (req, res) => { + const token = req.headers.authorization?.split(" ")[1] + const response = await service.getUserLogin(token, res) + return response +} + +exports.forgotPassword = async(req, res) => { + var body = req.body + const response = await service.forgotPassword(body, res) + return response + } + + exports.resetPassword = async(req, res, token) => { + var body = req.body + var token = token + const response = await service.resetPassword(body, token, res) + return response + } +exports.loginWithGoogle = async (req, res) => { + const response = await servicegoogle.loginWithGoogle(req, res) + return response +} + + + + diff --git a/app/core/models/index.js b/app/core/models/index.js new file mode 100644 index 0000000..e69de29 diff --git a/app/core/resources/user.resource.js b/app/core/resources/user.resource.js new file mode 100644 index 0000000..116e148 --- /dev/null +++ b/app/core/resources/user.resource.js @@ -0,0 +1,17 @@ +class UserResource { + constructor(user) { + this.id = user?.id ?? null + this.name = user?.name ?? null + this.email = user?.email ?? null + this.phone = user?.phone ?? null + //this.role = user?.role ?? null + this.via = user?.login_via ?? null + this.google_id = user?.google_id ?? null + this.last_login = user?.last_login ?? null + this.created_at = user?.created_at ?? null + this.updated_at = user?.updated_at ?? null + } + +} + +module.exports = UserResource \ No newline at end of file diff --git a/app/core/routes/api.route.js b/app/core/routes/api.route.js new file mode 100644 index 0000000..e01fe80 --- /dev/null +++ b/app/core/routes/api.route.js @@ -0,0 +1,19 @@ +const express = require('express') + +const router = express.Router() +const authRouter = require('./auth.route') +const ProfileRouter = require('../../modules/profile/routes/profile.route') +const AbsensRouter = require('../../modules/absensi/routes/absensi.route') +const BranchRouter = require('../../modules/branch/routes/branch.route') + +router.use('/auth', authRouter) +router.use('/profiles', ProfileRouter) +router.use('/attedances', AbsensRouter) +router.use('/branches', BranchRouter) + + + + + + +module.exports = router diff --git a/app/core/routes/auth.route.js b/app/core/routes/auth.route.js new file mode 100644 index 0000000..8c81d62 --- /dev/null +++ b/app/core/routes/auth.route.js @@ -0,0 +1,41 @@ +const express = require('express') +const router = express.Router() +const controller = require('../controllers/auth.controller') +const authentication = require("../../middlewares/authentication.js"); +const apiKey = require("../../middlewares/apiKey.js"); + +router.post('/signIn', apiKey, (req, res) => { + controller.signIn(req, res) +}) + +router.post('/signUp', (req, res) => { + controller.signUp(req, res) +}) + +router.get('/me', authentication,apiKey, (req, res) => { + controller.getUserlogin(req,res) +}) + +router.post('/send-otp',apiKey, (req, res) => { + controller.sendOtp(req, res) +}) + +router.post('/check-otp', apiKey, (req, res) => { + controller.checkOtp(req, res) +}) + +router.post("/forgot-password", apiKey, function name(req, res) { + controller.forgotPassword(req, res); + }); + + router.post("/reset/:token", apiKey, function name(req, res) { + const { token } = req.params; + controller.resetPassword(req, res, token); + }); + + + +module.exports = router + + + diff --git a/app/core/routes/authGoogle.route.js b/app/core/routes/authGoogle.route.js new file mode 100644 index 0000000..bc09d9d --- /dev/null +++ b/app/core/routes/authGoogle.route.js @@ -0,0 +1,12 @@ +const express = require('express'); + +const router = express.Router(); + +const apiKey = require('../../middlewares/apiKey') +const controller = require('../controllers/auth.controller') + +router.post('/google', apiKey, controller.loginWithGoogle); + + + +module.exports = router; \ No newline at end of file diff --git a/app/core/services/auth.service.js b/app/core/services/auth.service.js new file mode 100644 index 0000000..1128854 --- /dev/null +++ b/app/core/services/auth.service.js @@ -0,0 +1,390 @@ +require('dotenv').config() +const bcrypt = require('bcrypt') +const jwt = require('jsonwebtoken') +const moment = require('moment') +const crypto = require('crypto') +const { Sequelize } = require('sequelize') +const { sequelize } = require('../../../models/migration.js') +const response = require('../../helpers/responses') +const { sendOTP, generateOTP, sendForgotPassword, normalizePhone } = require('../../helpers/helpers') +const UserResource = require('../resources/user.resource') +const { sendMail } = require('../../config/mail.config') +const db = require('../../../models/migration.js') +const { password } = require('../../config/db.config.js') +const errorHandler = require('../../middlewares/errorHandler.js') +const { v4: uuidv4 } = require("uuid"); +const User = db.User +const Branch = db.Branch +const UserOtp = db.UserOtp +const PasswordReset = db.PasswordReset +const { Op } = require('sequelize') +const { OAuth2Client } = require("google-auth-library"); + +const signIn = async (req, res) => { + +const client = new OAuth2Client("GOOGLE_CLIENT_ID"); + + try { + const { email, phone, password, token: tokenGoogle, login_via } = req.body; + let user; + + + if (email) { + user = await User.findOne({ where: { email } }); + + if (login_via == 'GOOGLE') { + // Verify Google token + const ticket = await client.verifyIdToken({ + idToken: tokenGoogle, // Changed from tokenGoogle to idToken + audience: process.env.GOOGLE_CLIENT_ID, + }); + + const payload = ticket.getPayload(); + const googleEmail = payload.email; + + // Find or create user with Google credentials + user = await User.findOne({ + where: { + email: googleEmail + } + }); + + if (!user) { + // Create new user if doesn't exist + user = await User.create({ + email: googleEmail, + name: payload.name, + login_via: 'GOOGLE', + token: tokenGoogle, + google_id: payload.sub, + avatar_url: payload.picture, + }); + }else{ + // Update user's Google token + user.google_id = payload.sub; + user.avatar_url = payload.picture; + user.token = tokenGoogle; + await user.save(); + } + } + + } else if (phone) { + const normalizedPhone = normalizePhone(phone); + user = await User.findOne({ where: { phone: normalizedPhone } }); + } + + if (login_via === 'GOOGLE') { + } else { + if (!user || !(await bcrypt.compare(password, user.password))) { + return res.status(400).json({ error: "Email / Phone atau Password salah" }); + } + } + + if (user.is_suspended) { + return res.status(403).json({ error: "Akun Anda telah ditangguhkan" }); + } + + const now = new Date(); + + // === Generate JWT mirip Supabase === + + const token = jwt.sign( + { + name: user.name, + id: user.id, + email: user.email, + phone: user.phone || "", + role: user.role || "user", + + }, + process.env.JWT_SECRET_KEY + ); + + + // Update last login + user.last_login = now; + user.is_first_login = false; + await user.save(); + + return res.json({ + access_token: token, + token_type: "bearer", + user: { + id: user.id, + name: user.name, + email: user.email, + role: user.role || "user", + created_at: user.created_at.toISOString(), + }, + }); + } catch (error) { + errorHandler(error, req, res) + return response.failed(res, 500, error.message) + } +}; + + + + +const sendOtp = async (req, res) => { + try { + const { email, phone, via } = req.body + const otp = generateOTP(6) + const hash = await bcrypt.hash(otp, 10) + const data = via === 'WHATSAPP' ? email : phone + const otpData = await UserOtp.create({ data, otp, token: hash, expire_in: 60, via }) + + await sendOTP('OTP', otp, via, phone, email) + + return response.success(res, { otpData }, `OTP dikirim ${via}`) + } catch (error) { + return response.failed(res, 500, error.message) + } +} + +const checkOtp = async (req, res) => { + try { + const { otp, token } = req.body + const data = await UserOtp.findOne({ where: { otp, token } }) + + if (!data) { + return response.failed(res, 404, 'OTP tidak valid') + } + + const userCondition = data.via === 'WHATSAPP' ? { email: data.data } : { phone: data.data } + let user = await User.findOne({ where: userCondition }) + + if (!user) { + user = await User.create(userCondition) + } + + const jwtToken = jwt.sign({ id: user.id }, process.env.JWT_SECRET_KEY, { expiresIn: '1d' }) + return response.success(res, { user: new UserResource(user), token: jwtToken }, 'Login OTP berhasil') + + + } catch (error) { + return response.failed(res, 500, error.message) + } +} + +const signUp = async (req, res) => { + try { + const { name, email, password, role, branch_id } = req.body; + + + // Cek apakah email / phone sudah ada + const existingUser = await User.findOne({ + where: { + [Sequelize.Op.or]: [{ email }], + }, + }); + + if (existingUser) { + return res.status(400).json({ error: "Email sudah terdaftar" }); + } + + const branch = await Branch.findOne({ where: { id: branch_id } }); + if (!branch) { + await t.rollback(); + return response.failed(res, 404, "Branch tidak ditemukan"); + } + + // Hash password + const hashedPassword = await bcrypt.hash(password, 10); + + // Buat user baru dalam transaksi + const user = await sequelize.transaction(async (t) => { + const newUser = await User.create( + { + name, + email, + branch_id, + password: hashedPassword, + role: role || "user", + }, + { transaction: t } + ); + + return newUser; + }); + + // Generate token JWT + + + const token = jwt.sign( + { + name: user.name, + id: user.id, + email: user.email, + phone: user.phone || "", + role: user.role || "user", + + }, + process.env.JWT_SECRET_KEY + ); + + const refreshToken = uuidv4(); + const now = new Date(); + + // Update last_login setelah register + user.last_login = now; + user.is_first_login = false; + await user.save(); + + return res.json({ + access_token: token, + token_type: "bearer", + user: { + id: user.id, + name: user.name, + email: user.email, + branch: branch.name, + role: user.role || "user", + created_at: user.created_at.toISOString(), + }, + }); + } catch (error) { + errorHandler(error, req, res) + return response.failed(res, 500, error.message) + } +}; + + +const getUserLogin = async (token, res) => { + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET_KEY) + + const user = await User.findByPk(decoded.id, { + attributes: { exclude: ['password'] } + }) + + if (!user) { + return response.failed(res, 404, 'User Tidak Ditemukan') + } + + return response.success(res, new UserResource(user), 'User ditemukan') + } catch (error) { + return response.failed(res, 401, 'Token Tidak Valid atau expired') + } +} + + + +const forgotPassword = async (body, res) => { + const [user, resetPassword] = await Promise.all([ + User.findOne({ + where: { + email: body.email, + }, + }), + PasswordReset.findOne({ + where: { + email: body.email, + is_used: false, + }, + }), + ]); + + if (!user) { + return response.error(res, 1307, 400); + } + + if (resetPassword) { + resetPassword.token = crypto.randomBytes(20).toString('hex'); + resetPassword.expires_at = moment().add(15, 'minutes').toISOString(); + + await resetPassword.save(); + + await sequelize.transaction(async () => { + await sendForgotPassword(body.email, resetPassword.token); + }); + + const tokenUpdated = { + user, + forgot_password: resetPassword, + }; + + return response.success(res, tokenUpdated, 'Token Updated'); + } + + const emailToken = crypto.randomBytes(20).toString('hex'); + const expiresAt = moment().add(15, 'minutes').toISOString(); + + const dataPasswordReset = await PasswordReset.create({ + email: body.email, + token: emailToken, + expires_at: expiresAt, + }); + + const tokenCreated = { + user, + forgot_password: dataPasswordReset, + }; + + return response.success(res, tokenCreated, 'Token Created'); +}; + + +const resetPassword = async (body, token, res) => { + try { + const resetPassword = await PasswordReset.findOne({ + where: { + is_used: false, + token: token, + }, + }); + console.log('resetPassword data:', resetPassword); + + if (!resetPassword) { + return response.error(res, 1306, 400); + } + + const now = moment(); + const expiresAt = moment(resetPassword.expires_at); + if (now.isAfter(expiresAt)) { + return response.error(res, 1306, 400); + } + + const password = body.new_password; + const confirm_password = body.confirm_password; + + if (password !== confirm_password) { + return response.failed(res, 1105, 400); + } + + const salt = await bcrypt.genSalt(10); + const hash = await bcrypt.hash(password, salt); + + const user = await User.findOne({ + where: { + email: resetPassword.email, + }, + }); + user.password = hash; + user.is_default = false; + await user.save(); + + resetPassword.is_used = true; + await resetPassword.save(); + + return response.success(res, null, 'Password reset successfully'); + } catch (error) { + console.error('Reset password error:', error); + return response.failed(res, 500, error.message || 'Something went wrong'); + } +}; + + +module.exports = { + signIn, + sendOtp, + checkOtp, + signUp, + getUserLogin, + resetPassword, + forgotPassword +} + + + + diff --git a/app/core/services/authGoogle.service.js b/app/core/services/authGoogle.service.js new file mode 100644 index 0000000..d5fbb8f --- /dev/null +++ b/app/core/services/authGoogle.service.js @@ -0,0 +1,128 @@ +const db = require('../../../models/migration') + +const { OAuth2Client } = require("google-auth-library"); +const dotenv = require('dotenv'); + +dotenv.config(); + +const googleClientId = process.env.GOOGLE_CLIENT_ID; +const jwt = require('jsonwebtoken') + +exports.loginWithGoogle = async (req, res) => { + const t = await db.sequelize.transaction(); + try { + const {name, email, google_token } = req.body; + + if (!email || !google_token) { + await t.rollback(); + return responses.failed(res, 400, 'Email and Google token are required'); + } + + const client = new OAuth2Client(googleClientId); + const ticket = await client.verifyIdToken({ + idToken: google_token, + audience: googleClientId, + }); + + const payload = ticket.getPayload(); + + const googleName = payload.name; + const googleAvatar = payload.picture; + + if ( + payload.iss !== 'accounts.google.com' && + payload.iss !== 'https://accounts.google.com' + ) { + await t.rollback(); + return responses.failed(res, 400, 'Invalid token issuer'); + } + + if (payload.email !== email) { + await t.rollback(); + return responses.failed(res, 400, 'Email mismatch'); + } + + let findUser = await db.User.findOne({ + where: { email: email }, + }); + + let isFirstLogin = false; + + if (!findUser) { + findUser = await db.User.create({ + email: email, + name: googleName, + avatar: googleAvatar, + status: 'ACTIVE', + login_via: 'GOOGLE', + }, { transaction: t }); + + const user_id = findUser.id; + + await db.Branch.create({ + name: 'Personal', + initial_balance: 0, + current_balance: 0, + created_by: user_id, + user_id: user_id + }, { transaction: t }); + + await db.Category.create({ + name: 'Umum', + transaction_type: 'income', + icons: 'πŸ’΅', + user_id: user_id + }, { transaction: t }); + + await db.Wallet.create({ + name: 'Dompet', + current_balance: 0, + type: 'Cash', + user_id: user_id + }, { transaction: t }); + + + isFirstLogin = true; + } else { + isFirstLogin = findUser.last_login === null; + } + + + + + + const tokenJwt = jwt.sign( + { + id: findUser.id, + }, + process.env.JWT_SECRET_KEY + ); + + + const data = { + is_google_login: true, + token: tokenJwt, + user_data: { + id: findUser.id, + name: findUser.name || googleName, + avatar: findUser.avatar || googleAvatar, + username: findUser.username, + email: findUser.email, + + }, + google_ticket: ticket, + }; + + await t.commit(); + + return res + .status(200) + .json({ success: true, message: 'Login successful', data }); + } catch (error) { + await t.rollback() + console.error('Error logging in with Google:', error); + return res + .status(500) + .json({ success: false, message: error.message }); + } +}; \ No newline at end of file diff --git a/app/helpers/bodyResponses.js b/app/helpers/bodyResponses.js new file mode 100644 index 0000000..0108b86 --- /dev/null +++ b/app/helpers/bodyResponses.js @@ -0,0 +1,118 @@ +const bodyParser = require("body-parser"); +const express = require("express"); +const { getErrorMessage } = require("../constants/api_constans.js"); + +const success = (data, msg, param = null) => { + var dataJson = data; + var data = {}; + if (param != null) data[param] = dataJson; + else data = dataJson; + + return { + success: true, + message: msg, + data, + }; +}; + +const succes = (res, data, msg, param) => { + var dataJson = data; + var data = {}; + if (param != null) data[param] = dataJson; + else data = dataJson; + + return res.status(200).json({ + success: true, + message: msg, + data, + }); +}; + +const error = (res, errorCode, code, message = null) => { + var errorMessage = getErrorMessage(errorCode); + + return res.status(code).json({ + success: false, + message: message ?? errorMessage, + error_code: errorCode, + data: null, + }); +}; + +const failed = (errorCode, message = null) => { + var errorMessage = getErrorMessage(errorCode); + + return { + success: false, + message: message ?? errorMessage, + error_code: errorCode, + data: null, + }; +}; + +const pagination = (page, size) => { + let limit = null; + let offset = null; + if (page != null) { + limit = size ? +size : 5; + offset = limit ? 0 + (page - 1) * limit : null; + } + + return { + limit, + offset, + }; +}; + +const pagingData = (data) => { + const { count: totalItems, rows: items } = data; + return items; +}; + +const pagingInfo = (data, page, limit) => { + const { count: totalItems, rows: items } = data; + const currentPage = page ? +page : 0; + const totalPages = Math.ceil(totalItems / limit); + const pageSize = +limit; + + return { + page: currentPage, + per_page: pageSize, + total: totalItems, + total_pages: totalPages, + }; +}; + +const paginationResponse = (data, page, limit = 5, key) => { + const response = {}; + + if (page != null) { + const items = pagingData(data); + const paginate = pagingInfo(data, page, limit); + + response[key] = items; + if (paginate != null) response["pagination"] = paginate; + } else { + const items = pagingData(data); + response[key] = items; + } + + return response; +}; + +const json = (res, data) => { + var statusCode = data.success == true ? 200 : 500; + return res.status(statusCode).json(data); +}; + +module.exports = { + success, + succes, + error, + pagination, + pagingData, + pagingInfo, + paginationResponse, + failed, + json, +}; diff --git a/app/helpers/distance.js b/app/helpers/distance.js new file mode 100644 index 0000000..a1b7ba9 --- /dev/null +++ b/app/helpers/distance.js @@ -0,0 +1,27 @@ +function getDistance(lat1, lon1, lat2, lon2) { + // pastikan semuanya number + lat1 = parseFloat(lat1); + lon1 = parseFloat(lon1); + lat2 = parseFloat(lat2); + lon2 = parseFloat(lon2); + + const R = 6371000; // radius bumi dalam meter + const toRad = Math.PI / 180; + + const dLat = (lat2 - lat1) * toRad; + const dLon = (lon2 - lon1) * toRad; + + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(lat1 * toRad) * + Math.cos(lat2 * toRad) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + const distance = R * c; + + return distance; + } + + module.exports = { getDistance }; + \ No newline at end of file diff --git a/app/helpers/enum.helper.js b/app/helpers/enum.helper.js new file mode 100644 index 0000000..4bb5811 --- /dev/null +++ b/app/helpers/enum.helper.js @@ -0,0 +1,29 @@ +// app/helpers/enums.helper.js + +const walletTypes = [ + { value: 'Credit Card', label: 'Credit Card' }, + { value: 'E-Wallet', label: 'E-Wallet' }, + { value: 'Debit', label: 'Debit' } + ] + + const transactionTypes = [ + { value: 'income', label: 'Pemasukan' }, + { value: 'expanse', label: 'Pengeluaran' }, + { value: 'debt', label: 'Hutang' }, + { value: 'Receivable', label: 'Piutang' }, + ] + + const balanceTypes = [ + { value: 'balance', label: 'Saldo' }, + { value: 'debt', label: 'Utang' }, + { value: 'receivable', label: 'Piutang' }, + { value: 'income', label: 'Pemasukan' }, + { value: 'expanse', label: 'Pengeluaran' } + ] + + module.exports = { + walletTypes, + transactionTypes, + balanceTypes + } + \ No newline at end of file diff --git a/app/helpers/helpers.js b/app/helpers/helpers.js new file mode 100644 index 0000000..42c5f6c --- /dev/null +++ b/app/helpers/helpers.js @@ -0,0 +1,111 @@ +const bcrypt = require('bcrypt'); +const crypto = require('crypto'); +const moment = require('moment'); +const { sendMail } = require('../config/mail.config'); + +// πŸ” Generate OTP (panjang default 6 digit) +function generateOTP(length = 6) { + const digits = '0123456789'; + let otp = ''; + for (let i = 0; i < length; i++) { + otp += digits[Math.floor(Math.random() * 10)]; + } + return otp; +} + +// πŸ“€ Kirim OTP via WhatsApp atau Email +async function sendOTP(type, otp, via, phone, email) { + if (via === 'EMAIL') { + const subject = `${type} Kode Verifikasi`; + const html = ` +

Kode verifikasi Anda adalah:

+

${otp}

+

Gunakan kode ini untuk melanjutkan proses.

+ `; + await sendMail(email, subject, html); + } else if (via === 'WHATSAPP') { + // Simulasi WhatsApp - bisa diintegrasi ke API WhatsApp asli + console.log(`[WhatsApp] Kirim OTP ke ${phone}: ${otp}`); + // TODO: Ganti dengan pengiriman lewat API WhatsApp nyata + } else { + throw new Error('Metode pengiriman OTP tidak valid.'); + } +} + +// πŸ” Kirim Link Reset Password via Email +const sendForgotPassword = async (email, token) => { + + const resetLink = `http://localhost:3000/auth/reset/${token}`; + + const subject = 'Reset Password'; + const html = ` +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Reset Password

+

Halo, User!

+

Here’s the link to reset your Cashfio Account:

+

+ ${resetLink} +

+

For your security, do not share the link with anyone!

+
+
+ Copyright Cashfio – ${new Date().getFullYear()} +
+
+ `; + + await sendMail({ to: email, subject, html }); + + + }; + + +const normalizePhone = (phone) => { + return phone ? phone.replace(/[^+\d]/g, '') : null; + }; + + +module.exports = { + generateOTP, + sendOTP, + sendForgotPassword, + normalizePhone +}; diff --git a/app/helpers/responses.js b/app/helpers/responses.js new file mode 100644 index 0000000..3abf940 --- /dev/null +++ b/app/helpers/responses.js @@ -0,0 +1,80 @@ +const bodyParser = require('body-parser'); +const express = require('express'); +const { getErrorMessage } = require('../constants/api_constans'); + +const success = (res, data, msg, param = null) => { + var dataJson = data; + var data = {}; + if (param != null) data[param] = dataJson; + else data = dataJson; + + return res.status(200).json({ + success: true, + message: msg, + data, + }); +}; + +const failed = (res, errorCode, message = null) => { + const error = getErrorMessage(errorCode); + + return res.status(error.httpCode).json({ + success: false, + message: message || error.message, + error_code: errorCode, + data: null, + }); +}; + +const pagination = (page, size) => { + let limit = null; + let offset = null; + + if(page == null) page = 1; + if(size == null) size = 10; + + if (page != null) { + limit = size ? +size : 5; + offset = limit ? 0 + (page - 1) * limit : null; + } + + return { + limit, + offset, + }; +} + +function paginationResponse(model, page, limit, dataKey, resource) { + const totalItem = model.count; + const currentPage = parseInt(page); + const totalPages = Math.ceil(totalItem / limit); + + return { + [dataKey]: resource, + pagination: { + total_item: totalItem, + total_page: totalPages, + current_page: currentPage, + limit: parseInt(limit), + }, + }; +} + +const error = (res, errorCode, message = null) => { + const error = getErrorMessage(errorCode); + + return res.status(error.httpCode).json({ + success: false, + message: message || error.message, + error_code: errorCode, + data: null, + }); +}; + +module.exports = { + success, + pagination, + paginationResponse, + failed, + error, +}; diff --git a/app/helpers/sync_model.js b/app/helpers/sync_model.js new file mode 100644 index 0000000..92c0347 --- /dev/null +++ b/app/helpers/sync_model.js @@ -0,0 +1,45 @@ +const fs = require('fs'); +const path = require('path'); +const Sequelize = require('sequelize'); +const process = require('process'); +const basename = path.basename(__filename); +const env = process.env.NODE_ENV || 'coba'; +require('dotenv').config(); + +const config = { + username: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE, + host: process.env.DB_HOST, + dialect: process.env.DB_CONNECTION || 'mysql', +}; + +const db = {}; + +let sequelize; +if (config.use_env_variable) { + sequelize = new Sequelize(process.env[config.use_env_variable], config); +} else { + sequelize = new Sequelize( + config.database, + config.username, + config.password, + config + ); +} + +// Import your model +const argv = require('yargs').argv; +const modelName = argv.modelName || 'defaultValue'; + +const Model = require(`../../models/${modelName}.model`)(sequelize); + +// Synchronize the Model model to create its table +Model.sync({ alter: true }) // Set `force: true` to drop the table if it exists + .then(() => { + console.log(`Model ${modelName} created successfully.`); + // Do any additional setup or actions here + }) + .catch((error) => { + console.error('Error creating Model table:', error); + }); diff --git a/app/helpers/token.js b/app/helpers/token.js new file mode 100644 index 0000000..5fb0cb4 --- /dev/null +++ b/app/helpers/token.js @@ -0,0 +1,15 @@ +const jwt = require('jsonwebtoken'); +const dotenv = require('dotenv'); +dotenv.config(); + +module.exports = { + createToken: (payload) => { + return jwt.sign(payload, process.env.JWT_SECRET_KEY, { + expiresIn: '1d', + }); + }, + + validateToken: (token) => { + return jwt.verify(token, process.env.JWT_SECRET_KEY); + }, +}; diff --git a/app/helpers/updateBalance.js b/app/helpers/updateBalance.js new file mode 100644 index 0000000..399bca3 --- /dev/null +++ b/app/helpers/updateBalance.js @@ -0,0 +1,22 @@ + +const db = require('../../../../models/migration') +const Balance = db.Balance + +async function updateBalance(user_id, type, amount, t) { + if (!['income', 'expanse', 'debt', 'receivable'].includes(type)) return; + + const [balance, created] = await Balance.findOrCreate({ + where: { user_id, type }, + defaults: { amount: 0 }, + transaction: t, + }); + + const operation = type === 'expanse' ? -1 : 1; + + await balance.increment('amount', { + by: amount * operation, + transaction: t + }); + + await balance.reload({ transaction: t }); +} diff --git a/app/middlewares/OptionalLogin.js b/app/middlewares/OptionalLogin.js new file mode 100644 index 0000000..d0078cb --- /dev/null +++ b/app/middlewares/OptionalLogin.js @@ -0,0 +1,34 @@ +require('dotenv').config(); +const jwt = require('jsonwebtoken'); +const db = require('../../models/migration'); +const User = db.User; + +const optionalAuth = async (req, res, next) => { + try { + const header = req.header('Authorization'); + + // kalau tidak ada token, user dianggap belum login β†’ lanjut aja + if (!header) { + req.user = null; + return next(); + } + + const idToken = header.replace('Bearer ', ''); + const decoded = jwt.verify(idToken, process.env.JWT_SECRET_KEY); + + const user = await User.findByPk(decoded.id); + if (!user || user.is_suspended) { + req.user = null; + return next(); + } + + req.user = user; + return next(); + } catch (e) { + // kalau token invalid, tetap lanjut tapi tanpa user + req.user = null; + return next(); + } +}; + +module.exports = optionalAuth; diff --git a/app/middlewares/apiKey.js b/app/middlewares/apiKey.js new file mode 100644 index 0000000..07717b8 --- /dev/null +++ b/app/middlewares/apiKey.js @@ -0,0 +1,48 @@ +const express = require("express"); +const app = express(); +const Sequelize = require("sequelize"); +const db = require("../../models/migration"); +const ApiKey = db.apiKey; + +async function apiKey(req, res, next) { + const api_key = req.get("ApiKey"); + + if (!api_key) { + + return res.status(401).json({ + success: false, + message: "API Key is missing", + code: 401, + }); + } + + try { + + const apiKeyData = await ApiKey.findOne({ + where: { + api_key: api_key, + is_actived: 1, + }, + }); + + if (!apiKeyData) { + + return res.status(401).json({ + success: false, + message: "Unauthorized", + code: 401, + }); + } + + next(); + } catch (err) { + console.error("Error querying API key from database:", err); + return res.status(500).json({ + success: false, + message: "Internal Server Error", + code: 500, + }); + } +} + +module.exports = apiKey; diff --git a/app/middlewares/authentication.js b/app/middlewares/authentication.js new file mode 100644 index 0000000..6b90407 --- /dev/null +++ b/app/middlewares/authentication.js @@ -0,0 +1,35 @@ +require('dotenv').config(); +const jwt = require('jsonwebtoken'); +const responses = require('../helpers/responses'); +const db = require('../../models/migration'); +const User = db.User; + +const authentication = async (req, res, next) => { + try { + const header = req.header('Authorization'); + if (!header) { + return responses.failed(res, 401); + } + + const idToken = header.replace('Bearer ', ''); + const decoded = jwt.verify(idToken, process.env.JWT_SECRET_KEY); + + const userId = decoded.id; + const user = await User.findByPk(userId); + + if (!user) { + return responses.failed(res, 401); + } + if (user.is_suspended) { + return responses.failed(res, 403, 'Akun Anda telah ditangguhkan'); + } + + req.user = user; + return next(); + } catch (e) { + console.error(e); + return responses.failed(res, 401); + } +}; + +module.exports = authentication; diff --git a/app/middlewares/checkRole.js b/app/middlewares/checkRole.js new file mode 100644 index 0000000..cab2158 --- /dev/null +++ b/app/middlewares/checkRole.js @@ -0,0 +1,16 @@ +// middlewares/checkRole.js +const responses = require('../helpers/responses'); + +module.exports = function(...allowedRoles) { + return (req, res, next) => { + if (!req.user) { + return responses.failed(res, 401, 'Anda belum login'); + } + + if (!allowedRoles.includes(req.user.role)) { + return responses.failed(res, 403, 'Anda tidak memiliki izin untuk mengakses resource ini'); + } + + next(); + }; +}; diff --git a/app/middlewares/errorHandler.js b/app/middlewares/errorHandler.js new file mode 100644 index 0000000..fe18819 --- /dev/null +++ b/app/middlewares/errorHandler.js @@ -0,0 +1,50 @@ +const fs = require('fs'); +const db = require('../../models/migration'); +const BugReporting = db.BugReporting; +const onFinished = require('on-finished'); + +// Fungsi untuk menyimpan bug report ke database +async function saveBugReport(report) { + try { + const bugReport = await BugReporting.create(report); + return bugReport.insertId; + } catch (error) { + console.error('Error while saving bug report to database:', error); + throw error; + } +} + +function errorHandler(err, req, res) { + // console.error('Error :', err); + // console.error('request :', req); + // console.error('Response :', res); + + const bugReport = { + class: err.constructor.name, + file: err.fileName || null, + code: null, + status_code: res.statusCode || null, + line: err.lineNumber || null, + message: err.message, + trace: err.stack, + user_id: null, + data: JSON.stringify(req.body), + url: req.originalUrl, + method: req.method, + ip: req.ip, + created_at: new Date(), + updated_at: new Date(), + }; + + // Simpan bug report ke database + saveBugReport(bugReport) + .then(() => { + // Tangani respons kesalahan + console.error('error: Terjadi kesalahan dalam server'); + }) + .catch((error) => { + console.error('Error while saving bug report:', error); + }); +} + +module.exports = errorHandler; diff --git a/app/middlewares/login.js b/app/middlewares/login.js new file mode 100644 index 0000000..2f7daf1 --- /dev/null +++ b/app/middlewares/login.js @@ -0,0 +1,20 @@ +const responses = require('../helpers/responses'); +const { validateToken } = require('../helpers/token'); + +const login = async (req, res, next) => { + let token = req.headers.authorization; + + if (!token) return responses.failed(res, 404, 'token not found!'); + token = token.split('')[1]; + + try { + const validateTokenResult = validateToken(token); + dataToken = validateTokenResult; + // console.log(dataToken); + next(); + } catch (error) { + responses.failed(res, 500, error.message); + } +}; + +module.exports = { login }; diff --git a/app/middlewares/optionalAuth.js b/app/middlewares/optionalAuth.js new file mode 100644 index 0000000..1b069c3 --- /dev/null +++ b/app/middlewares/optionalAuth.js @@ -0,0 +1,20 @@ +const jwt = require('jsonwebtoken') + +module.exports = (req, res, next) => { + req.user = null // default selalu null + + const authHeader = req.headers['authorization'] + if (authHeader) { + const token = authHeader.split(' ')[1] + if (token) { + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET) + req.user = decoded + } catch (err) { + // invalid β†’ tetap null + } + } + } + + next() +} diff --git a/app/middlewares/upload.js b/app/middlewares/upload.js new file mode 100644 index 0000000..27d7a21 --- /dev/null +++ b/app/middlewares/upload.js @@ -0,0 +1,24 @@ +const multer = require("multer"); + +// βœ… Pakai memoryStorage β†’ file masuk ke RAM, ada file.buffer +const storage = multer.memoryStorage(); + +// βœ… Filter hanya image (jpg, jpeg, png, webp) +const fileFilter = (req, file, cb) => { + const allowedTypes = ["image/jpeg", "image/jpg", "image/png", "image/webp"]; + if (allowedTypes.includes(file.mimetype)) { + cb(null, true); // lolos + } else { + cb(new Error("Hanya file gambar yang diperbolehkan (jpg, jpeg, png, webp)"), false); + } +}; + +const upload = multer({ + storage, + fileFilter, + limits: { + fileSize: 100 * 1024 * 1024, + }, +}); + +module.exports = upload; diff --git a/app/modules/absensi/controllers/absensi.controller.js b/app/modules/absensi/controllers/absensi.controller.js new file mode 100644 index 0000000..d628663 --- /dev/null +++ b/app/modules/absensi/controllers/absensi.controller.js @@ -0,0 +1,35 @@ + +const services = require('../services/absensi.service') + +const history = async (req, res) => { + const response = await services.history(req, res); + return response; +}; + +const create = async (req, res) => { + const response = await services.create(req, res); + return response; +}; + +const clockOut = async (req, res) => { + const response = await services.clockOut(req, res) + return response +} + +const update = async (req, res) => { + const response = await services.update(req, res); + return response +} + +const destroy = async (req, res) => { + const response = await services.destroy(req, res); + return response; +}; + +module.exports = { + create, + history, + update, + destroy, + clockOut +} \ No newline at end of file diff --git a/app/modules/absensi/resources/category.resource.js b/app/modules/absensi/resources/category.resource.js new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/absensi/routes/absensi.route.js b/app/modules/absensi/routes/absensi.route.js new file mode 100644 index 0000000..295b15d --- /dev/null +++ b/app/modules/absensi/routes/absensi.route.js @@ -0,0 +1,21 @@ +const express = require('express') +const router = express.Router() +const controller = require('../controllers/absensi.controller') +const apiKey = require('../../../middlewares/apiKey') +const jwt = require('../../../middlewares/authentication') +const upload = require('../../../middlewares/upload') + +router.get('/history', apiKey, jwt, (req, res) => { + controller.history(req, res); +}) + +router.post('/', jwt, apiKey, upload.single('attendances'), (req, res) => { + controller.create(req, res) +}) + +router.post('/clock-out', jwt, apiKey, (req, res) => { + controller.clockOut(req, res) +}) + + +module.exports = router \ No newline at end of file diff --git a/app/modules/absensi/services/absensi.service.js b/app/modules/absensi/services/absensi.service.js new file mode 100644 index 0000000..0e36d80 --- /dev/null +++ b/app/modules/absensi/services/absensi.service.js @@ -0,0 +1,309 @@ +const response = require('../../../helpers/responses'); +const db = require('../../../../models/migration'); +const errorHandler = require('../../../middlewares/errorHandler') +const { sequelize, Op } = require('../../../../models/migration'); +const { getDistance } = require('../../../helpers/distance'); +const moment = require('moment-timezone') +const path = require("path"); +const fs = require("fs"); +const axios = require("axios"); + + +const User = db.User +const Attedances = db.Attedances +const Branch = db.Branch + +const saveUploadedFile = (file, folder = "public/uploads") => { + const dir = path.join(process.cwd(), folder); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + + const fileName = `${Date.now()}-${file.originalname}`; + const filePath = path.join(dir, fileName); + + // file.buffer karena kita pakai memoryStorage + fs.writeFileSync(filePath, file.buffer); + + // return URL yang bisa diakses dari frontend + return `/${folder.replace("public/", "")}/${fileName}`.replace(/\\/g, "/"); +}; + +const create = async (req, res) => { + const t = await sequelize.transaction(); + try { + const { type, lat, lng, reason } = req.body; + const user_id = req.user.id; + const now = moment().tz('Asia/Jakarta'); + const today = now.format('YYYY-MM-DD'); + + const user = await User.findOne({ where: { id: user_id } }); + if (!user) return response.failed(res, 404, 'User tidak ditemukan'); + + const branch = await Branch.findOne({ where: { id: user.branch_id } }); + if (!branch) return response.failed(res, 404, 'Branch kantor tidak ditemukan'); + + let attendance = await Attedances.findOne({ + where: { user_id, date: today }, + }); + + // === Jika izin (sakit / izin) === + if (['sick', 'permission'].includes(type)) { + if (attendance) return response.failed(res, 400, 'Sudah ada absensi hari ini'); + + attendance = await Attedances.create({ + user_id, + branch_id: user.branch_id, + name: user.name, + type, + reason, + date: today, + }, { transaction: t }); + + await t.commit(); + return response.success(res, attendance, 'Izin berhasil disimpan'); + } + + // === Jika sudah absen masuk tapi belum pulang === + // if (attendance && attendance.clock_in && !attendance.clock_out) { + // attendance.clock_out = now.toDate(); // waktu WIB + // const durationMs = moment(attendance.clock_out).diff(moment(attendance.clock_in)); + // const hours = Math.floor(durationMs / (1000 * 60 * 60)); + // const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60)); + // attendance.work_duration = `${hours} jam ${minutes} menit`; + + // await attendance.save({ transaction: t }); + // await t.commit(); + // return response.success(res, { + // ...attendance.toJSON(), + // clock_in: moment(attendance.clock_in).tz('Asia/Jakarta').format('YYYY-MM-DD HH:mm:ss'), + // clock_out: attendance.clock_out + // ? moment(attendance.clock_out).tz('Asia/Jakarta').format('YYYY-MM-DD HH:mm:ss') + // : null + // }, 'Absen pulang berhasil'); + // } + + // === Jika belum absen sama sekali, cek lokasi === + const distance = getDistance(branch.lat, branch.lng, lat, lng); + const allowedRadius = parseFloat(process.env.ABSENCE_RADIUS) || 100; + + if (distance > allowedRadius) { + await t.rollback(); + return response.failed(res, 400, `Lokasi di luar area kantor (${distance.toFixed(2)} meter)`); + } + + let finalPhotoUrl = null; + + if (req.file) { + // Jika upload file langsung + finalPhotoUrl = saveUploadedFile(req.file, "public/uploads/attendance_photos"); + } else if (req.body.photo) { + // Jika kirim URL dari FE (misal kamera web base64 atau URL publik) + const imageUrl = req.body.photo; + const fileName = `${Date.now()}-${Math.random() + .toString(36) + .substring(7)}.jpg`; + const filePath = path.join("public/uploads/attendance_photos", fileName); + + const responseImg = await axios({ + url: imageUrl, + responseType: "arraybuffer", + }); + + fs.writeFileSync(filePath, responseImg.data); + finalPhotoUrl = filePath.replace("public", "").replace(/\\/g, "/"); + } + + + + // === Absen masuk === + attendance = await Attedances.create({ + user_id, + name: user.name, + photo: finalPhotoUrl, + branch_id: user.branch_id, + type: 'present', + date: today, + clock_in: now.toDate(), + lat, + lng, + }, { transaction: t }); + + await t.commit(); + return response.success(res, { + ...attendance.toJSON(), + clock_in: moment(attendance.clock_in).tz('Asia/Jakarta').format('YYYY-MM-DD HH:mm:ss'), + clock_out: attendance.clock_out + ? moment(attendance.clock_out).tz('Asia/Jakarta').format('YYYY-MM-DD HH:mm:ss') + : null + }, 'Absen masuk berhasil'); + } catch (error) { + await t.rollback(); + errorHandler(error, res, req); + return response.failed(res, 500, error.message); + } +}; + +const clockOut = async (req, res) => { + const t = await sequelize.transaction(); + try { + const user_id = req.user.id; + const now = moment().tz('Asia/Jakarta'); + const today = now.format('YYYY-MM-DD'); + + const user = await User.findOne({ where: { id: user_id } }); + if (!user) return response.failed(res, 404, 'User tidak ditemukan'); + + const attendance = await Attedances.findOne({ + where: { user_id, date: today, type: 'present' }, + }); + + if (!attendance) { + await t.rollback(); + return response.failed(res, 400, 'Belum absen masuk hari ini'); + } + + if (attendance.clock_out) { + await t.rollback(); + return response.failed(res, 400, 'Sudah absen pulang hari ini'); + } + + // Set jam pulang (tanpa cek lokasi) + attendance.clock_out = now.toDate(); + + // Hitung durasi kerja + const durationMs = moment(attendance.clock_out).diff(moment(attendance.clock_in)); + const hours = Math.floor(durationMs / (1000 * 60 * 60)); + const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60)); + attendance.work_duration = `${hours} jam ${minutes} menit`; + + await attendance.save({ transaction: t }); + await t.commit(); + + return response.success(res, { + ...attendance.toJSON(), + clock_in: moment(attendance.clock_in).tz('Asia/Jakarta').format('YYYY-MM-DD HH:mm:ss'), + clock_out: moment(attendance.clock_out).tz('Asia/Jakarta').format('YYYY-MM-DD HH:mm:ss'), + work_duration: attendance.work_duration, + }, 'Absen pulang berhasil'); + } catch (error) { + await t.rollback(); + errorHandler(error, res, req); + return response.failed(res, 500, error.message); + } +}; + + + +const history = async (req, res) => { + try { + const user_id = req.user?.id; + if (!user_id) return response.failed(res, 401, "User tidak terautentikasi"); + + const { type } = req.query; + const where = { user_id }; + + if (type === 'today') { + const today = new Date(Date.now() + 7 * 60 * 60 * 1000).toISOString().split('T')[0]; + where.date = today; // pakai equality, bukan LIKE + } + + const attendances = await Attedances.findAll({ + where, + order: [['date', 'DESC']], + }); + + if (!attendances.length) + return response.success(res, [], "Tidak ada data absensi"); + + const result = attendances.map(a => { + let duration = null; + + if (a.clock_in && a.clock_out) { + const diffMs = new Date(a.clock_out) - new Date(a.clock_in); + const totalMinutes = Math.floor(diffMs / 60000); + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + duration = `${hours} jam ${minutes} menit`; + } + + return { + name: a.name, + date: moment(a.date).tz('Asia/Jakarta').format('YYYY-MM-DD'), + clock_in: a.clock_in + ? moment(a.clock_in).tz('Asia/Jakarta').format('YYYY-MM-DD HH:mm:ss') + : '-', + clock_out: a.clock_out + ? moment(a.clock_out).tz('Asia/Jakarta').format('YYYY-MM-DD HH:mm:ss') + : '-', + duration: duration || '-', + type: a.type, + }; + }); + + return response.success(res, result, "Riwayat absensi berhasil dimuat"); + } catch (error) { + errorHandler(error, req, res); + return response.failed(res, 500, error.message); + } +}; + + + +const update = async (req, res) => { + const t = await sequelize.transaction(); + try { + const id = req.params.id; + const user_id = req.user.id; + const body = req.body; + + const categories = await Category.findOne({ + where: { id }, + transaction: t + }); + + const categoriesUpdate = await categories.update({ + ...body, + user_id, + }, { transaction: t }); + + await t.commit(); + return response.success(res, categoriesUpdate, 'Category Berhasil Di update'); + } catch (error) { + await t.rollback(); + errorHandler(error, req, res); + return response.failed(res, 500, error.message); + } +} + +const destroy = async (req, res) => { + const t = await sequelize.transaction(); + try { + const id = req.params.id; + const category = await Category.findOne({ + where: { id }, + transaction: t, + }); + + if (!category) { + await t.rollback(); + return response.failed(res, 404, 'Category tidak ditemukan'); + } + + await category.destroy({ transaction: t }); + await t.commit(); + + return response.success(res, null, 'Category berhasil dihapus'); + } catch (error) { + await t.rollback(); + errorHandler(error, req, res); + return response.failed(res, 500, error.message); + } +}; + + +module.exports = { + create, + destroy, + history, + update, + clockOut +} \ No newline at end of file diff --git a/app/modules/branch/controllers/branch.controller.js b/app/modules/branch/controllers/branch.controller.js new file mode 100644 index 0000000..2ad81df --- /dev/null +++ b/app/modules/branch/controllers/branch.controller.js @@ -0,0 +1,28 @@ +const services = require('../services/branch.service') + +const getAll = async (req, res) => { + const response = await services.getAll(req, res) + return response +} + +const create = async (req, res) => { + const response = await services.create(req, res) + return response +} + +const update = async (req, res) => { + const response = await services.update(req, res) + return response +} + +const destroy = async (req, res) => { + const response = await services.destroy(req, res) + return response +} + +module.exports = { + getAll, + create, + update, + destroy +} \ No newline at end of file diff --git a/app/modules/branch/routes/branch.route.js b/app/modules/branch/routes/branch.route.js new file mode 100644 index 0000000..171f91c --- /dev/null +++ b/app/modules/branch/routes/branch.route.js @@ -0,0 +1,23 @@ +const express = require('express') +const router = express.Router() +const controller = require('../controllers/branch.controller') +const apiKey = require('../../../middlewares/apiKey') +const jwt = require('../../../middlewares/authentication') + +router.get('/', apiKey, (req, res) => { + controller.getAll(req, res) +}) + +router.post('/', apiKey, jwt, (req, res) => { + controller.create(req, res) +} ) + +router.put('/:id', apiKey, jwt, (req, res) => { + controller.update(req, res) +}) + +router.put('/:id', apiKey, jwt, (req, res) => { + controller.destroy(req, res) +}) + +module.exports = router \ No newline at end of file diff --git a/app/modules/branch/services/branch.service.js b/app/modules/branch/services/branch.service.js new file mode 100644 index 0000000..c8d7b93 --- /dev/null +++ b/app/modules/branch/services/branch.service.js @@ -0,0 +1,96 @@ +const response = require('../../../helpers/responses') +const db = require('../../../../models/migration') +const errorHandler = require('../../../middlewares/errorHandler') +const {sequelize} = require('../../../../models/migration') +const { where } = require('sequelize') +const Branch = db.Branch + +const getAll = async (req, res) => { + try { + const branch = await Branch.findAll({ + order: [['created_at', 'DESC']] + }) + return response.success(res, branch, 'successfully loaded') + } catch (error) { + errorHandler(error, req, res) + return response.failed(res, 500, error.message) + } +} + +const create = async (req, res) => { + const t = await sequelize.transaction() + try { + const user_id = req.user.id + const body = req.body + + const branch = await Branch.create({ + ...body, + user_id + }) + + await t.commit() + return response.success(res, branch, 'create successfuly') + } catch (error) { + errorHandler(error, req, res) + return response.failed(res, 500, error.message) + } +} + +const update = async (req, res) => { + const t = await sequelize.transaction() + try { + const id = req.params.id + const body = req.body + const user_id = req.user.id + + const branch = await Branch.findOne({ + where: {id}, + transaction: t + }) + + if (!branch) { + await t.rollback() + return response.failed(res, 404, 'Data Not Found') + } + + const branchUpdate = await branch.update({ + ...body, + user_id + }) + + await t.commit() + return response.success(res, branchUpdate, 'Updated Successfuly') + + + } catch (error) { + await t.rollback() + errorHandler(error, req, res) + return response.failed(res, 500, error.message) + } +} + +const destroy = async (req, res) => { + try { + const id = req.params.id + const branch = await Branch.findOne({ + where: { id }, + }) + + if (!branch) { + return response.failed(res, 404, 'Data Not Found') + } + await branch.destroy(); + return response.success(res, null, 'Deleted Successfuly') + + } catch (error) { + errorHandler(error, req, res) + return response.failed(res, 500, error.message) + } +} + +module.exports = { + getAll, + create, + update, + destroy +} \ No newline at end of file diff --git a/app/modules/profile/controllers/profile.controller.js b/app/modules/profile/controllers/profile.controller.js new file mode 100644 index 0000000..4a46e1f --- /dev/null +++ b/app/modules/profile/controllers/profile.controller.js @@ -0,0 +1,22 @@ +const service = require('../services/profile.service') + +const update = async (req, res) => { + const response = await service.update(req, res) + return response +} + +const getProfile = async (req, res) => { + const response = await service.getProfile(req, res) + return response +} + +const getOverview = async (req, res) => { + const response = await service.getOverview(req, res) + return response +} + +module.exports = { + update, + getProfile, + getOverview +} \ No newline at end of file diff --git a/app/modules/profile/resources/profile.resource.js b/app/modules/profile/resources/profile.resource.js new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/profile/routes/profile.route.js b/app/modules/profile/routes/profile.route.js new file mode 100644 index 0000000..365cc07 --- /dev/null +++ b/app/modules/profile/routes/profile.route.js @@ -0,0 +1,20 @@ +const express = require('express') +const router = express.Router() +const controller = require('../controllers/profile.controller') +const apiKey = require('../../../middlewares/apiKey') +const jwt = require('../../../middlewares/authentication') +const upload = require('../../../middlewares/upload') + +router.get('/', apiKey, jwt, (req, res) => { + controller.getProfile(req, res) +}) + +router.put('/', apiKey, jwt,upload.single("avatar"), (req, res) => { + controller.update(req, res) +}) + +router.get('/overview', apiKey, jwt, (req, res) => { + controller.getOverview(req, res) +}) + +module.exports = router \ No newline at end of file diff --git a/app/modules/profile/services/profile.service.js b/app/modules/profile/services/profile.service.js new file mode 100644 index 0000000..b70a209 --- /dev/null +++ b/app/modules/profile/services/profile.service.js @@ -0,0 +1,262 @@ +const response = require('../../../helpers/responses') +const db = require('../../../../models/migration') +const errorHandler = require('../../../middlewares/errorHandler') +const { sequelize, Op } = require('../../../../models/migration') + +const User = db.User +const path = require("path"); +const fs = require("fs"); +const Story = db.Story; +const StoryReader = db.StoryReader +const Serial = db.Serial; +const StoryRating = db.StoryRating +const StoryLike = db.StoryLike +const Category = db.Category; +const DifficultyLevel = db.DifficultyLevel +const AgeTarget = db.AgeTarget + + + +// UPDATE +const update = async (req, res) => { + const t = await sequelize.transaction(); + try { + const id = req.user.id; + + const users = await User.findOne({ + where: { id }, + transaction: t, + }); + + if (!users) { + await t.rollback(); + return response.failed(res, 404, "User tidak ditemukan atau bukan milik Anda"); + } + + const body = req.body; + let avatarUrl = users.avatar_url; // default tetap avatar lama + + // πŸ”Ή Kalau ada file diupload + if (req.file) { + const uploadDir = path.join(process.cwd(), "public", "uploads", "avatars"); + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); + } + + // Simpan file + const fileName = `${Date.now()}-${req.file.originalname}`; + const filePath = path.join(uploadDir, fileName); + fs.writeFileSync(filePath, req.file.buffer); + + // Bisa pakai URL public (misal /uploads/avatars/xxx.jpg) + avatarUrl = `/uploads/avatars/${fileName}`; + } + + const updatedUser = await users.update( + { + ...body, + avatar_url: avatarUrl, + }, + { transaction: t } + ); + + await t.commit(); + + return response.success(res, updatedUser, "Profil berhasil diperbarui"); + } catch (error) { + await t.rollback(); + errorHandler(error, req, res); + return response.failed(res, 500, error.message); + } +}; + +// GET ALL +const getProfile = async (req, res) => { + try { + const user_id = req.user.id + + const user = await User.findOne({ + where: { id: user_id } + }) + + if (!user) { + return response.failed(res, 404, 'User tidak ditemukan') + } + + // Mapper agar sesuai response yang kamu mau + const result = { + id: user.id, + email: user.email, + display_name: user.name, // atau field display_name jika ada + role: user.role, + avatar_url: user.avatar_url || null, + bio: user.bio || null, + birth: user.birth || null, + created_at: user.created_at, + updated_at: user.updated_at + } + + return response.success(res, result, 'Profile berhasil dimuat') + } catch (error) { + errorHandler(error, req, res) + return response.failed(res, 500, error.message) + } +} + + +const getOverview = async (req, res) => { + try { + const user_id = req.user.id; + + // Total cerita + const totalStories = await Story.count({ where: { user_id } }); + const totalPublished = await Story.count({ where: { user_id, is_published: true } }); + + // Total pembaca unik + const totalReaders = await StoryReader.count({ + include: [{ model: Story, where: { user_id }, attributes: [] }], + distinct: true, + col: "user_id" + }); + + // Rating rata-rata (FIX: kualifikasi kolom supaya tidak ambiguous) + const ratingResult = await StoryRating.findOne({ + attributes: [ + [sequelize.fn("AVG", sequelize.col("StoryRating.rating")), "avgRating"] + ], + include: [{ model: Story, where: { user_id }, attributes: [] }], + raw: true + }); + const avgRating = ratingResult && ratingResult.avgRating + ? Number(parseFloat(ratingResult.avgRating).toFixed(1)) + : 0; + + // === Ambil semua serial user === + const serialsRaw = await Serial.findAll({ + where: { user_id }, + attributes: [ + "id", + "title", + "description", + "reading_time", + "rating", + "cover_image_url", + "createdAt", + "is_active" + ], + include: [{ model: Category, attributes: ["id", "title", "emoji"] }], + order: [["createdAt", "DESC"]] + }); + + // Ambil semua story user (sekalian untuk lookup cover) + const stories = await Story.findAll({ + where: { user_id }, + attributes: [ + "id", + "title", + "synopsis", + "is_published", + "cover_image_url", + "createdAt", + "series_id" + ], + include: [ + { model: Category, attributes: ["id", "title", "emoji"] }, + { model: DifficultyLevel, attributes: ["id", "title", "emoji"] }, + { model: AgeTarget, attributes: ["id", "title", "emoji"] } + ], + order: [["createdAt", "DESC"]] + }); + + // Buat map serial_id β†’ serial + const serialMap = {}; + serialsRaw.forEach(serial => { + serialMap[serial.id] = serial.toJSON(); + }); + + // Buat map serial_id β†’ story pertama (untuk fallback cover) + const storiesBySerial = {}; + for (const story of stories) { + if (story.series_id && !storiesBySerial[story.series_id]) { + storiesBySerial[story.series_id] = story.cover_image_url; + } + } + + // === Gabungkan serial dengan fallback cover === + const serials = serialsRaw.map(serial => { + let coverImage = serial.cover_image_url; + if (!coverImage && storiesBySerial[serial.id]) { + coverImage = storiesBySerial[serial.id]; + } + + const storiesOfSerial = stories.filter(s => s.series_id === serial.id); + + return { + id: serial.id, + title: serial.title, + description: serial.description, + reading_time: serial.reading_time, + rating: serial.rating, + cover_image_url: coverImage, + is_active: serial.is_active, + createdAt: serial.createdAt, + category: serial.Category + ? { id: serial.Category.id, title: serial.Category.title, emoji: serial.Category.emoji } + : null, + story: storiesOfSerial.map(s => ({ + id: s.id, + title: s.title, + cover_image_url: s.cover_image_url, + is_published: s.is_published + })), + total_episodes: storiesOfSerial.length + }; + }); + + // === Gabungkan story dengan serial data === + const storiesWithSerial = stories.map(story => { + const serial = story.series_id ? serialMap[story.series_id] || null : null; + let coverImage = story.cover_image_url; + + // Fallback cover dari serial + if (!coverImage && serial) { + coverImage = serial.cover_image_url || storiesBySerial[serial.id] || null; + } + + return { + ...story.toJSON(), + cover_image_url: coverImage, + Serial: serial + }; + }); + + // === Response === + return response.success( + res, + { + total_stories: totalStories, + total_published: totalPublished, + total_readers: totalReaders, + average_rating: avgRating, + stories: storiesWithSerial, + serials + }, + "Overview berhasil dimuat" + ); + } catch (error) { + errorHandler(error, req, res); + return response.failed(res, 500, error.message); + } +}; + + + + + + + +module.exports = { + update, + getProfile, + getOverview +} diff --git a/auth.html b/auth.html new file mode 100644 index 0000000..72bc3a4 --- /dev/null +++ b/auth.html @@ -0,0 +1,28 @@ + + + + Google Login Test + + + + +

Login dengan Google

+ +
+
+ +
+ +

Google Token (id_token):

+

+  
+
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..19e2087
--- /dev/null
+++ b/index.js
@@ -0,0 +1,66 @@
+const express = require('express');
+const cors = require('cors');
+const app = express();
+const path = require("path");
+const bodyParser = require('body-parser');
+const http = require('http');
+
+const model = require('./models/migration.js');
+const apiRoute = require('./app/core/routes/api.route.js');
+
+const corsOptions = {
+    origin: '*',
+    credentials: true,
+};
+const client = require('prom-client');
+const register = new client.Registry();
+const passport = require('passport')
+require('./app/core/services/authGoogle.service.js')
+
+
+
+client.collectDefaultMetrics({ register });
+
+const httpRequestDurationMicroseconds = new client.Histogram({
+    name: 'api_http_request_duration_seconds',
+    help: 'Duration of HTTP requests in seconds',
+    // labelNames: ["method", "route", "code"],
+    // buckets: [0.1, , 0.5, 1, 5, , 10],
+});
+
+
+const customCounter = new client.Counter({
+    name: 'my_custom_counter',
+    help: 'Custom counter for my application',
+});
+
+
+
+
+app.use(cors(corsOptions));
+app.use(bodyParser.json());
+app.use(
+    bodyParser.urlencoded({
+        extended: true,
+    })
+);
+app.use(passport.initialize());
+
+const authGoogle = require('./app/core/routes/authGoogle.route.js')
+app.use('/auth', authGoogle);
+
+app.use('/api', apiRoute);
+
+
+app.use("/api/uploads", express.static(path.join(__dirname, "public", "uploads")));
+
+
+app.use(express.static('public'));
+
+const port = process.env.APP_PORT || 4042;
+const server = http.createServer(app);
+
+server.listen(port, () => {
+    // model.sequelize.sync({ alter: true });
+    console.log(`Server is running on port http://localhost:${port}.`);
+});
\ No newline at end of file
diff --git a/models/api_key.model.js b/models/api_key.model.js
new file mode 100644
index 0000000..97ffa8d
--- /dev/null
+++ b/models/api_key.model.js
@@ -0,0 +1,60 @@
+'use strict'
+const { Model, DataTypes } = require('sequelize')
+
+module.exports = (sequelize) => {
+  class apiKey extends Model {
+    static associate(models) { }
+  }
+
+  apiKey.init(
+    {
+      id: {
+        type: DataTypes.UUID,
+        defaultValue: DataTypes.UUIDV4,
+        primaryKey: true,
+        allowNull: false,
+      },
+      api_key: {
+        type: DataTypes.STRING,
+        allowNull: false,
+      },
+      is_actived: {
+        type: DataTypes.INTEGER,
+        defaultValue: 1,
+      },
+      created_at: {
+        type: DataTypes.DATE,
+        allowNull: true,
+      },
+      updated_at: {
+        type: DataTypes.DATE,
+        allowNull: true,
+      },
+      deleted_at: {
+        type: DataTypes.DATE,
+        allowNull: true,
+      },
+      created_by: {
+        type: DataTypes.UUID,
+        allowNull: true,
+      },
+      updated_by: {
+        type: DataTypes.UUID,
+        allowNull: true,
+      },
+    },
+    {
+      sequelize,
+      modelName: 'apiKey',
+      tableName: 'ref_api_keys',
+      timestamps: true,
+      createdAt: 'created_at',
+      updatedAt: 'updated_at',
+      deletedAt: 'deleted_at',
+    }
+  )
+
+  return apiKey
+}
+
+
diff --git a/models/attendances.model.js b/models/attendances.model.js
new file mode 100644
index 0000000..0c46b28
--- /dev/null
+++ b/models/attendances.model.js
@@ -0,0 +1,90 @@
+"use strict";
+const { Model, DataTypes } = require("sequelize");
+
+module.exports = (sequelize) => {
+  class Attedances extends Model {
+    static associate(models) {
+       this.belongsTo(models.User, {
+        foreignKey: 'user_id',
+        as: 'user'
+       })
+       this.belongsTo(models.Branch, {
+        foreignKey: 'branch_id',
+        as: 'branch',
+        onDelete: 'CASCADE'
+       })
+    }
+  }
+
+  Attedances.init({
+    id: {
+      type: DataTypes.UUID,
+      defaultValue: DataTypes.UUIDV4,
+      allowNull: false,
+      primaryKey: true,
+    },
+    user_id: {
+        type: DataTypes.UUID,
+        allowNull: false,
+    },
+    branch_id: {
+        type: DataTypes.UUID,
+        allowNull: false,
+    },
+    date: {
+        type: DataTypes.DATEONLY,
+        allowNull: false
+    },
+    photo: {
+       type: DataTypes.STRING,
+       allowNull: true
+    },
+    clock_in: {
+        type: DataTypes.DATE,
+    },
+    clock_out: {
+        type: DataTypes.DATE
+    },
+    name: {
+      type: DataTypes.STRING,
+      allowNull: true
+    },
+    lat: {
+      type: DataTypes.DECIMAL(20, 15),
+      allowNull: true,
+    },
+    lng: {
+        type: DataTypes.DECIMAL(20, 15),
+        allowNull: true,
+    },
+    type: {
+        type: DataTypes.ENUM('present', 'sick', 'permission'),
+        defaultValue: 'present'
+    },
+    reason:{
+       type: DataTypes.TEXT
+    },
+
+    created_at: {
+      type: DataTypes.DATE,
+    },
+    updated_at: {
+      type: DataTypes.DATE,
+    },
+    deleted_at: {
+      type: DataTypes.DATE,
+    },
+  },
+    {
+      sequelize,
+      modelName: "Attedances",
+      tableName: "ref_attedances",
+      timestamps: true,
+      createdAt: "created_at",
+      updatedAt: "updated_at",
+      deletedAt: "deleted_at",
+      paranoid: true,
+    });
+
+  return Attedances;
+};
diff --git a/models/branch.model.js b/models/branch.model.js
new file mode 100644
index 0000000..c8231e8
--- /dev/null
+++ b/models/branch.model.js
@@ -0,0 +1,57 @@
+"use strict";
+const { Model, DataTypes } = require("sequelize");
+
+module.exports = (sequelize) => {
+  class Branch extends Model {
+    static associate(models) {
+       this.hasMany(models.User, {
+        foreignKey: 'branch_id',
+        as: 'user',
+        onDelete: 'CASCADE'
+       })
+    }
+  }
+
+  Branch.init({
+    id: {
+      type: DataTypes.UUID,
+      defaultValue: DataTypes.UUIDV4,
+      allowNull: false,
+      primaryKey: true,
+    },
+    name: {
+      type: DataTypes.STRING,
+      allowNull: true
+    },
+    lat: {
+      type: DataTypes.DECIMAL(20, 15),
+      allowNull: false,
+    },
+    lng: {
+        type: DataTypes.DECIMAL(20, 15),
+        allowNull: false,
+    },
+
+    created_at: {
+      type: DataTypes.DATE,
+    },
+    updated_at: {
+      type: DataTypes.DATE,
+    },
+    deleted_at: {
+      type: DataTypes.DATE,
+    },
+  },
+    {
+      sequelize,
+      modelName: "Branch",
+      tableName: "ref_branches",
+      timestamps: true,
+      createdAt: "created_at",
+      updatedAt: "updated_at",
+      deletedAt: "deleted_at",
+      paranoid: true,
+    });
+
+  return Branch;
+};
diff --git a/models/bug_reporting.model.js b/models/bug_reporting.model.js
new file mode 100644
index 0000000..0f5a231
--- /dev/null
+++ b/models/bug_reporting.model.js
@@ -0,0 +1,102 @@
+'use strict'
+const { Model, DataTypes } = require('sequelize')
+
+module.exports = (sequelize) => {
+  class BugReporting extends Model {
+    static associate(models) {
+      // Define associations, if any
+    }
+  }
+
+  BugReporting.init(
+    {
+      id: {
+        type: DataTypes.UUID,
+        defaultValue: DataTypes.UUIDV4,
+        primaryKey: true,
+        allowNull: false,
+      },
+      class: {
+        type: DataTypes.STRING(191),
+        allowNull: true,
+      },
+      file: {
+        type: DataTypes.STRING(191),
+        allowNull: true,
+      },
+      code: {
+        type: DataTypes.INTEGER,
+        allowNull: true,
+      },
+      status_code: {
+        type: DataTypes.INTEGER,
+        allowNull: true,
+        defaultValue: 0,
+      },
+      line: {
+        type: DataTypes.INTEGER,
+        allowNull: true,
+      },
+      message: {
+        type: DataTypes.TEXT,
+        allowNull: true,
+      },
+      trace: {
+        type: DataTypes.TEXT,
+        allowNull: true,
+      },
+      user_id: {
+        type: DataTypes.UUID,
+        defaultValue: null,
+        allowNull: true,
+      },
+      data: {
+        type: DataTypes.TEXT,
+        defaultValue: null,
+        allowNull: true,
+      },
+      url: {
+        type: DataTypes.TEXT,
+        defaultValue: null,
+        allowNull: true,
+      },
+      method: {
+        type: DataTypes.STRING(191),
+        defaultValue: null,
+        allowNull: true,
+      },
+      ip: {
+        type: DataTypes.STRING(191),
+        defaultValue: null,
+        allowNull: true,
+      },
+      updated_at: {
+        type: DataTypes.DATE,
+        allowNull: true,
+      },
+      deleted_at: {
+        type: DataTypes.DATE,
+        allowNull: true,
+      },
+      created_by: {
+        type: DataTypes.UUID,
+        allowNull: true,
+      },
+      updated_by: {
+        type: DataTypes.UUID,
+        allowNull: true,
+      }
+    },
+    {
+      sequelize,
+      modelName: 'BugReporting',
+      tableName: 'core_bug_reportings',
+      timestamps: true,
+      createdAt: 'created_at',
+      updatedAt: 'updated_at',
+      deletedAt: 'deleted_at',
+    }
+  )
+
+  return BugReporting
+}
diff --git a/models/migration.js b/models/migration.js
new file mode 100644
index 0000000..bf90279
--- /dev/null
+++ b/models/migration.js
@@ -0,0 +1,70 @@
+"use strict";
+
+const fs = require("fs");
+const path = require("path");
+const Sequelize = require("sequelize");
+const process = require("process");
+const basename = path.basename(__filename);
+let config;
+
+try {
+  // Prefer legacy app-specific configuration when available
+  // eslint-disable-next-line import/no-dynamic-require, global-require
+  config = require("../app/config/db.config.js");
+} catch (error) {
+  if (error.code !== "MODULE_NOT_FOUND") {
+    throw error;
+  }
+  // Fallback to the shared config loader
+  // eslint-disable-next-line import/no-dynamic-require, global-require
+  const appConfig = require("../src/config/config.js");
+  const dbConfig = appConfig.db || {};
+  config = {
+    database: dbConfig.name,
+    username: dbConfig.username,
+    password: dbConfig.password,
+    host: dbConfig.host,
+    port: dbConfig.port,
+    dialect: dbConfig.dialect,
+    logging: false
+  };
+}
+require("dotenv").config();
+
+const db = {};
+
+const sequelize = new Sequelize(
+  config.database,
+  config.username,
+  config.password,
+  config
+);
+
+fs.readdirSync(__dirname)
+  .filter((file) => {
+    return (
+      file.indexOf(".") !== 0 &&
+      file !== basename &&
+      file.slice(-3) === ".js" &&
+      file.indexOf(".test.js") === -1
+    );
+  })
+  .forEach((file) => {
+    const model = require(path.join(__dirname, file))(
+      sequelize,
+      Sequelize.DataTypes
+    );
+    db[model.name] = model;
+  });
+
+Object.keys(db).forEach((modelName) => {
+  if (db[modelName].associate) {
+    db[modelName].associate(db);
+  }
+});
+
+db.sequelize = sequelize;
+// db.Sequelize = Sequelize;
+db.Op = Sequelize.Op;
+
+module.exports = db;
diff --git a/models/user.model.js b/models/user.model.js
new file mode 100644
index 0000000..b78577d
--- /dev/null
+++ b/models/user.model.js
@@ -0,0 +1,116 @@
+"use strict";
+const { Model, DataTypes } = require("sequelize");
+
+module.exports = (sequelize) => {
+  class User extends Model {
+    static associate(models) {
+      this.belongsTo(models.Branch, {foreignKey: 'branch_id', as: 'branch', onDelete: 'CASCADE'})
+    }
+  }
+
+  User.init({
+    id: {
+      type: DataTypes.UUID,
+      defaultValue: DataTypes.UUIDV4,
+      allowNull: false,
+      primaryKey: true,
+    },
+    branch_id: {
+      type: DataTypes.UUID,
+      defaultValue: DataTypes.UUIDV4,
+      allowNull: false,
+    },
+    name: {
+      type: DataTypes.STRING,
+      allowNull: true
+    },
+    role: {
+      type: DataTypes.ENUM('admin', 'user'),
+      allowNull: true,
+      defaultValue: 'user'
+    },
+    email: {
+      type: DataTypes.STRING,
+      unique: true,
+      allowNull: false,
+      validate: {
+        isEmail: true,
+      },
+    },
+
+    avatar_url: {
+      type: DataTypes.STRING,
+      allowNull: true
+    },
+
+    phone: {
+      type: DataTypes.STRING,
+      allowNull: true,
+      unique: true,
+      validate: {
+        is: {
+          args: /^\+?[0-9\s\-]{6,20}$/,
+          msg: 'Format nomor telepon tidak valid',
+        },
+        len: {
+          args: [6, 15],
+          msg: "Nomor telepon maksimal 15 karakter"
+        }
+      }
+    },
+    password: {
+      type: DataTypes.STRING,
+      allowNull: true
+    },
+
+
+    provider_id: {
+      type: DataTypes.STRING,
+      allowNull: true
+    },
+    login_via: {
+      type: DataTypes.STRING,
+      defaultValue: 'EMAIL',
+    },
+    google_id: {
+      type: DataTypes.STRING,
+    },
+    is_suspended: {
+      type: DataTypes.BOOLEAN,
+      allowNull: false,
+      defaultValue: false
+    },
+    birth: {
+      type: DataTypes.DATE,
+      allowNull: true
+    },
+    last_login: {
+      type: DataTypes.DATE,
+    },
+    is_first_login: {
+      type: DataTypes.BOOLEAN,
+      defaultValue: true
+    },
+    created_at: {
+      type: DataTypes.DATE,
+    },
+    updated_at: {
+      type: DataTypes.DATE,
+    },
+    deleted_at: {
+      type: DataTypes.DATE,
+    },
+  },
+    {
+      sequelize,
+      modelName: "User",
+      tableName: "core_users",
+      timestamps: true,
+      createdAt: "created_at",
+      updatedAt: "updated_at",
+      deletedAt: "deleted_at",
+      paranoid: true,
+    });
+
+  return User;
+};
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..e86a43d
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,3501 @@
+{
+  "name": "absensi",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "absensi",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "axios": "^1.12.2",
+        "bcrypt": "^6.0.0",
+        "body-parser": "^2.2.0",
+        "cors": "^2.8.5",
+        "crypto": "^1.0.1",
+        "dotenv": "^17.2.3",
+        "express": "^5.1.0",
+        "google-auth-library": "^10.4.0",
+        "handlebars": "^4.7.8",
+        "jsonwebtoken": "^9.0.2",
+        "moment": "^2.30.1",
+        "moment-timezone": "^0.6.0",
+        "multer": "^2.0.2",
+        "mysql2": "^3.15.2",
+        "node-fetch": "^3.3.2",
+        "nodemailer": "^7.0.9",
+        "nodemailer-html-to-text": "^3.2.0",
+        "nodemon": "^3.1.10",
+        "passport": "^0.7.0",
+        "prom-client": "^15.1.3",
+        "sequelize": "^6.37.7",
+        "sequelize-cli": "^6.6.3"
+      }
+    },
+    "node_modules/@isaacs/cliui": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+      "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^5.1.2",
+        "string-width-cjs": "npm:string-width@^4.2.0",
+        "strip-ansi": "^7.0.1",
+        "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+        "wrap-ansi": "^8.1.0",
+        "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@one-ini/wasm": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
+      "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==",
+      "license": "MIT"
+    },
+    "node_modules/@opentelemetry/api": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
+      "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/@pkgjs/parseargs": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+      "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@types/debug": {
+      "version": "4.1.12",
+      "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
+      "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/ms": "*"
+      }
+    },
+    "node_modules/@types/ms": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+      "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+      "license": "MIT"
+    },
+    "node_modules/@types/node": {
+      "version": "24.7.2",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz",
+      "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==",
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~7.14.0"
+      }
+    },
+    "node_modules/@types/validator": {
+      "version": "13.15.3",
+      "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz",
+      "integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==",
+      "license": "MIT"
+    },
+    "node_modules/abbrev": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
+      "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
+      "license": "ISC",
+      "engines": {
+        "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+      }
+    },
+    "node_modules/accepts": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+      "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-types": "^3.0.0",
+        "negotiator": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/accepts/node_modules/mime-db": {
+      "version": "1.54.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+      "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/accepts/node_modules/mime-types": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+      "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "^1.54.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/agent-base": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+      "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+      "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "6.2.3",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+      "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/anymatch": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "license": "ISC",
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/append-field": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
+      "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
+      "license": "MIT"
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/at-least-node": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+      "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">= 4.0.0"
+      }
+    },
+    "node_modules/aws-ssl-profiles": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
+      "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/axios": {
+      "version": "1.12.2",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
+      "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.4",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "license": "MIT"
+    },
+    "node_modules/base64-js": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/bcrypt": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
+      "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "dependencies": {
+        "node-addon-api": "^8.3.0",
+        "node-gyp-build": "^4.8.4"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/bignumber.js": {
+      "version": "9.3.1",
+      "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
+      "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/binary-extensions": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+      "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/bintrees": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz",
+      "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==",
+      "license": "MIT"
+    },
+    "node_modules/bluebird": {
+      "version": "3.7.2",
+      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+      "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
+      "license": "MIT"
+    },
+    "node_modules/body-parser": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
+      "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "^3.1.2",
+        "content-type": "^1.0.5",
+        "debug": "^4.4.0",
+        "http-errors": "^2.0.0",
+        "iconv-lite": "^0.6.3",
+        "on-finished": "^2.4.1",
+        "qs": "^6.14.0",
+        "raw-body": "^3.0.0",
+        "type-is": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/brace-expansion": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+      "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "license": "MIT",
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/buffer-equal-constant-time": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+      "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/buffer-from": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+      "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+      "license": "MIT"
+    },
+    "node_modules/busboy": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
+      "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+      "dependencies": {
+        "streamsearch": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=10.16.0"
+      }
+    },
+    "node_modules/bytes": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/chokidar": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+      "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+      "license": "MIT",
+      "dependencies": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      },
+      "engines": {
+        "node": ">= 8.10.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/cliui": {
+      "version": "7.0.4",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+      "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.0",
+        "wrap-ansi": "^7.0.0"
+      }
+    },
+    "node_modules/cliui/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/cliui/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/cliui/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "license": "MIT"
+    },
+    "node_modules/cliui/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/cliui/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/cliui/node_modules/wrap-ansi": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "license": "MIT"
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/commander": {
+      "version": "10.0.1",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
+      "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+      "license": "MIT"
+    },
+    "node_modules/concat-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
+      "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
+      "engines": [
+        "node >= 6.0"
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "buffer-from": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^3.0.2",
+        "typedarray": "^0.0.6"
+      }
+    },
+    "node_modules/config-chain": {
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
+      "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ini": "^1.3.4",
+        "proto-list": "~1.2.1"
+      }
+    },
+    "node_modules/content-disposition": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
+      "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "5.2.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/content-type": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie-signature": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+      "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.6.0"
+      }
+    },
+    "node_modules/cors": {
+      "version": "2.8.5",
+      "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+      "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+      "license": "MIT",
+      "dependencies": {
+        "object-assign": "^4",
+        "vary": "^1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/crypto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
+      "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
+      "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.",
+      "license": "ISC"
+    },
+    "node_modules/data-uri-to-buffer": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
+      "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/deepmerge": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+      "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/denque": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
+      "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/depd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+      "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/dom-serializer": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
+      "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
+      "license": "MIT",
+      "dependencies": {
+        "domelementtype": "^2.0.1",
+        "domhandler": "^4.2.0",
+        "entities": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+      }
+    },
+    "node_modules/domelementtype": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+      "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fb55"
+        }
+      ],
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/domhandler": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
+      "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "domelementtype": "^2.2.0"
+      },
+      "engines": {
+        "node": ">= 4"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/domhandler?sponsor=1"
+      }
+    },
+    "node_modules/domutils": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
+      "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "dom-serializer": "^1.0.1",
+        "domelementtype": "^2.2.0",
+        "domhandler": "^4.2.0"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/domutils?sponsor=1"
+      }
+    },
+    "node_modules/dotenv": {
+      "version": "17.2.3",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
+      "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://dotenvx.com"
+      }
+    },
+    "node_modules/dottie": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz",
+      "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==",
+      "license": "MIT"
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/eastasianwidth": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+      "license": "MIT"
+    },
+    "node_modules/ecdsa-sig-formatter": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+      "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/editorconfig": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz",
+      "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@one-ini/wasm": "0.1.1",
+        "commander": "^10.0.0",
+        "minimatch": "9.0.1",
+        "semver": "^7.5.3"
+      },
+      "bin": {
+        "editorconfig": "bin/editorconfig"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/editorconfig/node_modules/brace-expansion": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+      "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/editorconfig/node_modules/minimatch": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
+      "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+      "license": "MIT"
+    },
+    "node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+      "license": "MIT"
+    },
+    "node_modules/encodeurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+      "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/entities": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
+      "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
+      "license": "BSD-2-Clause",
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+      "license": "MIT"
+    },
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/express": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
+      "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "^2.0.0",
+        "body-parser": "^2.2.0",
+        "content-disposition": "^1.0.0",
+        "content-type": "^1.0.5",
+        "cookie": "^0.7.1",
+        "cookie-signature": "^1.2.1",
+        "debug": "^4.4.0",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "etag": "^1.8.1",
+        "finalhandler": "^2.1.0",
+        "fresh": "^2.0.0",
+        "http-errors": "^2.0.0",
+        "merge-descriptors": "^2.0.0",
+        "mime-types": "^3.0.0",
+        "on-finished": "^2.4.1",
+        "once": "^1.4.0",
+        "parseurl": "^1.3.3",
+        "proxy-addr": "^2.0.7",
+        "qs": "^6.14.0",
+        "range-parser": "^1.2.1",
+        "router": "^2.2.0",
+        "send": "^1.1.0",
+        "serve-static": "^2.2.0",
+        "statuses": "^2.0.1",
+        "type-is": "^2.0.1",
+        "vary": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/express/node_modules/mime-db": {
+      "version": "1.54.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+      "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/express/node_modules/mime-types": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+      "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "^1.54.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/extend": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+      "license": "MIT"
+    },
+    "node_modules/fetch-blob": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
+      "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/jimmywarting"
+        },
+        {
+          "type": "paypal",
+          "url": "https://paypal.me/jimmywarting"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "node-domexception": "^1.0.0",
+        "web-streams-polyfill": "^3.0.3"
+      },
+      "engines": {
+        "node": "^12.20 || >= 14.13"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/finalhandler": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
+      "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.0",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "on-finished": "^2.4.1",
+        "parseurl": "^1.3.3",
+        "statuses": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.11",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/foreground-child": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+      "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+      "license": "ISC",
+      "dependencies": {
+        "cross-spawn": "^7.0.6",
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+      "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/formdata-polyfill": {
+      "version": "4.0.10",
+      "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
+      "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+      "license": "MIT",
+      "dependencies": {
+        "fetch-blob": "^3.1.2"
+      },
+      "engines": {
+        "node": ">=12.20.0"
+      }
+    },
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fresh": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+      "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/fs-extra": {
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+      "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+      "license": "MIT",
+      "dependencies": {
+        "at-least-node": "^1.0.0",
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gaxios": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.2.tgz",
+      "integrity": "sha512-/Szrn8nr+2TsQT1Gp8iIe/BEytJmbyfrbFh419DfGQSkEgNEhbPi7JRJuughjkTzPWgU9gBQf5AVu3DbHt0OXA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "extend": "^3.0.2",
+        "https-proxy-agent": "^7.0.1",
+        "node-fetch": "^3.3.2"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/gcp-metadata": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz",
+      "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "gaxios": "^7.0.0",
+        "google-logging-utils": "^1.0.0",
+        "json-bigint": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/generate-function": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
+      "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
+      "license": "MIT",
+      "dependencies": {
+        "is-property": "^1.0.2"
+      }
+    },
+    "node_modules/get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "license": "ISC",
+      "engines": {
+        "node": "6.* || 8.* || >= 10.*"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/glob": {
+      "version": "10.4.5",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+      "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+      "license": "ISC",
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^3.1.2",
+        "minimatch": "^9.0.4",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^1.11.1"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/glob/node_modules/brace-expansion": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+      "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/glob/node_modules/minimatch": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/google-auth-library": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.4.0.tgz",
+      "integrity": "sha512-CmIrSy1bqMQUsPmA9+hcSbAXL80cFhu40cGMUjCaLpNKVzzvi+0uAHq8GNZxkoGYIsTX4ZQ7e4aInAqWxgn4fg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "base64-js": "^1.3.0",
+        "ecdsa-sig-formatter": "^1.0.11",
+        "gaxios": "^7.0.0",
+        "gcp-metadata": "^7.0.0",
+        "google-logging-utils": "^1.0.0",
+        "gtoken": "^8.0.0",
+        "jws": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/google-auth-library/node_modules/jwa": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+      "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+      "license": "MIT",
+      "dependencies": {
+        "buffer-equal-constant-time": "^1.0.1",
+        "ecdsa-sig-formatter": "1.0.11",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/google-auth-library/node_modules/jws": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
+      "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
+      "license": "MIT",
+      "dependencies": {
+        "jwa": "^2.0.0",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/google-logging-utils": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.1.tgz",
+      "integrity": "sha512-rcX58I7nqpu4mbKztFeOAObbomBbHU2oIb/d3tJfF3dizGSApqtSwYJigGCooHdnMyQBIw8BrWyK96w3YXgr6A==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+      "license": "ISC"
+    },
+    "node_modules/gtoken": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
+      "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==",
+      "license": "MIT",
+      "dependencies": {
+        "gaxios": "^7.0.0",
+        "jws": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/gtoken/node_modules/jwa": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+      "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+      "license": "MIT",
+      "dependencies": {
+        "buffer-equal-constant-time": "^1.0.1",
+        "ecdsa-sig-formatter": "1.0.11",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/gtoken/node_modules/jws": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
+      "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
+      "license": "MIT",
+      "dependencies": {
+        "jwa": "^2.0.0",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/handlebars": {
+      "version": "4.7.8",
+      "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
+      "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
+      "license": "MIT",
+      "dependencies": {
+        "minimist": "^1.2.5",
+        "neo-async": "^2.6.2",
+        "source-map": "^0.6.1",
+        "wordwrap": "^1.0.0"
+      },
+      "bin": {
+        "handlebars": "bin/handlebars"
+      },
+      "engines": {
+        "node": ">=0.4.7"
+      },
+      "optionalDependencies": {
+        "uglify-js": "^3.1.4"
+      }
+    },
+    "node_modules/has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/he": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+      "license": "MIT",
+      "bin": {
+        "he": "bin/he"
+      }
+    },
+    "node_modules/html-to-text": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-7.1.1.tgz",
+      "integrity": "sha512-c9QWysrfnRZevVpS8MlE7PyOdSuIOjg8Bt8ZE10jMU/BEngA6j3llj4GRfAmtQzcd1FjKE0sWu5IHXRUH9YxIQ==",
+      "license": "MIT",
+      "dependencies": {
+        "deepmerge": "^4.2.2",
+        "he": "^1.2.0",
+        "htmlparser2": "^6.1.0",
+        "minimist": "^1.2.5"
+      },
+      "bin": {
+        "html-to-text": "bin/cli.js"
+      },
+      "engines": {
+        "node": ">=10.23.2"
+      }
+    },
+    "node_modules/htmlparser2": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
+      "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==",
+      "funding": [
+        "https://github.com/fb55/htmlparser2?sponsor=1",
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fb55"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "domelementtype": "^2.0.1",
+        "domhandler": "^4.0.0",
+        "domutils": "^2.5.2",
+        "entities": "^2.0.0"
+      }
+    },
+    "node_modules/http-errors": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+      "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "depd": "2.0.0",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": "2.0.1",
+        "toidentifier": "1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+      "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/ignore-by-default": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+      "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+      "license": "ISC"
+    },
+    "node_modules/inflection": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz",
+      "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==",
+      "engines": [
+        "node >= 0.4.0"
+      ],
+      "license": "MIT"
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/ini": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+      "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+      "license": "ISC"
+    },
+    "node_modules/ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "license": "MIT",
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-core-module": {
+      "version": "2.16.1",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+      "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+      "license": "MIT",
+      "dependencies": {
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "license": "MIT",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/is-promise": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+      "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+      "license": "MIT"
+    },
+    "node_modules/is-property": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
+      "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
+      "license": "MIT"
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "license": "ISC"
+    },
+    "node_modules/jackspeak": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+      "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      },
+      "optionalDependencies": {
+        "@pkgjs/parseargs": "^0.11.0"
+      }
+    },
+    "node_modules/js-beautify": {
+      "version": "1.15.4",
+      "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz",
+      "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==",
+      "license": "MIT",
+      "dependencies": {
+        "config-chain": "^1.1.13",
+        "editorconfig": "^1.0.4",
+        "glob": "^10.4.2",
+        "js-cookie": "^3.0.5",
+        "nopt": "^7.2.1"
+      },
+      "bin": {
+        "css-beautify": "js/bin/css-beautify.js",
+        "html-beautify": "js/bin/html-beautify.js",
+        "js-beautify": "js/bin/js-beautify.js"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/js-cookie": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
+      "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/json-bigint": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
+      "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "bignumber.js": "^9.0.0"
+      }
+    },
+    "node_modules/jsonfile": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+      "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+      "license": "MIT",
+      "dependencies": {
+        "universalify": "^2.0.0"
+      },
+      "optionalDependencies": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "node_modules/jsonwebtoken": {
+      "version": "9.0.2",
+      "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
+      "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
+      "license": "MIT",
+      "dependencies": {
+        "jws": "^3.2.2",
+        "lodash.includes": "^4.3.0",
+        "lodash.isboolean": "^3.0.3",
+        "lodash.isinteger": "^4.0.4",
+        "lodash.isnumber": "^3.0.3",
+        "lodash.isplainobject": "^4.0.6",
+        "lodash.isstring": "^4.0.1",
+        "lodash.once": "^4.0.0",
+        "ms": "^2.1.1",
+        "semver": "^7.5.4"
+      },
+      "engines": {
+        "node": ">=12",
+        "npm": ">=6"
+      }
+    },
+    "node_modules/jwa": {
+      "version": "1.4.2",
+      "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
+      "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
+      "license": "MIT",
+      "dependencies": {
+        "buffer-equal-constant-time": "^1.0.1",
+        "ecdsa-sig-formatter": "1.0.11",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/jws": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+      "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+      "license": "MIT",
+      "dependencies": {
+        "jwa": "^1.4.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.includes": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+      "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isboolean": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+      "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isinteger": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+      "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isnumber": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+      "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isplainobject": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+      "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isstring": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+      "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.once": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+      "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+      "license": "MIT"
+    },
+    "node_modules/long": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+      "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/lru-cache": {
+      "version": "7.18.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+      "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/lru.min": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz",
+      "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==",
+      "license": "MIT",
+      "engines": {
+        "bun": ">=1.0.0",
+        "deno": ">=1.30.0",
+        "node": ">=8.0.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wellwelwel"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/media-typer": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+      "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/merge-descriptors": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+      "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/minipass": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+      "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/mkdirp": {
+      "version": "0.5.6",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+      "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+      "license": "MIT",
+      "dependencies": {
+        "minimist": "^1.2.6"
+      },
+      "bin": {
+        "mkdirp": "bin/cmd.js"
+      }
+    },
+    "node_modules/moment": {
+      "version": "2.30.1",
+      "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
+      "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/moment-timezone": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.6.0.tgz",
+      "integrity": "sha512-ldA5lRNm3iJCWZcBCab4pnNL3HSZYXVb/3TYr75/1WCTWYuTqYUb5f/S384pncYjJ88lbO8Z4uPDvmoluHJc8Q==",
+      "license": "MIT",
+      "dependencies": {
+        "moment": "^2.29.4"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/multer": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz",
+      "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==",
+      "license": "MIT",
+      "dependencies": {
+        "append-field": "^1.0.0",
+        "busboy": "^1.6.0",
+        "concat-stream": "^2.0.0",
+        "mkdirp": "^0.5.6",
+        "object-assign": "^4.1.1",
+        "type-is": "^1.6.18",
+        "xtend": "^4.0.2"
+      },
+      "engines": {
+        "node": ">= 10.16.0"
+      }
+    },
+    "node_modules/multer/node_modules/media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/multer/node_modules/type-is": {
+      "version": "1.6.18",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "license": "MIT",
+      "dependencies": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mysql2": {
+      "version": "3.15.2",
+      "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.2.tgz",
+      "integrity": "sha512-kFm5+jbwR5mC+lo+3Cy46eHiykWSpUtTLOH3GE+AR7GeLq8PgfJcvpMiyVWk9/O53DjQsqm6a3VOOfq7gYWFRg==",
+      "license": "MIT",
+      "dependencies": {
+        "aws-ssl-profiles": "^1.1.1",
+        "denque": "^2.1.0",
+        "generate-function": "^2.3.1",
+        "iconv-lite": "^0.7.0",
+        "long": "^5.2.1",
+        "lru.min": "^1.0.0",
+        "named-placeholders": "^1.1.3",
+        "seq-queue": "^0.0.5",
+        "sqlstring": "^2.3.2"
+      },
+      "engines": {
+        "node": ">= 8.0"
+      }
+    },
+    "node_modules/mysql2/node_modules/iconv-lite": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
+      "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/named-placeholders": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz",
+      "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==",
+      "license": "MIT",
+      "dependencies": {
+        "lru-cache": "^7.14.1"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/negotiator": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+      "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/neo-async": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+      "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+      "license": "MIT"
+    },
+    "node_modules/node-addon-api": {
+      "version": "8.5.0",
+      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
+      "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
+      "license": "MIT",
+      "engines": {
+        "node": "^18 || ^20 || >= 21"
+      }
+    },
+    "node_modules/node-domexception": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+      "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+      "deprecated": "Use your platform's native DOMException instead",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/jimmywarting"
+        },
+        {
+          "type": "github",
+          "url": "https://paypal.me/jimmywarting"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.5.0"
+      }
+    },
+    "node_modules/node-fetch": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
+      "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
+      "license": "MIT",
+      "dependencies": {
+        "data-uri-to-buffer": "^4.0.0",
+        "fetch-blob": "^3.1.4",
+        "formdata-polyfill": "^4.0.10"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/node-fetch"
+      }
+    },
+    "node_modules/node-gyp-build": {
+      "version": "4.8.4",
+      "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
+      "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
+      "license": "MIT",
+      "bin": {
+        "node-gyp-build": "bin.js",
+        "node-gyp-build-optional": "optional.js",
+        "node-gyp-build-test": "build-test.js"
+      }
+    },
+    "node_modules/nodemailer": {
+      "version": "7.0.9",
+      "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.9.tgz",
+      "integrity": "sha512-9/Qm0qXIByEP8lEV2qOqcAW7bRpL8CR9jcTwk3NBnHJNmP9fIJ86g2fgmIXqHY+nj55ZEMwWqYAT2QTDpRUYiQ==",
+      "license": "MIT-0",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/nodemailer-html-to-text": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/nodemailer-html-to-text/-/nodemailer-html-to-text-3.2.0.tgz",
+      "integrity": "sha512-RJUC6640QV1PzTHHapOrc6IzrAJUZtk2BdVdINZ9VTLm+mcQNyBO9LYyhrnufkzqiD9l8hPLJ97rSyK4WanPNg==",
+      "license": "MIT",
+      "dependencies": {
+        "html-to-text": "7.1.1"
+      },
+      "engines": {
+        "node": ">= 10.23.0"
+      }
+    },
+    "node_modules/nodemon": {
+      "version": "3.1.10",
+      "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
+      "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==",
+      "license": "MIT",
+      "dependencies": {
+        "chokidar": "^3.5.2",
+        "debug": "^4",
+        "ignore-by-default": "^1.0.1",
+        "minimatch": "^3.1.2",
+        "pstree.remy": "^1.1.8",
+        "semver": "^7.5.3",
+        "simple-update-notifier": "^2.0.0",
+        "supports-color": "^5.5.0",
+        "touch": "^3.1.0",
+        "undefsafe": "^2.0.5"
+      },
+      "bin": {
+        "nodemon": "bin/nodemon.js"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/nodemon"
+      }
+    },
+    "node_modules/nopt": {
+      "version": "7.2.1",
+      "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz",
+      "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==",
+      "license": "ISC",
+      "dependencies": {
+        "abbrev": "^2.0.0"
+      },
+      "bin": {
+        "nopt": "bin/nopt.js"
+      },
+      "engines": {
+        "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+      }
+    },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/on-finished": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+      "license": "MIT",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/package-json-from-dist": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+      "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+      "license": "BlueOak-1.0.0"
+    },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/passport": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
+      "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "passport-strategy": "1.x.x",
+        "pause": "0.0.1",
+        "utils-merge": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/jaredhanson"
+      }
+    },
+    "node_modules/passport-strategy": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
+      "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+      "license": "MIT"
+    },
+    "node_modules/path-scurry": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+      "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^10.2.0",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/path-scurry/node_modules/lru-cache": {
+      "version": "10.4.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+      "license": "ISC"
+    },
+    "node_modules/path-to-regexp": {
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
+      "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/pause": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
+      "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
+    },
+    "node_modules/pg-connection-string": {
+      "version": "2.9.1",
+      "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
+      "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==",
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/prom-client": {
+      "version": "15.1.3",
+      "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz",
+      "integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/api": "^1.4.0",
+        "tdigest": "^0.1.1"
+      },
+      "engines": {
+        "node": "^16 || ^18 || >=20"
+      }
+    },
+    "node_modules/proto-list": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
+      "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
+      "license": "ISC"
+    },
+    "node_modules/proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "license": "MIT",
+      "dependencies": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+      "license": "MIT"
+    },
+    "node_modules/pstree.remy": {
+      "version": "1.1.8",
+      "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+      "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+      "license": "MIT"
+    },
+    "node_modules/qs": {
+      "version": "6.14.0",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
+      "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/raw-body": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
+      "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "3.1.2",
+        "http-errors": "2.0.0",
+        "iconv-lite": "0.7.0",
+        "unpipe": "1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/raw-body/node_modules/iconv-lite": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
+      "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/readable-stream": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/readdirp": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "license": "MIT",
+      "dependencies": {
+        "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
+      }
+    },
+    "node_modules/require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/resolve": {
+      "version": "1.22.10",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
+      "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+      "license": "MIT",
+      "dependencies": {
+        "is-core-module": "^2.16.0",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/retry-as-promised": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz",
+      "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==",
+      "license": "MIT"
+    },
+    "node_modules/router": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+      "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.0",
+        "depd": "^2.0.0",
+        "is-promise": "^4.0.0",
+        "parseurl": "^1.3.3",
+        "path-to-regexp": "^8.0.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "license": "MIT"
+    },
+    "node_modules/semver": {
+      "version": "7.7.3",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+      "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/send": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
+      "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.3.5",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "etag": "^1.8.1",
+        "fresh": "^2.0.0",
+        "http-errors": "^2.0.0",
+        "mime-types": "^3.0.1",
+        "ms": "^2.1.3",
+        "on-finished": "^2.4.1",
+        "range-parser": "^1.2.1",
+        "statuses": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/send/node_modules/mime-db": {
+      "version": "1.54.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+      "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/send/node_modules/mime-types": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+      "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "^1.54.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/seq-queue": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
+      "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
+    },
+    "node_modules/sequelize": {
+      "version": "6.37.7",
+      "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz",
+      "integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/sequelize"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@types/debug": "^4.1.8",
+        "@types/validator": "^13.7.17",
+        "debug": "^4.3.4",
+        "dottie": "^2.0.6",
+        "inflection": "^1.13.4",
+        "lodash": "^4.17.21",
+        "moment": "^2.29.4",
+        "moment-timezone": "^0.5.43",
+        "pg-connection-string": "^2.6.1",
+        "retry-as-promised": "^7.0.4",
+        "semver": "^7.5.4",
+        "sequelize-pool": "^7.1.0",
+        "toposort-class": "^1.0.1",
+        "uuid": "^8.3.2",
+        "validator": "^13.9.0",
+        "wkx": "^0.5.0"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependenciesMeta": {
+        "ibm_db": {
+          "optional": true
+        },
+        "mariadb": {
+          "optional": true
+        },
+        "mysql2": {
+          "optional": true
+        },
+        "oracledb": {
+          "optional": true
+        },
+        "pg": {
+          "optional": true
+        },
+        "pg-hstore": {
+          "optional": true
+        },
+        "snowflake-sdk": {
+          "optional": true
+        },
+        "sqlite3": {
+          "optional": true
+        },
+        "tedious": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/sequelize-cli": {
+      "version": "6.6.3",
+      "resolved": "https://registry.npmjs.org/sequelize-cli/-/sequelize-cli-6.6.3.tgz",
+      "integrity": "sha512-1YYPrcSRt/bpMDDSKM5ubY1mnJ2TEwIaGZcqITw4hLtGtE64nIqaBnLtMvH8VKHg6FbWpXTiFNc2mS/BtQCXZw==",
+      "license": "MIT",
+      "dependencies": {
+        "fs-extra": "^9.1.0",
+        "js-beautify": "1.15.4",
+        "lodash": "^4.17.21",
+        "picocolors": "^1.1.1",
+        "resolve": "^1.22.1",
+        "umzug": "^2.3.0",
+        "yargs": "^16.2.0"
+      },
+      "bin": {
+        "sequelize": "lib/sequelize",
+        "sequelize-cli": "lib/sequelize"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/sequelize-pool": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz",
+      "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10.0.0"
+      }
+    },
+    "node_modules/sequelize/node_modules/moment-timezone": {
+      "version": "0.5.48",
+      "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
+      "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
+      "license": "MIT",
+      "dependencies": {
+        "moment": "^2.29.4"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/serve-static": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
+      "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
+      "license": "MIT",
+      "dependencies": {
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "parseurl": "^1.3.3",
+        "send": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+      "license": "ISC"
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/side-channel": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+      "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3",
+        "side-channel-list": "^1.0.0",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+      "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/simple-update-notifier": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+      "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+      "license": "MIT",
+      "dependencies": {
+        "semver": "^7.5.3"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/sqlstring": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
+      "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/statuses": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+      "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/streamsearch": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
+      "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/string_decoder": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "~5.2.0"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+      "license": "MIT",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/string-width-cjs": {
+      "name": "string-width",
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "license": "MIT"
+    },
+    "node_modules/string-width-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+      "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/strip-ansi-cjs": {
+      "name": "strip-ansi",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/tdigest": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz",
+      "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==",
+      "license": "MIT",
+      "dependencies": {
+        "bintrees": "1.0.2"
+      }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "license": "MIT",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
+    "node_modules/toposort-class": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz",
+      "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==",
+      "license": "MIT"
+    },
+    "node_modules/touch": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+      "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+      "license": "ISC",
+      "bin": {
+        "nodetouch": "bin/nodetouch.js"
+      }
+    },
+    "node_modules/type-is": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+      "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+      "license": "MIT",
+      "dependencies": {
+        "content-type": "^1.0.5",
+        "media-typer": "^1.1.0",
+        "mime-types": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/type-is/node_modules/mime-db": {
+      "version": "1.54.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+      "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/type-is/node_modules/mime-types": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+      "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "^1.54.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/typedarray": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+      "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
+      "license": "MIT"
+    },
+    "node_modules/uglify-js": {
+      "version": "3.19.3",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
+      "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
+      "license": "BSD-2-Clause",
+      "optional": true,
+      "bin": {
+        "uglifyjs": "bin/uglifyjs"
+      },
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/umzug": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz",
+      "integrity": "sha512-Z274K+e8goZK8QJxmbRPhl89HPO1K+ORFtm6rySPhFKfKc5GHhqdzD0SGhSWHkzoXasqJuItdhorSvY7/Cgflw==",
+      "license": "MIT",
+      "dependencies": {
+        "bluebird": "^3.7.2"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/undefsafe": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+      "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+      "license": "MIT"
+    },
+    "node_modules/undici-types": {
+      "version": "7.14.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
+      "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==",
+      "license": "MIT"
+    },
+    "node_modules/universalify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+      "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10.0.0"
+      }
+    },
+    "node_modules/unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "license": "MIT"
+    },
+    "node_modules/utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
+    "node_modules/uuid": {
+      "version": "8.3.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
+    "node_modules/validator": {
+      "version": "13.15.15",
+      "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
+      "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/web-streams-polyfill": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
+      "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/wkx": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz",
+      "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/wordwrap": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+      "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
+      "license": "MIT"
+    },
+    "node_modules/wrap-ansi": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+      "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.1.0",
+        "string-width": "^5.0.1",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs": {
+      "name": "wrap-ansi",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "license": "MIT"
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "license": "ISC"
+    },
+    "node_modules/xtend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4"
+      }
+    },
+    "node_modules/y18n": {
+      "version": "5.0.8",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+      "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/yargs": {
+      "version": "16.2.0",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
+      "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+      "license": "MIT",
+      "dependencies": {
+        "cliui": "^7.0.2",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.0",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^20.2.2"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/yargs-parser": {
+      "version": "20.2.9",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
+      "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/yargs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/yargs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "license": "MIT"
+    },
+    "node_modules/yargs/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/yargs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    }
+  }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..1d1c387
--- /dev/null
+++ b/package.json
@@ -0,0 +1,36 @@
+{
+  "name": "absensi",
+  "version": "1.0.0",
+  "main": "index.js",
+  "scripts": {
+    "start": "nodemon app.js"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "description": "",
+  "dependencies": {
+    "axios": "^1.12.2",
+    "bcrypt": "^6.0.0",
+    "body-parser": "^2.2.0",
+    "cors": "^2.8.5",
+    "crypto": "^1.0.1",
+    "dotenv": "^17.2.3",
+    "express": "^5.1.0",
+    "google-auth-library": "^10.4.0",
+    "handlebars": "^4.7.8",
+    "jsonwebtoken": "^9.0.2",
+    "moment": "^2.30.1",
+    "moment-timezone": "^0.6.0",
+    "multer": "^2.0.2",
+    "mysql2": "^3.15.2",
+    "node-fetch": "^3.3.2",
+    "nodemailer": "^7.0.9",
+    "nodemailer-html-to-text": "^3.2.0",
+    "nodemon": "^3.1.10",
+    "passport": "^0.7.0",
+    "prom-client": "^15.1.3",
+    "sequelize": "^6.37.7",
+    "sequelize-cli": "^6.6.3"
+  }
+}
diff --git a/uploads/attendance_photos/att_1760415503732-451735290.png b/uploads/attendance_photos/att_1760415503732-451735290.png
new file mode 100644
index 0000000..6ff874d
Binary files /dev/null and b/uploads/attendance_photos/att_1760415503732-451735290.png differ
diff --git a/uploads/attendance_photos/att_1760415583599-914745805.png b/uploads/attendance_photos/att_1760415583599-914745805.png
new file mode 100644
index 0000000..6ff874d
Binary files /dev/null and b/uploads/attendance_photos/att_1760415583599-914745805.png differ
diff --git a/uploads/attendance_photos/att_1760415878072-565599008.png b/uploads/attendance_photos/att_1760415878072-565599008.png
new file mode 100644
index 0000000..6ff874d
Binary files /dev/null and b/uploads/attendance_photos/att_1760415878072-565599008.png differ
diff --git a/uploads/attendance_photos/att_1760416090348-869902078.png b/uploads/attendance_photos/att_1760416090348-869902078.png
new file mode 100644
index 0000000..6ff874d
Binary files /dev/null and b/uploads/attendance_photos/att_1760416090348-869902078.png differ