created API For Aplication Absensi

This commit is contained in:
2025-10-14 14:08:11 +07:00
commit 96d206d892
56 changed files with 6533 additions and 0 deletions

49
.gitignore vendored Normal file
View File

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

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "models"]
path = models
url = git@git.oxinos.io:max/models.git

13
app/config/config.json Normal file
View File

@@ -0,0 +1,13 @@
{
"development": {
"username": "oxinos",
"password": "Z9jUA33GwblqN1Vk06",
"database": "absens",
"host": "31.97.110.178",
"dialect": "mysql"
}
}

13
app/config/db.config.js Normal file
View File

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

34
app/config/mail.config.js Normal file
View File

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

View File

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

View File

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

View File

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

0
app/core/models/index.js Normal file
View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

27
app/helpers/distance.js Normal file
View File

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

View File

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

111
app/helpers/helpers.js Normal file
View File

@@ -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 = `
<p>Kode verifikasi Anda adalah:</p>
<h2>${otp}</h2>
<p>Gunakan kode ini untuk melanjutkan proses.</p>
`;
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 = `
<div style="max-width:600px;margin:auto;border:1px solid #ddd;padding:20px;border-radius:10px;font-family:sans-serif;">
<div style="text-align:right;">
<svg style="margin-top: 15px;" width="35" height="35" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="20" height="20" rx="4" fill="url(#paint0_linear_563_10546)"/>
<g clip-path="url(#clip0_563_10546)">
<path d="M15.6071 6.33079C15.697 6.27014 15.721 6.14788 15.6578 6.05969C14.8161 4.88431 13.6288 3.99802 12.259 3.52594C10.8272 3.0325 9.27365 3.0184 7.8331 3.48577C6.39256 3.95315 5.14323 4.8766 4.27382 6.11666C3.40441 7.35671 2.96214 8.84601 3.01383 10.3596C3.06552 11.8732 3.60834 13.3288 4.56032 14.5067C5.51229 15.6846 6.82171 16.5206 8.29078 16.8887C9.75985 17.2567 11.3088 17.1366 12.7036 16.5467C14.0381 15.9822 15.1621 15.017 15.9217 13.787C15.9787 13.6947 15.9465 13.5743 15.8526 13.52L13.5124 12.1643C13.4186 12.1099 13.2988 12.1423 13.2398 12.2333C12.8187 12.8821 12.2122 13.3916 11.4971 13.694C10.7215 14.0221 9.86026 14.0888 9.04342 13.8842C8.22659 13.6796 7.49852 13.2147 6.9692 12.5598C6.43988 11.9049 6.13806 11.0955 6.10932 10.2539C6.08058 9.4123 6.32649 8.58422 6.8099 7.89472C7.29331 7.20522 7.98797 6.69176 8.78894 6.43189C9.58992 6.17202 10.4537 6.17986 11.2498 6.45422C11.9839 6.70721 12.6238 7.17414 13.0881 7.79273C13.1532 7.87947 13.2749 7.90356 13.3648 7.84292L15.6071 6.33079Z" fill="white"/>
<path d="M15.6071 6.33079C15.697 6.27014 15.721 6.14788 15.6578 6.05969C14.8264 4.89869 13.6576 4.0194 12.308 3.54304C10.8967 3.04487 9.36252 3.01381 7.93215 3.45445C6.50179 3.89509 5.25106 4.78406 4.36469 5.99007C3.51714 7.14326 3.04572 8.52786 3.01174 9.95545C3.00916 10.0639 3.09776 10.1515 3.20623 10.151L5.9107 10.1392C6.01917 10.1388 6.10619 10.0504 6.11121 9.94204C6.14654 9.17917 6.40644 8.44204 6.86043 7.82434C7.35327 7.15377 8.0487 6.65948 8.84402 6.41447C9.63934 6.16947 10.4924 6.18674 11.2771 6.46373C12 6.71889 12.6296 7.18198 13.0881 7.79273C13.1532 7.87947 13.2749 7.90356 13.3648 7.84292L15.6071 6.33079Z" fill="white"/>
<path d="M15.625 6.35746C15.7152 6.29724 15.7398 6.17509 15.677 6.0866C14.841 4.90741 13.6581 4.01559 12.2908 3.53697C10.8615 3.0367 9.30832 3.01505 7.86569 3.47529C6.42306 3.93553 5.16935 4.85266 4.29388 6.0882C3.41841 7.32373 2.96872 8.81057 3.01271 10.3242C3.05671 11.8378 3.592 13.296 4.53776 14.4786C5.48351 15.6612 6.78838 16.504 8.2553 16.8797C9.72223 17.2553 11.2716 17.1435 12.6693 16.5611C14.0066 16.0038 15.1357 15.0448 15.9018 13.819C15.9593 13.7271 15.9277 13.6065 15.8341 13.5517L13.5013 12.1834C13.4077 12.1285 13.2878 12.1602 13.2283 12.2508C12.8037 12.8973 12.1946 13.4034 11.478 13.702C10.7008 14.0259 9.83934 14.0881 9.0237 13.8792C8.20805 13.6703 7.48252 13.2017 6.95666 12.5442C6.43079 11.8866 6.13316 11.0758 6.1087 10.2342C6.08423 9.3926 6.33427 8.56588 6.82106 7.87889C7.30784 7.19191 8.00493 6.68196 8.80706 6.42606C9.6092 6.17016 10.4728 6.1822 11.2675 6.46036C12.0003 6.71685 12.6378 7.18676 13.0991 7.80745C13.1638 7.8945 13.2854 7.91917 13.3756 7.85895L15.625 6.35746Z" fill="url(#paint1_radial_563_10546)"/>
<path d="M8.9834 10.7974C8.9834 11.1889 9.28385 11.5046 9.65715 11.5046H10.4189C10.7436 11.5046 11.0077 11.2284 11.0077 10.8885C11.0077 10.5182 10.8468 10.3877 10.6071 10.3027L9.38401 9.87785C9.14425 9.79287 8.9834 9.66237 8.9834 9.29211C8.9834 8.9522 9.24743 8.67603 9.57217 8.67603H10.3339C10.7072 8.67603 11.0077 8.99166 11.0077 9.38316" stroke="url(#paint2_radial_563_10546)" stroke-width="0.455235" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.99316 8.26929V11.9112" stroke="url(#paint3_radial_563_10546)" stroke-width="0.455235" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<linearGradient id="paint0_linear_563_10546" x1="19.1806" y1="0.757954" x2="-0.0446659" y2="18.6575" gradientUnits="userSpaceOnUse">
<stop stop-color="#87CEF1"/>
<stop offset="0.343102" stop-color="#81ACD6"/>
<stop offset="0.704149" stop-color="#2D46C2"/>
<stop offset="1" stop-color="#283FB1"/>
</linearGradient>
<radialGradient id="paint1_radial_563_10546" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(9.98601 10.1215) rotate(90) scale(6.97625)">
<stop stop-color="#86C6EA"/>
<stop offset="1" stop-color="white"/>
</radialGradient>
<radialGradient id="paint2_radial_563_10546" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(9.99554 10.0903) rotate(90) scale(1.41426 1.01214)">
<stop stop-color="#86C6EA"/>
<stop offset="1" stop-color="white"/>
</radialGradient>
<radialGradient id="paint3_radial_563_10546" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10.4932 10.0902) rotate(90) scale(1.82094 0.5)">
<stop stop-color="#86C6EA"/>
<stop offset="1" stop-color="white"/>
</radialGradient>
<clipPath id="clip0_563_10546">
<rect width="13.9909" height="13.9909" fill="white" transform="translate(2.97461 3.11011)"/>
</clipPath>
</defs>
</svg>
</div>
<h2 style="color:#000;">Reset Password</h2>
<p>Halo, <b>User</b>!</p>
<p>Heres the link to reset your Cashfio Account:</p>
<p>
<a href="${resetLink}" style="color:#1a73e8;text-decoration:none;">${resetLink}</a>
</p>
<p style="color:#d32f2f;"><strong>For your security, do not share the link with anyone!</strong></p>
<br>
<div style="background:#0d47a1;color:#fff;text-align:center;padding:10px;border-radius:0 0 10px 10px;">
Copyright Cashfio ${new Date().getFullYear()}
</div>
</div>
`;
await sendMail({ to: email, subject, html });
};
const normalizePhone = (phone) => {
return phone ? phone.replace(/[^+\d]/g, '') : null;
};
module.exports = {
generateOTP,
sendOTP,
sendForgotPassword,
normalizePhone
};

80
app/helpers/responses.js Normal file
View File

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

45
app/helpers/sync_model.js Normal file
View File

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

15
app/helpers/token.js Normal file
View File

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

View File

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

View File

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

48
app/middlewares/apiKey.js Normal file
View File

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

View File

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

View File

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

View File

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

20
app/middlewares/login.js Normal file
View File

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

View File

@@ -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()
}

24
app/middlewares/upload.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

28
auth.html Normal file
View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<title>Google Login Test</title>
<script src="https://accounts.google.com/gsi/client" async defer></script>
<script>
function handleCredentialResponse(response) {
console.log("Google ID Token:", response.credential);
// Tampilkan token di halaman agar bisa kamu salin ke Postman
document.getElementById("result").innerText = response.credential;
}
</script>
</head>
<body>
<h2>Login dengan Google</h2>
<div id="g_id_onload"
data-client_id="270521628718-s79hja01mjur7ee18fkooup3fq5n96p9.apps.googleusercontent.com"
data-callback="handleCredentialResponse">
</div>
<div class="g_id_signin" data-type="standard"></div>
<h3>Google Token (id_token):</h3>
<pre id="result" style="background:#f0f0f0;padding:10px;"></pre>
</body>
</html>

66
index.js Normal file
View File

@@ -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}.`);
});

60
models/api_key.model.js Normal file
View File

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

View File

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

57
models/branch.model.js Normal file
View File

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

View File

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

70
models/migration.js Normal file
View File

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

116
models/user.model.js Normal file
View File

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

3501
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

36
package.json Normal file
View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB