refactor: rewrite ke codebase lama

This commit is contained in:
Muhammad Zhafran Ilham
2026-06-07 20:17:49 +07:00
parent ce5487dcbf
commit fe74b2213d
146 changed files with 14608 additions and 6686 deletions
Regular → Executable
+16 -10
View File
@@ -1,13 +1,19 @@
# Server Configuration
PORT=5000
NODE_ENV=development
PORT=**********
NODE_ENV=production
# Database
DATABASE_URL="mysql://db_username:db_pass@db_host:3306/internify_lms_db"
DATABASE_URL="mysql://username:password@host:3306/internify_db"
# Authentication Config
JWT_SECRET="your_jwt_secret_key_here"
JWT_EXPIRES_IN="7d"
// RSA 256 KEY PAIR FOR JWT SIGNING AND VERIFICATION (bisa digenerate dengan openssl)
JWT_PRIVATE_KEY=-----BEGIN RSA PRIVATE KEY-----\n{token}\n-----END RSA PRIVATE KEY-----
JWT_PUBLIC_KEY=-----BEGIN PUBLIC KEY-----\n{token}\n-----END PUBLIC KEY-----
# Swagger
SWAGGER_ENABLE=true
// UNTUK RECAPTCHA, bisa didapatkan di https://www.google.com/recaptcha/admin/create
RECAPTCHA_SECRET_KEY=**********
RECAPTCHA_SITE_KEY=**********
SWAGGER_ENABLED=false
// UNTUK EMAIL SERVICE
AUTH_EMAIL=**********
AUTH_PASSWORD=**********
+83
View File
@@ -0,0 +1,83 @@
# name: Deploy to Vercel
# on:
# push:
# branches:
# - main
# - refactor
# pull_request:
# branches:
# - main
# - refactor
# env:
# VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
# VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
# jobs:
# Deploy-Production:
# runs-on: ubuntu-latest
# if: github.ref == 'refs/heads/main'
# steps:
# - name: Checkout code
# uses: actions/checkout@v3
# - name: Setup Node.js
# uses: actions/setup-node@v3
# with:
# node-version: "18"
# cache: "npm"
# - name: Install dependencies
# run: npm ci
# - name: Generate Prisma Client
# run: npx prisma generate
# - name: Run Database Migrations
# run: npx prisma migrate deploy
# env:
# DATABASE_URL: ${{ secrets.DATABASE_URL }}
# - name: Install Vercel CLI
# run: npm install --global vercel@latest
# - name: Pull Vercel Environment Information
# run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
# - name: Build Project Artifacts
# run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
# - name: Deploy Project Artifacts to Vercel
# run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
# Deploy-Preview:
# runs-on: ubuntu-latest
# if: github.ref != 'refs/heads/main'
# steps:
# - name: Checkout code
# uses: actions/checkout@v3
# - name: Setup Node.js
# uses: actions/setup-node@v3
# with:
# node-version: "18"
# cache: "npm"
# - name: Install dependencies
# run: npm ci
# - name: Generate Prisma Client
# run: npx prisma generate
# - name: Install Vercel CLI
# run: npm install --global vercel@latest
# - name: Pull Vercel Environment Information
# run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
# - name: Build Project Artifacts
# run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
# - name: Deploy Project Artifacts to Vercel
# run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}
+72
View File
@@ -0,0 +1,72 @@
# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
# More GitHub Actions for Azure: https://github.com/Azure/actions
# name: Build and deploy Node.js app to Azure Web App - internify-backend
# on:
# push:
# branches:
# - Dev-Deploy
# workflow_dispatch:
# jobs:
# build:
# runs-on: ubuntu-latest
# permissions:
# contents: read #This is required for actions/checkout
# steps:
# - uses: actions/checkout@v4
# - name: Set up Node.js version
# uses: actions/setup-node@v3
# with:
# node-version: '22.x'
# - name: npm install, build, and test
# run: |
# npm install
# npm run build --if-present
# - name: Zip artifact for deployment
# run: zip release.zip ./* -r
# - name: Upload artifact for deployment job
# uses: actions/upload-artifact@v4
# with:
# name: node-app
# path: release.zip
# deploy:
# runs-on: ubuntu-latest
# needs: build
# environment:
# name: 'Production'
# url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
# permissions:
# id-token: write #This is required for requesting the JWT
# contents: read #This is required for actions/checkout
# steps:
# - name: Download artifact from build job
# uses: actions/download-artifact@v4
# with:
# name: node-app
# - name: Unzip artifact for deployment
# run: unzip release.zip
# - name: Login to Azure
# uses: azure/login@v2
# with:
# client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_1933F867B08648A1B3AC4897BD38462C }}
# tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_91C2FA1078FA401F9914699B69CE0946 }}
# subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_94677CCA0BF6460A8ECB8F7259AA880E }}
# - name: 'Deploy to Azure Web App'
# id: deploy-to-webapp
# uses: azure/webapps-deploy@v3
# with:
# app-name: 'internify-backend'
# slot-name: 'Production'
# package: .
Regular → Executable
+141 -31
View File
@@ -1,43 +1,153 @@
# Dependencies
node_modules/
# Production Build Output
dist/
# Uploads
uploads/docs/*
!uploads/docs/.gitkeep
uploads/images/*
!uploads/images/.gitkeep
# Local Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
*.log
.pnpm-debug.log*
# Editor & IDE settings
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Security Keys
private.pem
public.pem
# OS And Editor files
.DS_Store
.vscode/
.idea/
.zed/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# OS metadata
.DS_Store
Thumbs.db
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Prisma
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# docs directory
src/docs/note.txt
template_db.sql
# upload files
uploads/
/src/generated/prisma
# Vercel
.vercel
.vercel/*.json
.env*.local
Executable
+26
View File
@@ -0,0 +1,26 @@
node_modules
.env
.env.local
.env.development
.env.test
.git
.gitignore
*.log
npm - debug.log *
yarn - debug.log *
yarn - error.log *
.DS_Store
coverage
.nyc_output
*.orig
.idea
.vscode
*.swp
*.swo
* ~
.prisma
.github
README.md
CICD_SETUP.md
VERCEL_DEPLOYMENT_GUIDE.md
UPLOAD_LIMITATIONS.md
Regular → Executable
+1822 -1
View File
File diff suppressed because it is too large Load Diff
+220
View File
@@ -0,0 +1,220 @@
# Internify Backend Overview
Dokumen ini merangkum struktur, alur kerja, dan komponen backend Internify (Express.js + Prisma + MySQL). Fokus hanya pada backend.
## Ringkasan singkat
- Backend melayani dua role utama: admin dan mahasiswa.
- Fitur inti: manajemen lowongan, lamaran, batch, data mahasiswa, partnership, hasil riset, FAQ, dan feedback.
- Arsitektur modul menerapkan pola 4 layer: route -> controller -> service -> repository -> Prisma.
## Struktur folder utama
```
prisma/ Prisma schema dan migrasi
src/
index.js Entry point server Express
docs/ Swagger config dan SQL dump lama
helpers/ Utilitas umum (wrapper, validator, error, db, mail, file)
middleware/ JWT, role guard, multer, recaptcha
modules/ Modul fitur (admin, auth, batch, dll)
routes/ Routing per fitur
uploads/ Folder upload lokal (dev)
oldCode/ Kode lama berbasis query SQL
```
## Entry point dan request flow
- Entry point: `src/index.js`.
- Mendaftarkan CORS, JSON body, dan static file `/uploads`.
- Memasang route per fitur:
- `/admin-api`
- `/auth-api`
- `/lowongan-magang-api`
- `/lamaran-magang-api`
- `/mahasiswa-api`
- `/partnership-api`
- `/hasil-research-api`
- `/faq-api`
- `/feedback-api`
- `/batch-api`
- Swagger UI di `/api-docs` (aktif jika environment mengizinkan).
Alur umum request:
1. Route memanggil controller.
2. Controller validasi input (Joi) dan memanggil service.
3. Service menjalankan business logic dan memanggil repository.
4. Repository menjalankan query Prisma.
5. Response dibungkus dengan helper `wrapper` untuk format yang konsisten.
## Konfigurasi dan environment
- Template env ada di `.env.example`.
- Config global di `src/helpers/infra/global_config.js`.
- Token JWT memakai RSA key pair (RS256).
- `vercel.json` mengatur build dan route untuk Vercel.
## Database dan Prisma
- Prisma schema di `prisma/schema.prisma`.
- Generator Prisma client output ke `src/generated/prisma`.
- Model utama:
- Admin
- Mahasiswa
- LowonganMagang
- LamaranMagang
- Batch
- Partnership
- HasilResearch
- Faq
- Feedback
- RiwayatPelamar (statistik historis)
- Enum utama: Role, StudentRole, StatusLamaran, StatusLowongan, PaidStatus.
## Modul dan endpoint utama
### 1) Auth
Tujuan: login admin dan identitas user.
- POST `/auth-api/login`
- GET `/auth-api/me`
### 2) Admin
Tujuan: CRUD admin.
- POST `/admin-api/add`
- GET `/admin-api/get`
### 3) Batch
Tujuan: mengelola batch aktif dan urutan batch.
- POST `/batch-api/add`
- GET `/batch-api/`
- PUT `/batch-api/:id`
- DELETE `/batch-api/:id`
- PATCH `/batch-api/switch/:batch_number`
### 4) Lowongan Magang
Tujuan: CRUD lowongan, filter kelompok peminatan, status otomatis.
- POST `/lowongan-magang-api/add`
- GET `/lowongan-magang-api/get`
- GET `/lowongan-magang-api/get/id/:id`
- GET `/lowongan-magang-api/get/kelompok/:kelompok_peminatan`
- GET `/lowongan-magang-api/get/kelompok-all`
- PATCH `/lowongan-magang-api/update/:id`
- DELETE `/lowongan-magang-api/delete/:id`
### 5) Lamaran Magang
Tujuan: apply magang, export, statistik.
- POST `/lamaran-magang-api/add/:id_lowongan_magang`
- POST `/lamaran-magang-api/add-mobile/:id_lowongan_magang`
- GET `/lamaran-magang-api/get`
- GET `/lamaran-magang-api/get/:id_lamaran_magang`
- PATCH `/lamaran-magang-api/update/:id_lamaran_magang`
- GET `/lamaran-magang-api/export`
- DELETE `/lamaran-magang-api/delete/:id`
- GET `/lamaran-magang-api/statistics/dashboard`
- GET `/lamaran-magang-api/statistics/position`
- GET `/lamaran-magang-api/statistics/country`
- GET `/lamaran-magang-api/statistics/university`
### 6) Mahasiswa
Tujuan: list mahasiswa (admin).
- GET `/mahasiswa-api/get`
### 7) Partnership
Tujuan: CRUD partnership dan upload logo.
- POST `/partnership-api/add`
- GET `/partnership-api/get`
- GET `/partnership-api/get/:id`
- PATCH `/partnership-api/update/:id`
- DELETE `/partnership-api/delete/:id`
### 8) Hasil Research
Tujuan: CRUD hasil riset dan upload gambar.
- POST `/hasil-research-api/add`
- GET `/hasil-research-api/get`
- GET `/hasil-research-api/get/:id`
- PATCH `/hasil-research-api/update/:id`
- DELETE `/hasil-research-api/delete/:id`
### 9) FAQ
Tujuan: CRUD FAQ.
- GET `/faq-api/get`
- GET `/faq-api/get/:id`
- POST `/faq-api/add`
- PATCH `/faq-api/update/:id`
- DELETE `/faq-api/delete/:id`
### 10) Feedback
Tujuan: CRUD feedback, filter batch dan tahun, upload gambar.
- POST `/feedback-api/add`
- GET `/feedback-api/get`
- GET `/feedback-api/get/:id`
- PATCH `/feedback-api/update/:id`
- DELETE `/feedback-api/delete/:id`
## Middleware penting
- `verifyJWT`: validasi JWT RSA (RS256), set `req.id`, `req.role`.
- `isAdmin`: guard role admin.
- `multer`: upload file + size check + error handler.
- `recaptcha`: verifikasi reCAPTCHA (saat ini tidak diaktifkan pada route lamaran utama).
## Helper penting
- `wrapper`: format response konsisten dan mapping error ke HTTP code.
- `validator`: validasi payload menggunakan Joi.
- `fileHelper`: hapus file, cek file, dan validasi path file.
- `imageUpload.helper`: normalisasi path upload.
- `mail`: transporter Nodemailer.
## Uploads dan file storage
- Lokasi upload dev: `src/uploads`.
- Lokasi upload production: `/tmp/uploads`.
- File yang diupload dibersihkan otomatis saat validasi gagal atau error.
## Email
- Email konfirmasi dan status lamaran dikirim dari module lamaran.
- Template HTML ada di `src/modules/lamaranMagang/helpers/email.helper.js`.
## Swagger
- Swagger config di `src/docs/swagger.js`.
- Route dokumentasi di `/api-docs`.
## Legacy (oldCode)
Folder `oldCode/` berisi controller dan model lama berbasis query SQL langsung. Saat ini tidak dipakai oleh entry point utama.
## Catatan teknis penting
- JWT menggunakan RSA key pair (RS256), bukan HS256.
- Setiap mahasiswa hanya boleh melamar satu posisi (check pada lamaran).
- Status lowongan dihitung otomatis dari durasi awal dan akhir.
- Statistik negara dan universitas menggunakan raw query karena join kompleks.
- Prisma client utama dipakai dari `helpers/db/db_connection.js`.
## Paragraf laporan backend (siap pakai)
Backend Internify dibangun menggunakan Express.js sebagai web framework dengan Prisma ORM dan MySQL sebagai basis data. Arsitektur modular menerapkan pola 4 layer (route, controller, service, repository) agar alur bisnis terstruktur dan mudah dirawat, sementara validasi input dilakukan dengan Joi dan response distandarkan melalui wrapper. Autentikasi admin menggunakan JWT berbasis RSA (RS256), disertai middleware role guard untuk keamanan. Fitur upload file dikelola oleh Multer dengan pengecekan ukuran serta mekanisme cleanup, dan notifikasi email dikirim menggunakan Nodemailer. Dokumentasi API disediakan melalui Swagger untuk memudahkan pengujian dan integrasi.
+45
View File
@@ -0,0 +1,45 @@
import * as adminService from '../models/admin.js';
export const addAdmin = async (req, res) => {
const { nama_depan, nama_belakang, email, password } = req.body;
const role = 'admin';
const requireRole = req.role;
try {
if (requireRole !== 'admin') {
return res.status(400).json({ message: 'Only admins can access this' });
}
const [foundAdmin] = await adminService.getAdminByEmail(email);
if (foundAdmin.length > 0) {
return res.status(400).json({ message: 'Email already exists' });
}
await adminService.addAdmin(
nama_depan,
nama_belakang,
email,
password,
role
);
return res.status(200).json({ message: 'Admin created successfully' });
} catch (error) {
console.error(error);
return res.status(500).json({ message: 'Internal server error' });
}
};
export const getAdmin = async (_req, res) => {
try {
const [admin] = await adminService.getAdmin();
if (admin.length === 0) {
return res.status(404).json({ message: 'Admin not found' });
}
return res.status(200).json({
message: 'Get all admin successfully',
data: admin,
});
} catch {
return res.status(500).json({ message: 'Internal server error' });
}
};
+70
View File
@@ -0,0 +1,70 @@
const adminService = require("../models/admin");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const login = async (req, res) => {
const { email, password } = req.body;
try {
const [foundAdmin] = await adminService.getAdminByEmail(email);
if (foundAdmin.length === 0) {
return res.status(400).json({
message: "Invalid email or password",
});
}
const isPasswordValid = await bcrypt.compare(
password,
foundAdmin[0].password
);
if (!isPasswordValid) {
return res.status(400).json({
message: "Invalid email or password",
});
}
const token = jwt.sign(
{ id: foundAdmin[0].id, role: foundAdmin[0].role },
process.env.JWT_SECRET,
{ expiresIn: "1h" }
);
return res.status(200).json({
message: "Login successful",
token,
});
} catch (error) {
console.error(error);
return res.status(500).json({
message: "Internal server error",
});
}
}
const me = async (req, res) => {
const id = req.id;
try {
console.log(id);
const [admin] = await adminService.getAdminById(id);
if (admin.length === 0) {
return res.status(404).json({
message: "Admin not found",
});
}
return res.status(200).json({
message: "Get admin successfully",
data: admin[0],
});
} catch (error) {
console.error(error);
return res.status(500).json({
message: "Internal server error",
});
}
}
module.exports = {
login,
me,
}
+178
View File
@@ -0,0 +1,178 @@
const hasilResearchModel = require('../models/hasilResearch');
const path = require('path');
const { deleteFileIfExists } = require('../config/fileHelper');
const addhasilResearch = async (req, res) => {
const { nama_project } = req.body;
const { deskripsi } = req.body;
const image = req.file;
const {link_project} = req.body;
try {
const image_path = await uploadGambar(image);
await hasilResearchModel.addhasilResearch(nama_project,deskripsi, link_project, image_path);
res.status(200).json({
message: "Research result data successfully added",
data: { nama_project,deskripsi, image_path,link_project },
});
} catch (error) {
console.error("Error adding research results:", error);
res.status(500).json({
message: "Internal server error",
error: error.message,
});
}
};
const uploadGambar = async (image) => {
try {
if (!image) {
throw new Error("Please upload an image!");
}
const file = `/uploads/${image.filename}`;
return file;
} catch (error) {
console.error(error.message);
throw new Error("Failed to upload image.");
}
};
const gethasilResearch = async (req, res) => {
try {
const [result] = await hasilResearchModel.gethasilResearch();
if (result.length === 0) {
return res.status(404).json({
message: "There is no research data",
});
}
res.status(200).json({
message: "Research data was successfully collected",
data: result,
});
} catch (error) {
console.error("Error getting research results:", error);
res.status(500).json({
message: "Internal server error",
error: error.message,
});
}
}
const deleteHasilResearch = async (req, res) => {
try {
const { id } = req.params;
const [hasilResearch] = await hasilResearchModel.gethasilResearchById(id);
if (hasilResearch.length === 0) {
return res.status(404).json({
messsage: "Hasil research not found",
});
}
const image_path = hasilResearch[0].image_path;
if (image_path) {
const fileName = path.basename(image_path);
await deleteFileIfExists(fileName);
}
await hasilResearchModel.deletehasilResearch(id);
res.status(200).json({
message: "Research result data successfully deleted",
});
} catch (error) {
console.error("Error deleting research results :", error);
res.status(500).json({
message: "Internal server error",
error: error.message,
});
}
}
const gethasilResearchById = async (req, res) => {
try {
const { id } = req.params
const [hasilResearch] = await hasilResearchModel.gethasilResearchById(id);
if (hasilResearch.length === 0) {
return res.status(404).json({
messsage: "Hasil research not found",
});
}
res.status(200).json({
message: "Research data was successfully collected",
data: hasilResearch,
});
} catch (error) {
console.error("Error getting research results:", error);
res.status(500).json({
message: "Internal server error",
error: error.message,
});
}
}
const updateHasilResearch = async (req, res) => {
try {
const { id } = req.params;
const { nama_project, deskripsi, link_project } = req.body;
const image = req.file;
const [existing] = await hasilResearchModel.gethasilResearchById(id);
if (existing.length === 0) {
return res.status(404).json({ message: "Hasil research not found" });
}
const currentData = existing[0];
let image_path = existing[0].image_path;
if (image) {
const oldFile = path.basename(image_path);
await deleteFileIfExists(oldFile);
image_path = await uploadGambar(image);
}
const updatedData = {
nama_project: nama_project?.trim() ? nama_project : currentData.nama_project,
deskripsi: deskripsi?.trim() ? deskripsi : currentData.deskripsi,
link_project: link_project?.trim() ? link_project : currentData.link_project,
image_path,
};
await hasilResearchModel.updateHasilResearch(
id,
updatedData.nama_project,
updatedData.deskripsi,
updatedData.link_project,
updatedData.image_path
);
return res.status(200).json({
message: "Hasil research successfully updated",
data: {
id,
nama_project: updatedData.nama_project,
deskripsi: updatedData.deskripsi,
link_project: updatedData.link_project,
image_path: updatedData.image_path,
},
});
} catch (error) {
console.error("Error updating research:", error);
res.status(500).json({
message: "Internal server error",
error: error.message,
});
}
};
module.exports = {
addhasilResearch,
gethasilResearch,
gethasilResearchById,
updateHasilResearch,
deleteHasilResearch
}
+361
View File
@@ -0,0 +1,361 @@
const mahasiswaModel = require("../models/mahasiswa");
const lowonganMagangModel = require("../models/lowonganMagang");
const lamaranMagangModel = require("../models/lamaranMagang");
const transporter = require("../config/mail");
const ExcelJS = require('exceljs');
require("dotenv").config();
const path = require('path');
const { deleteFileIfExists } = require('../config/fileHelper');
const addLamaranMagang = async (req, res) => {
const { id_lowongan_magang } = req.params;
const {
nama_depan,
nama_belakang,
email,
kontak,
jurusan,
motivasi,
relevant_skills,
} = req.body;
const CV = req.files.cv?.[0];
const Portofolio = req.files.portofolio?.[0];
const status = "diproses";
const role = "student";
try {
const cv_path = await uploadCV(CV);
const portofolio_path = await uploadPortofolio(Portofolio);
const [lowonganMagang] = await lowonganMagangModel.getLowonganMagangById(
id_lowongan_magang
);
if (lowonganMagang.length === 0) {
return res.status(404).json({
message: "Internship vacancy not found.",
});
}
const namaBelakangFinal = nama_belakang?.trim() || null;
const dataMahasiswa = {
nama_depan,
nama_belakang: namaBelakangFinal,
email,
kontak,
jurusan,
role,
cv_path,
portofolio_path,
motivasi,
relevant_skills,
};
const result = await mahasiswaModel.addMahasiswa(
nama_depan,
namaBelakangFinal,
email,
kontak,
jurusan,
role,
cv_path,
portofolio_path,
motivasi,
relevant_skills
);
const id_mahasiswa = result[0].insertId;
await lamaranMagangModel.addlowonganMagang(
id_mahasiswa,
id_lowongan_magang,
status
);
await sendEmail(dataMahasiswa, lowonganMagang[0]);
res.status(200).json({
message: "Internship application submitted successfully.",
data: {
id_lowongan_magang,
dataMahasiswa,
status,
},
});
} catch (error) {
console.error("Error adding internship application:", error);
res.status(500).json({
message: "Internal server error.",
error: error.message,
});
}
};
const uploadCV = async (CV) => {
try {
if (!CV) {
throw new Error("Please upload your CV.");
}
const file = `/uploads/${CV.filename}`;
return file;
} catch (error) {
console.error(error.message);
throw new Error("Failed to upload CV.");
}
};
const uploadPortofolio = async (portofolio) => {
try {
if (!portofolio) {
throw new Error("Please upload your portfolio.");
}
const file = `/uploads/${portofolio.filename}`;
return file;
} catch (error) {
console.error(error.message);
throw new Error("Failed to upload portfolio.");
}
};
const getAllLamaranMagang = async (req, res) => {
try {
const [result] = await lamaranMagangModel.getAllLamaranMagang();
if (result.length === 0) {
return res.status(404).json({
message: "No internship applications found.",
});
}
res.status(200).json({
message: "Internship applications retrieved successfully.",
data: result,
});
} catch (error) {
console.error("Error fetching all internship applications:", error);
res.status(500).json({
message: "Internal server error.",
error: error.message,
});
}
};
const getLamaranByID = async (req, res) => {
const { id_lamaran_magang } = req.params;
try {
const [result] = await lamaranMagangModel.getLamaranByID(
id_lamaran_magang
);
if (result.length === 0) {
return res.status(404).json({
message: "No internship applications found.",
});
}
res.status(200).json({
message: "Internship applications retrieved successfully.",
data: result,
});
} catch (error) {
console.error("Error fetching internship applications by ID:", error);
res.status(500).json({
message: "Internal server error.",
error: error.message,
});
}
};
const updateStatusLamaran = async (req, res) => {
const { id_lamaran_magang } = req.params;
const { status } = req.body;
const role = req.role;
try {
if (role !== "admin") {
return res.status(403).json({
message: "Unauthorized access.",
});
}
const [lamaran] = await lamaranMagangModel.getDetailLamaranById(
id_lamaran_magang
);
// console.log(lamaran[0]);
if (lamaran.length === 0) {
return res.status(404).json({
message: "Internship application not found.",
});
}
await lamaranMagangModel.updateStatusLamaran(id_lamaran_magang, status);
res.status(200).json({
message: "Internship application status successfully updated.",
});
await sendStatusEmail(lamaran[0], status)
} catch (error) {
console.error("Error updating internship application status:", error);
res.status(500).json({
message: "Internal server error.",
error: error.message,
});
}
};
const sendEmail = async (dataMahasiswa, dataPekerjaan) => {
try {
const fullName = `${dataMahasiswa.nama_depan}${dataMahasiswa.nama_belakang ? ' ' + dataMahasiswa.nama_belakang : ''}`;
const mailOptions = {
from: process.env.AUTH_EMAIL,
to: dataMahasiswa.email,
subject: "Konfirmasi Penerimaan Lamaran Anda",
headers: {
"X-Priority": "1",
"X-MSMail-Priority": "High",
Importance: "high",
},
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: auto; padding: 20px; border: 1px solid #e0e0e0; border-radius: 10px;">
<h2 style="color: #333;">Lamaran Anda Telah Kami Terima</h2>
<p>Halo ${fullName},</p>
<p>Terima kasih telah mengirimkan lamaran untuk posisi <strong>${dataPekerjaan.posisi}</strong> di <strong>Humic Engineering</strong>.</p>
<p>Kami telah menerima dokumen dan informasi yang Anda kirimkan, dan saat ini tim rekrutmen kami sedang meninjaunya dengan seksama.</p>
<p>Jika profil Anda sesuai dengan kebutuhan kami, tim kami akan menghubungi Anda untuk tahapan seleksi berikutnya. Proses ini dapat memakan waktu hingga beberapa minggu.</p>
<p>Kami sangat menghargai minat Anda untuk bergabung bersama tim kami.</p>
<p>Salam hangat,</p>
<p><strong>Tim Rekrutmen Humic Engineering</strong></p>
<hr style="margin: 30px 0;">
<p style="font-size: 12px; color: #888;">Email ini dikirim secara otomatis. Jika Anda memiliki pertanyaan lebih lanjut, silakan hubungi kami melalui email resmi yang tersedia di website kami.</p>
</div>
`,
};
await transporter.sendMail(mailOptions);
} catch (error) {
console.error("Error mengirim email lamaran magang:", error);
}
};
const sendStatusEmail = async (dataLamaran, statusLamaran) => {
try {
const isAccepted = statusLamaran === "diterima";
const fullName = `${dataLamaran.nama_depan}${dataLamaran.nama_belakang ? ' ' + dataLamaran.nama_belakang : ''}`;
const subject = isAccepted
? "Selamat! Lamaran Anda Diterima"
: "Pemberitahuan Status Lamaran Anda";
const messageContent = isAccepted
? `
<p>Halo ${fullName},</p>
<p>Selamat! Lamaran Anda untuk posisi <strong>${dataLamaran.posisi}</strong> di <strong>Humic Engineering</strong> telah diterima.</p>
<p>Tim kami sangat terkesan dengan profil dan kualifikasi Anda. Kami akan segera menghubungi Anda terkait tahapan selanjutnya.</p>
<p>Terima kasih telah melamar dan kami menantikan kerja sama yang luar biasa bersama Anda.</p>
`
: `
<p>Halo ${fullName},</p>
<p>Terima kasih atas lamaran Anda untuk posisi <strong>${dataLamaran.posisi}</strong> di <strong>Humic Engineering</strong>.</p>
<p>Setelah mempertimbangkan secara seksama, kami memutuskan untuk tidak melanjutkan proses lamaran Anda ke tahap berikutnya.</p>
<p>Jangan berkecil hati—kami sangat menghargai waktu dan usaha Anda. Kami mendorong Anda untuk terus mencoba dan semoga sukses dalam perjalanan karier Anda.</p>
`;
const mailOptions = {
from: process.env.AUTH_EMAIL,
to: dataLamaran.email,
subject,
headers: {
"X-Priority": "1",
"X-MSMail-Priority": "High",
Importance: "high",
},
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: auto; padding: 20px; border: 1px solid #e0e0e0; border-radius: 10px;">
${messageContent}
<p>Salam hangat,</p>
<p><strong>Tim Rekrutmen Humic Engineering</strong></p>
<hr style="margin: 30px 0;">
<p style="font-size: 12px; color: #888;">Email ini dikirim secara otomatis. Untuk informasi lebih lanjut, silakan hubungi kami melalui kontak resmi.</p>
</div>
`,
};
await transporter.sendMail(mailOptions);
} catch (error) {
console.error("Gagal mengirim email status lamaran:", error);
}
};
const exportDataToExcel = async (req, res) => {
try {
const [rows] = await lamaranMagangModel.getAllExportData();
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Data');
// Add header
const headers = Object.keys(rows[0]);
worksheet.addRow(headers);
// Add rows
rows.forEach(row => {
worksheet.addRow(Object.values(row));
});
// setting header buat file excel
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', 'attachment; filename=data.xlsx');
await workbook.xlsx.write(res);
res.end();
} catch (err) {
console.error(err);
res.status(500).send('Error generating Excel file');
}
}
const deleteAllLamaranMagang = async (req, res) => {
try {
// Ambil semua mahasiswa
const [mahasiswaList] = await mahasiswaModel.getAllMahasiswa();
for (const mhs of mahasiswaList) {
const files = [mhs.cv_path, mhs.portofolio_path];
for (const filePath of files) {
if (filePath) {
const fileName = path.basename(filePath);
await deleteFileIfExists(fileName);
}
}
}
// Hapus semua lamaran & mahasiswa
await lamaranMagangModel.deleteAllLamaranMagang();
await mahasiswaModel.deleteAllMahasiswa();
res.status(200).json({
message: "All student data, applications, and files were successfully deleted.",
});
} catch (err) {
console.error("Failed to delete all:", err);
res.status(500).json({ message: "Failed to delete all data.", error: err.message });
}
};
module.exports = {
addLamaranMagang,
getAllLamaranMagang,
getLamaranByID,
updateStatusLamaran,
exportDataToExcel,
deleteAllLamaranMagang
};
+261
View File
@@ -0,0 +1,261 @@
const lowonganMagangService = require("../models/lowonganMagang");
const { v4: uuidv4 } = require("uuid");
const fs = require("fs").promises;
const path = require('path');
const { deleteFileIfExists } = require('../helpers/utils/fileHelper'); // ganti sesuai lokasi fileHelper.js
// Ini komentar buat penerus pengembangan proyek ini
// di db, field status_lowongan cuman di define pas add lowongan doang
// jadi pas dia get data lowongan, field status_lowongan bakal di override
// kenapa gk diapus aja fieldnya? Pertanyaan yang bagus
// itu aja fyi-nya
const addLowonganMagang = async (req, res) => {
const {
posisi,
kelompok_peminatan,
jobdesk,
lokasi,
kualifikasi,
benefit,
durasi_awal,
durasi_akhir,
paid,
} = req.body;
const require = req.role;
const now = new Date();
const tgl_awal = new Date(durasi_awal);
const tgl_akhir = new Date(durasi_akhir);
let status_lowongan = "ditutup";
if (now >= tgl_awal && now <= tgl_akhir) {
status_lowongan = "dibuka";
}
const image_path = req.file;
try {
if (require === "admin") {
const id = await uuidv4();
const file = await uploadGambar(image_path);
await lowonganMagangService.addlowonganMagang(
id,
posisi,
kelompok_peminatan,
jobdesk,
lokasi,
kualifikasi,
benefit,
durasi_awal,
durasi_akhir,
status_lowongan,
paid,
file
);
return res.status(200).json({
message: "Internship vacancy created successfully",
});
}
return res.status(403).json({
message: "Access denied: Only admins can access this route",
});
} catch (error) {
console.error(error);
return res.status(500).json({
message: "Internal server error",
error: error.message,
});
}
};
const uploadGambar = async (image) => {
try {
if (!image) {
throw new Error("Please upload an image!");
}
const file = `/uploads/${image.filename}`;
return file;
} catch (error) {
console.error(error.message);
throw new Error("Failed to upload image.");
}
};
const getAllLowonganMagang = async (req, res) => {
try {
const [lowonganMagang] = await lowonganMagangService.getAllLowonganMagang();
if (lowonganMagang.length === 0) {
return res.status(404).json({
message: "No internship vacancies found",
});
}
const now = new Date();
const updatedLowongan = lowonganMagang.map(item => {
const durasiAwal = new Date(item.durasi_awal);
const durasiAkhir = new Date(item.durasi_akhir);
const status = now >= durasiAwal && now <= durasiAkhir ? "dibuka" : "ditutup";
return {
...item,
status_lowongan: status,
};
});
return res.status(200).json({
message: "Successfully retrieved all internship vacancies",
data: updatedLowongan,
});
} catch (error) {
return res.status(500).json({
message: "Internal server error",
error: error.message,
});
}
};
const getLowonganMagangById = async (req, res) => {
const { id } = req.params;
try {
const [lowonganMagang] = await lowonganMagangService.getLowonganMagangById(
id
);
if (lowonganMagang.length === 0) {
return res.status(404).json({
message: "Internship vacancy not found",
});
}
const now = new Date();
const tgl_awal = new Date(lowonganMagang[0].durasi_awal);
const tgl_akhir = new Date(lowonganMagang[0].durasi_akhir);
let status_lowongan = "ditutup";
if (now >= tgl_awal && now <= tgl_akhir) {
status_lowongan = "dibuka";
}
lowonganMagang[0].status_lowongan = status_lowongan
return res.status(200).json({
message: "Successfully retrieved internship vacancy by ID",
data: lowonganMagang[0],
});
} catch (error) {
console.error(error);
return res.status(500).json({
message: "Internal server error",
error: error.message,
});
}
};
const getLowonganByKelompokPeminatan = async (req, res) => {
const { kelompok_peminatan } = req.params;
try {
const [lowonganMagang] = await lowonganMagangService.getLowonganByKelompokPeminatan(kelompok_peminatan);
if (lowonganMagang.length === 0) {
return res.status(404).json({
message: "No internship vacancies found for this specialization group",
});
}
const now = new Date();
const updatedLowongan = lowonganMagang.map(item => {
const durasiAwal = new Date(item.durasi_awal);
const durasiAkhir = new Date(item.durasi_akhir);
const status = now >= durasiAwal && now <= durasiAkhir ? "dibuka" : "ditutup";
return {
...item,
status_lowongan: status,
};
});
return res.status(200).json({
message: "Successfully retrieved internship vacancies by specialization group",
data: updatedLowongan,
});
} catch (error) {
console.error(error);
return res.status(500).json({
message: "Internal server error",
error: error.message,
});
}
}
const getAllKelompokPeminatan = async (req, res) => {
try {
const [kelompokPeminatan] = await lowonganMagangService.getAllKelompokPeminatan();
if (kelompokPeminatan.length === 0) {
return res.status(404).json({
message: "No internship specialization group found",
});
}
return res.status(200).json({
message: "Successfully retrieved all specialization group",
data: kelompokPeminatan.map(row => row.kelompok_peminatan),
});
} catch (error) {
return res.status(500).json({
message: "Internal server error",
error: error.message,
});
}
};
const deleteLowonganMagangById = async (req, res) => {
const { id } = req.params;
const role = req.role;
try {
if (role === "admin") {
const [lowonganMagang] = await lowonganMagangService.getLowonganMagangById(id);
if (!lowonganMagang || lowonganMagang.length === 0) {
return res.status(404).json({
message: "Internship vacancy not found",
});
}
const imagePath = lowonganMagang[0].image_path;
if (imagePath) {
const fileName = path.basename(imagePath); // Ambil nama file dari path
await deleteFileIfExists(fileName);
}
await lowonganMagangService.deleteLowonganMagangById(id);
return res.status(200).json({
message: "Internship vacancy deleted successfully",
});
}
return res.status(403).json({
message: "Access denied: Only admins can access this route",
});
} catch (error) {
console.error(error);
return res.status(500).json({
message: "Internal server error",
error: error.message,
});
}
};
module.exports = {
addLowonganMagang,
getAllLowonganMagang,
getLowonganMagangById,
getLowonganByKelompokPeminatan,
getAllKelompokPeminatan,
deleteLowonganMagangById,
};
+21
View File
@@ -0,0 +1,21 @@
const mahasiswaModel = require("../models/mahasiswa");
const getAllMahasiswa = async (req, res) => {
try {
const [result] = await mahasiswaModel.getAllMahasiswa();
res.status(200).json({
message: "Student data retrieved successfully",
data: result,
});
} catch (error) {
console.error("Error fetching all students:", error);
res.status(500).json({
message: "Internal server error",
error: error.message,
});
}
};
module.exports = {
getAllMahasiswa,
};
+159
View File
@@ -0,0 +1,159 @@
const partnershipModel = require("../models/partnership");
const path = require('path');
const { deleteFileIfExists } = require('../config/fileHelper');
const addPartnership = async (req, res) => {
const { nama_partner } = req.body;
const image = req.file;
try {
const image_path = await uploadGambar(image);
await partnershipModel.addPartnership(nama_partner, image_path);
res.status(200).json({
message: "Partnership data successfully added",
data: { nama_partner, image_path },
});
} catch (error) {
console.error("Error adding partnership:", error);
res.status(500).json({
message: "Internal server error",
error: error.message,
});
}
};
const uploadGambar = async (image) => {
try {
if (!image) {
throw new Error("Please upload an image!");
}
const file = `/uploads/${image.filename}`;
return file;
} catch (error) {
console.error(error.message);
throw new Error("Failed to upload image.");
}
};
const getPartnership = async (req, res) => {
try {
const [result] = await partnershipModel.getPartnership();
if (result.length === 0) {
return res.status(404).json({
message: "No partnership data",
});
}
res.status(200).json({
message: "Partnership data successfully retrieved",
data: result,
});
} catch (error) {
console.error("Error getting partnership:", error);
res.status(500).json({
message: "Internal server error",
error: error.message,
});
}
};
const deletePartnership = async (req, res) => {
try {
const { id } = req.params;
const [result] = await partnershipModel.deletePartnership(id);
if (result.affectedRows === 0) {
return res.status(404).json({
message: "Partnership data not found",
});
}
res.status(200).json({
message: "Partnership data successfully deleted",
});
} catch (error) {
console.error("Error deleting partnership:", error);
res.status(500).json({
message: "Internal server error",
error: error.message,
});
}
};
const getPartnershipById = async (req, res) => {
try {
const { id } = req.params
const [partnership] = await partnershipModel.getPartnershipById(id);
if (partnership.length === 0) {
return res.status(404).json({
messsage: "partnership not found",
});
}
res.status(200).json({
message: "patnership data was successfully collected",
data: partnership,
});
} catch (error) {
console.error("Error getting partnership results:", error);
res.status(500).json({
message: "Internal server error",
error: error.message,
});
}
}
const updatePartnership = async (req, res) => {
try {
const { id } = req.params;
const { nama_partner } = req.body;
const image = req.file;
const [existing] = await partnershipModel.getPartnershipById(id);
if (existing.length === 0) {
return res.status(404).json({ message: "Partnership not found" });
}
const currentData = existing[0];
let image_path = existing[0].image_path;
if (image) {
const oldFile = path.basename(image_path);
await deleteFileIfExists(oldFile);
image_path = await uploadGambar(image);
}
const updatedData = {
nama_partner: nama_partner?.trim() ? nama_partner : currentData.nama_partner,
image_path,
};
//console.log("form:",nama_partner, "db", currentData.nama_partner, "updated",updatedData.nama_partner)
await partnershipModel.updatePartnership(
id,
updatedData.nama_partner,
updatedData.image_path
);
return res.status(200).json({
message: "Partnership successfully updated",
data: { id, nama_partner: updatedData.nama_partner, image_path: updatedData.image_path },
});
} catch (error) {
console.error("Error updating partnership:", error);
res.status(500).json({
message: "Internal server error",
error: error.message,
});
}
};
module.exports = {
addPartnership,
getPartnership,
getPartnershipById,
updatePartnership,
deletePartnership,
};
+36
View File
@@ -0,0 +1,36 @@
import db from '../helpers/db/db_connection.js';
import bcrypt from 'bcrypt';
export async function addAdmin(
nama_depan,
nama_belakang,
email,
plainpassword,
role
) {
const saltRounds = 10;
const hashedPass = await bcrypt.hash(plainpassword, saltRounds);
const SQLQuery =
'INSERT INTO admin (nama_depan, nama_belakang, email, password, role) VALUES (? ,?, ?, ?, ?)';
return db.execute(SQLQuery, [
nama_depan,
nama_belakang,
email,
hashedPass,
role,
]);
}
export function getAdmin() {
return db.execute('SELECT * FROM admin');
}
export function getAdminByEmail(email) {
return db.execute('SELECT * FROM admin WHERE email = ?', [email]);
}
export function getAdminById(id) {
return db.execute('SELECT * FROM admin WHERE id = ?', [id]);
}
+28
View File
@@ -0,0 +1,28 @@
import db from '../config/db_connection.js';
export function addhasilResearch(nama_project, deskripsi, link_project, image_path) {
const SQLQuery =
'INSERT INTO hasil_research (nama_project, deskripsi, link_project, image_path) VALUES (?,?,?,?)';
return db.execute(SQLQuery, [nama_project, deskripsi, link_project, image_path]);
}
export function gethasilResearch() {
return db.execute('SELECT * FROM hasil_research');
}
export function gethasilResearchById(id) {
return db.execute('SELECT * FROM hasil_research WHERE id = ?', [id]);
}
export function updateHasilResearch(id, nama_project, deskripsi, link_project, image_path) {
const SQLQuery = `
UPDATE hasil_research
SET nama_project = ?, deskripsi = ?, link_project = ?, image_path = ?
WHERE id = ?
`;
return db.execute(SQLQuery, [nama_project, deskripsi, link_project, image_path, id]);
}
export function deletehasilResearch(id) {
return db.execute('DELETE FROM hasil_research WHERE id = ?', [id]);
}
+123
View File
@@ -0,0 +1,123 @@
import db from '../config/db_connection.js';
export function addlowonganMagang(id_mahasiswa, id_lowongan_magang, status) {
const SQLQuery =
'INSERT INTO lamaran_magang (id_mahasiswa, id_lowongan_magang, status, created_at, update_at) VALUES (?, ?, ?, NOW(), NOW())';
return db.execute(SQLQuery, [id_mahasiswa, id_lowongan_magang, status]);
}
export function getAllLamaranMagang() {
const SQLQuery = `
SELECT
lm.id,
m.nama_depan,
m.nama_belakang,
l.posisi,
l.kelompok_peminatan,
lm.status,
lm.created_at
FROM
lamaran_magang lm
JOIN
mahasiswa m ON lm.id_mahasiswa = m.id
JOIN
lowongan_magang l ON lm.id_lowongan_magang = l.id;
`;
return db.execute(SQLQuery);
}
export function getLamaranByID(id_lamaran_magang) {
const SQLQuery = `
SELECT
m.nama_depan,
m.nama_belakang,
m.email,
m.kontak,
m.jurusan,
m.cv_path,
m.portofolio_path,
m.motivasi,
m.relevant_skills,
l.posisi,
lm.status
FROM
lamaran_magang lm
JOIN
mahasiswa m ON lm.id_mahasiswa = m.id
JOIN
lowongan_magang l ON lm.id_lowongan_magang = l.id
WHERE lm.id = ?;
`;
return db.execute(SQLQuery, [id_lamaran_magang]);
}
export function updateStatusLamaran(id_lamaran_magang, status) {
const SQLQuery =
'UPDATE lamaran_magang SET status = ?, update_at = NOW() WHERE id = ?';
return db.execute(SQLQuery, [status, id_lamaran_magang]);
}
export function getDetailLamaranById(idLamaran) {
const SQLQuery = `
SELECT
lm.id AS id_lamaran,
lm.status,
lm.created_at,
lm.update_at,
m.id AS id_mahasiswa,
m.nama_depan,
m.nama_belakang,
m.email,
m.kontak,
m.jurusan,
m.cv_path,
m.portofolio_path,
m.motivasi,
m.relevant_skills,
l.id AS id_lowongan,
l.posisi,
l.kelompok_peminatan,
l.image_path,
l.jobdesk,
l.lokasi,
l.kualifikasi,
l.benefit,
l.durasi_awal,
l.durasi_akhir,
l.status_lowongan,
l.paid,
l.created_at AS created_at_lowongan
FROM lamaran_magang lm
JOIN mahasiswa m ON lm.id_mahasiswa = m.id
JOIN lowongan_magang l ON lm.id_lowongan_magang = l.id
WHERE lm.id = ?;
`;
return db.execute(SQLQuery, [idLamaran]);
}
export function getAllExportData() {
const SQLQuery = `
SELECT
m.nama_depan,
m.nama_belakang,
m.email,
m.kontak,
m.jurusan,
l.posisi,
l.kelompok_peminatan
FROM
lamaran_magang lm
JOIN
mahasiswa m ON lm.id_mahasiswa = m.id
JOIN
lowongan_magang l ON lm.id_lowongan_magang = l.id;
`;
return db.execute(SQLQuery);
}
export function deleteAllLamaranMagang() {
return db.execute('DELETE FROM lamaran_magang');
}
+55
View File
@@ -0,0 +1,55 @@
import db from '../helpers/db/db_connection.js';
export function addlowonganMagang(
id,
posisi,
kelompok_peminatan,
jobdesk,
lokasi,
kualifikasi,
benefit,
durasi_awal,
durasi_akhir,
status_lowongan,
paid,
image_path
) {
const SQLQuery = `INSERT INTO lowongan_magang (id,posisi, kelompok_peminatan, jobdesk, lokasi, kualifikasi, benefit, durasi_awal, durasi_akhir, status_lowongan, paid,created_at,image_path) VALUES (?,?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(),?)`;
return db.execute(SQLQuery, [
id,
posisi,
kelompok_peminatan,
jobdesk,
lokasi,
kualifikasi,
benefit,
durasi_awal,
durasi_akhir,
status_lowongan,
paid,
image_path,
]);
}
export function getAllLowonganMagang() {
return db.execute('SELECT * FROM lowongan_magang');
}
export function getLowonganMagangById(id) {
return db.execute('SELECT * FROM lowongan_magang WHERE id = ?', [id]);
}
export function getLowonganByKelompokPeminatan(kelompok_peminatan) {
return db.execute(
'SELECT * FROM lowongan_magang WHERE kelompok_peminatan = ?',
[kelompok_peminatan]
);
}
export function getAllKelompokPeminatan() {
return db.execute('SELECT DISTINCT kelompok_peminatan FROM lowongan_magang');
}
export function deleteLowonganMagangById(id) {
return db.execute('DELETE FROM lowongan_magang WHERE id = ?', [id]);
}
+45
View File
@@ -0,0 +1,45 @@
import db from '../helpers/db/db_connection.js';
export function addMahasiswa(
nama_depan,
nama_belakang,
email,
kontak,
jurusan,
role,
cv_path,
portofolio_path,
motivasi,
relevant_skills
) {
const SQLQuery =
'INSERT INTO mahasiswa (nama_depan, nama_belakang, email, kontak, jurusan, role, cv_path, portofolio_path, motivasi, relevant_skills) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
return db.execute(SQLQuery, [
nama_depan,
nama_belakang,
email,
kontak,
jurusan,
role,
cv_path,
portofolio_path,
motivasi,
relevant_skills,
]);
}
export function getAllMahasiswa() {
return db.execute('SELECT * FROM mahasiswa');
}
export function getMahasiswaById(id) {
return db.execute('SELECT * FROM mahasiswa WHERE id = ?', [id]);
}
export function getMahasiswaByEmail(email) {
return db.execute('SELECT * FROM mahasiswa WHERE email = ?', [email]);
}
export function deleteAllMahasiswa() {
return db.execute('DELETE FROM mahasiswa');
}
+28
View File
@@ -0,0 +1,28 @@
import db from '../config/db_connection.js';
export function addPartnership(nama_partner, image_path) {
const SQLQuery =
'INSERT INTO partnership (nama_partner,image_path) VALUES (?, ?)';
return db.execute(SQLQuery, [nama_partner, image_path]);
}
export function getPartnership() {
return db.execute('SELECT * FROM partnership');
}
export function getPartnershipById(id) {
return db.execute('SELECT * FROM partnership WHERE id = ?', [id]);
}
export function updatePartnership(id, nama_partner, image_path) {
const SQLQuery = `
UPDATE partnership
SET nama_partner = ?, image_path = ?
WHERE id = ?
`;
return db.execute(SQLQuery, [nama_partner, image_path, id]);
}
export function deletePartnership(id) {
return db.execute('DELETE FROM partnership WHERE id = ?', [id]);
}
Generated Regular → Executable
+2016 -2097
View File
File diff suppressed because it is too large Load Diff
Regular → Executable
+27 -33
View File
@@ -1,47 +1,41 @@
{
"name": "internify-be",
"name": "be",
"version": "1.0.0",
"description": "Internify Backend Server",
"main": "dist/index.js",
"main": "index.js",
"type": "commonjs",
"scripts": {
"build": "rimraf dist && tsc",
"start": "node dist/index.js",
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"db:generate": "prisma generate",
"db:studio": "prisma studio",
"db:migrate": "prisma migrate dev",
"db:seed": "prisma db seed",
"db:reset": "prisma migrate reset"
"dev": "nodemon ./src/index.js",
"start": "node ./src/index.js",
"build": "prisma generate && prisma migrate deploy",
"postinstall": "prisma generate",
"vercel-build": "prisma generate && prisma migrate deploy"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@prisma/client": "^6.19.3",
"bcryptjs": "^3.0.3",
"@prisma/client": "^6.16.2",
"@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.8",
"axios": "^1.9.0",
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.3",
"morgan": "^1.10.0",
"multer": "^2.1.1",
"dotenv": "^16.5.0",
"exceljs": "^4.4.0",
"express": "^4.21.2",
"express-recaptcha": "^5.1.0",
"joi": "^18.0.1",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.2",
"mysql2": "^3.14.0",
"nodemailer": "^7.0.3",
"nodemon": "^3.1.10",
"qs": "^6.14.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"xlsx": "^0.18.5"
"uuid": "^11.1.0"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.10",
"@types/morgan": "^1.9.9",
"@types/multer": "^2.1.0",
"@types/node": "^20.12.12",
"prisma": "^6.19.3",
"rimraf": "^5.0.7",
"ts-node-dev": "^2.0.0",
"tsx": "^4.22.3",
"typescript": "^5.4.5"
"prisma": "^6.16.2"
}
}
-13
View File
@@ -1,13 +0,0 @@
// This file was generated by Prisma and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import 'dotenv/config';
import { defineConfig } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
seed: 'tsx prisma/seed.ts',
},
});
@@ -1,43 +1,139 @@
-- CreateTable
CREATE TABLE `user` (
`id` VARCHAR(191) NOT NULL,
CREATE TABLE `admin` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`nama_depan` VARCHAR(255) NOT NULL,
`nama_belakang` VARCHAR(255) NOT NULL,
`role` ENUM('admin') NOT NULL DEFAULT 'admin',
`email` VARCHAR(255) NOT NULL,
`password_hash` TEXT NOT NULL,
`role` ENUM('ADMIN', 'STUDENT') NOT NULL,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` DATETIME(3) NOT NULL,
`password` TEXT NOT NULL,
`signature` TEXT NULL,
UNIQUE INDEX `user_email_key`(`email`),
UNIQUE INDEX `admin_email_key`(`email`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `admin` (
CREATE TABLE `hasil_research` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`id_user` VARCHAR(191) NOT NULL,
`nama_depan` VARCHAR(255) NOT NULL,
`nama_belakang` VARCHAR(255) NULL,
`image_path` TEXT NOT NULL,
`nama_project` VARCHAR(255) NOT NULL,
`deskripsi` VARCHAR(255) NOT NULL,
`link_project` TEXT NOT NULL,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `lamaran_magang` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`id_mahasiswa` INTEGER NOT NULL,
`id_lowongan_magang` VARCHAR(255) NOT NULL,
`status` ENUM('diterima', 'diproses', 'ditolak') NOT NULL,
`batch` INTEGER NULL,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`update_at` DATETIME(3) NOT NULL,
INDEX `lamaran_magang_id_mahasiswa_foreign`(`id_mahasiswa`),
INDEX `lamaran_magang_id_lowongan_magang_foreign`(`id_lowongan_magang`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `lowongan_magang` (
`id` VARCHAR(255) NOT NULL,
`posisi` VARCHAR(255) NOT NULL,
`kelompok_peminatan` VARCHAR(255) NOT NULL,
`image_path` TEXT NOT NULL,
`jobdesk` TEXT NOT NULL,
`lokasi` VARCHAR(255) NOT NULL,
`kualifikasi` TEXT NOT NULL,
`benefit` TEXT NOT NULL,
`durasi_awal` DATE NOT NULL,
`durasi_akhir` DATE NOT NULL,
`batch` INTEGER NULL,
`status_lowongan` ENUM('dibuka', 'ditutup') NOT NULL,
`paid` ENUM('paid', 'unpaid') NOT NULL,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
UNIQUE INDEX `admin_id_user_key`(`id_user`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `mahasiswa` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`id_user` VARCHAR(191) NOT NULL,
`nama_depan` VARCHAR(255) NOT NULL,
`nama_belakang` VARCHAR(255) NULL,
`email` VARCHAR(255) NOT NULL,
`kontak` VARCHAR(255) NOT NULL,
`jurusan` VARCHAR(255) NOT NULL,
`universitas` VARCHAR(255) NOT NULL,
`negara` VARCHAR(255) NOT NULL,
`role` ENUM('student') NOT NULL DEFAULT 'student',
`cv_path` TEXT NOT NULL,
`portofolio_path` TEXT NOT NULL,
`motivasi` TEXT NOT NULL,
`relevant_skills` VARCHAR(255) NOT NULL,
UNIQUE INDEX `mahasiswa_id_user_key`(`id_user`),
UNIQUE INDEX `mahasiswa_email_key`(`email`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `partnership` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`nama_partner` VARCHAR(255) NOT NULL,
`image_path` TEXT NOT NULL,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `faq` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`pertanyaan` TEXT NOT NULL,
`jawaban` TEXT NOT NULL,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `riwayat_pelamar` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`batch` INTEGER NOT NULL,
`negara` VARCHAR(255) NULL,
`posisi` VARCHAR(255) NULL,
`tahun` INTEGER NOT NULL,
`jumlah` INTEGER NOT NULL,
`pelamar_diterima` INTEGER NOT NULL,
`pelamar_ditolak` INTEGER NOT NULL,
`pelamar_diproses` INTEGER NOT NULL,
`presentase_diterima` DOUBLE NOT NULL,
`presentase_ditolak` DOUBLE NOT NULL,
`presentase_diproses` DOUBLE NOT NULL,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
INDEX `riwayat_pelamar_batch_idx`(`batch`),
INDEX `riwayat_pelamar_negara_idx`(`negara`),
INDEX `riwayat_pelamar_posisi_idx`(`posisi`),
UNIQUE INDEX `riwayat_pelamar_batch_negara_posisi_key`(`batch`, `negara`, `posisi`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `feedback` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`nama` VARCHAR(255) NOT NULL,
`universitas` VARCHAR(255) NOT NULL,
`pesan` TEXT NOT NULL,
`batch` INTEGER NOT NULL,
`posisi` VARCHAR(255) NOT NULL,
`tahun` INTEGER NOT NULL,
`image_path` TEXT NOT NULL,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
@@ -55,55 +151,6 @@ CREATE TABLE `batch` (
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `lowongan_magang` (
`id` VARCHAR(255) NOT NULL,
`id_batch` INTEGER NOT NULL,
`posisi` VARCHAR(255) NOT NULL,
`kelompok_peminatan` VARCHAR(255) NOT NULL,
`image_path` TEXT NOT NULL,
`jobdesk` TEXT NOT NULL,
`lokasi` VARCHAR(255) NOT NULL,
`kualifikasi` TEXT NOT NULL,
`benefit` TEXT NOT NULL,
`durasi_awal` DATE NOT NULL,
`durasi_akhir` DATE NOT NULL,
`status_lowongan` ENUM('DIBUKA', 'DITUTUP') NOT NULL,
`paid` ENUM('PAID', 'UNPAID') NOT NULL,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
INDEX `lowongan_magang_id_batch_foreign`(`id_batch`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `lamaran_magang` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`id_mahasiswa` INTEGER NOT NULL,
`id_lowongan_magang` VARCHAR(255) NOT NULL,
`status` ENUM('DITERIMA', 'DIPROSES', 'DITOLAK') NOT NULL,
`batch` INTEGER NULL,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` DATETIME(3) NOT NULL,
INDEX `lamaran_magang_id_mahasiswa_foreign`(`id_mahasiswa`),
INDEX `lamaran_magang_id_lowongan_magang_foreign`(`id_lowongan_magang`),
INDEX `lamaran_magang_batch_idx`(`batch`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `admin` ADD CONSTRAINT `admin_id_user_foreign` FOREIGN KEY (`id_user`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `mahasiswa` ADD CONSTRAINT `mahasiswa_id_user_foreign` FOREIGN KEY (`id_user`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `lowongan_magang` ADD CONSTRAINT `lowongan_magang_id_batch_foreign` FOREIGN KEY (`id_batch`) REFERENCES `batch`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `lamaran_magang` ADD CONSTRAINT `lamaran_magang_batch_foreign` FOREIGN KEY (`batch`) REFERENCES `batch`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `lamaran_magang` ADD CONSTRAINT `lamaran_magang_id_mahasiswa_foreign` FOREIGN KEY (`id_mahasiswa`) REFERENCES `mahasiswa`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,16 @@
/*
Warnings:
- You are about to drop the column `batch` on the `lowongan_magang` table. All the data in the column will be lost.
- Added the required column `id_batch` to the `lowongan_magang` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE `lowongan_magang` DROP COLUMN `batch`,
ADD COLUMN `id_batch` INTEGER NOT NULL;
-- CreateIndex
CREATE INDEX `lowongan_magang_id_batch_foreign` ON `lowongan_magang`(`id_batch`);
-- AddForeignKey
ALTER TABLE `lowongan_magang` ADD CONSTRAINT `lowongan_magang_id_batch_foreign` FOREIGN KEY (`id_batch`) REFERENCES `batch`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,5 @@
-- CreateIndex
CREATE INDEX `lamaran_magang_batch_idx` ON `lamaran_magang`(`batch`);
-- AddForeignKey
ALTER TABLE `lamaran_magang` ADD CONSTRAINT `lamaran_magang_batch_foreign` FOREIGN KEY (`batch`) REFERENCES `batch`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
@@ -1,69 +0,0 @@
-- CreateTable
CREATE TABLE `riwayat_pelamar` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`batch` INTEGER NOT NULL,
`negara` VARCHAR(255) NULL,
`posisi` VARCHAR(255) NULL,
`tahun` INTEGER NOT NULL,
`jumlah` INTEGER NOT NULL,
`pelamar_diterima` INTEGER NOT NULL,
`pelamar_ditolak` INTEGER NOT NULL,
`pelamar_diproses` INTEGER NOT NULL,
`presentase_diterima` DOUBLE NOT NULL,
`presentase_ditolak` DOUBLE NOT NULL,
`presentase_diproses` DOUBLE NOT NULL,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
INDEX `riwayat_pelamar_batch_idx`(`batch`),
INDEX `riwayat_pelamar_negara_idx`(`negara`),
INDEX `riwayat_pelamar_posisi_idx`(`posisi`),
UNIQUE INDEX `riwayat_pelamar_batch_negara_posisi_key`(`batch`, `negara`, `posisi`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `feedback` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`nama` VARCHAR(255) NOT NULL,
`universitas` VARCHAR(255) NOT NULL,
`pesan` TEXT NOT NULL,
`batch` INTEGER NOT NULL,
`posisi` VARCHAR(255) NOT NULL,
`tahun` INTEGER NOT NULL,
`image_path` TEXT NOT NULL,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `hasil_research` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`image_path` TEXT NOT NULL,
`nama_project` VARCHAR(255) NOT NULL,
`deskripsi` VARCHAR(255) NOT NULL,
`link_project` TEXT NOT NULL,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `partnership` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`nama_partner` VARCHAR(255) NOT NULL,
`image_path` TEXT NOT NULL,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `faq` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`pertanyaan` TEXT NOT NULL,
`jawaban` TEXT NOT NULL,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
View File
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig } from '@prisma/client';
export default defineConfig({
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
});
Regular → Executable
+131 -138
View File
@@ -1,6 +1,11 @@
//docs: https://www.prisma.io/docs/orm/v6/prisma-schema/overview
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client"
provider = "prisma-client-js"
output = "../src/generated/prisma"
}
@@ -9,133 +14,110 @@ datasource db {
url = env("DATABASE_URL")
}
enum Role {
ADMIN
STUDENT
}
enum StatusLamaran {
DITERIMA
DIPROSES
DITOLAK
}
enum PaidStatus {
PAID
UNPAID
}
enum StatusLowongan {
DIBUKA
DITUTUP
}
model User {
id String @id @default(cuid())
email String @unique @db.VarChar(255)
passwordHash String @map("password_hash") @db.Text
role Role
// ...
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
admin Admin?
mahasiswa Mahasiswa?
@@map("user")
}
model Admin {
id Int @id @default(autoincrement())
idUser String @unique @map("id_user")
namaDepan String @map("nama_depan") @db.VarChar(255)
namaBelakang String? @map("nama_belakang") @db.VarChar(255)
user User @relation(fields: [idUser], references: [id], map: "admin_id_user_foreign")
nama_depan String @db.VarChar(255)
nama_belakang String @db.VarChar(255)
role Role @default(admin)
email String @unique @db.VarChar(255)
password String @db.Text
signature String? @db.Text // singature untuk identifikasi auth jwt (best practice)
@@map("admin")
}
model Mahasiswa {
model HasilResearch {
id Int @id @default(autoincrement())
idUser String @unique @map("id_user")
namaDepan String @map("nama_depan") @db.VarChar(255)
namaBelakang String? @map("nama_belakang") @db.VarChar(255)
kontak String @db.VarChar(255)
jurusan String @db.VarChar(255)
universitas String @db.VarChar(255)
negara String @db.VarChar(255)
cvPath String @map("cv_path") @db.Text
portofolioPath String @map("portofolio_path") @db.Text
motivasi String @db.Text
relevantSkills String @map("relevant_skills") @db.VarChar(255)
image_path String @db.Text
nama_project String @db.VarChar(255)
deskripsi String @db.VarChar(255)
link_project String @db.Text
created_at DateTime @default(now())
user User @relation(fields: [idUser], references: [id], map: "mahasiswa_id_user_foreign")
lamaranMagangs LamaranMagang[]
@@map("mahasiswa")
}
model Batch {
id Int @id @default(autoincrement())
batchNumber Int @unique @map("batch_number")
isActive Boolean @default(false) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
lowonganMagangs LowonganMagang[]
lamaranMagangs LamaranMagang[]
@@index([isActive])
@@index([batchNumber])
@@map("batch")
}
model LowonganMagang {
id String @id @db.VarChar(255)
idBatch Int @map("id_batch")
posisi String @db.VarChar(255)
kelompokPeminatan String @map("kelompok_peminatan") @db.VarChar(255)
imagePath String @map("image_path") @db.Text
jobdesk String @db.Text
lokasi String @db.VarChar(255)
kualifikasi String @db.Text
benefit String @db.Text
durasiAwal DateTime @map("durasi_awal") @db.Date
durasiAkhir DateTime @map("durasi_akhir") @db.Date
statusLowongan StatusLowongan @map("status_lowongan")
paid PaidStatus
createdAt DateTime @default(now()) @map("created_at")
batch Batch @relation(fields: [idBatch], references: [id], map: "lowongan_magang_id_batch_foreign")
lamaranMagangs LamaranMagang[]
@@index([idBatch], map: "lowongan_magang_id_batch_foreign")
@@map("lowongan_magang")
@@map("hasil_research")
}
model LamaranMagang {
id Int @id @default(autoincrement())
idMahasiswa Int @map("id_mahasiswa")
idLowonganMagang String @map("id_lowongan_magang") @db.VarChar(255)
id_mahasiswa Int
id_lowongan_magang String @db.VarChar(255)
status StatusLamaran
batch Int?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
created_at DateTime @default(now())
update_at DateTime
batchData Batch? @relation(fields: [batch], references: [id], map: "lamaran_magang_batch_foreign")
mahasiswa Mahasiswa @relation(fields: [idMahasiswa], references: [id], map: "lamaran_magang_id_mahasiswa_foreign")
lowonganMagang LowonganMagang @relation(fields: [idLowonganMagang], references: [id], map: "lamaran_magang_id_lowongan_magang_foreign")
batch_data Batch? @relation(fields: [batch], references: [id], map: "lamaran_magang_batch_foreign")
mahasiswa Mahasiswa @relation(fields: [id_mahasiswa], references: [id], map: "lamaran_magang_id_mahasiswa_foreign")
lowongan_magang LowonganMagang @relation(fields: [id_lowongan_magang], references: [id], map: "lamaran_magang_id_lowongan_magang_foreign")
@@index([idMahasiswa], map: "lamaran_magang_id_mahasiswa_foreign")
@@index([idLowonganMagang], map: "lamaran_magang_id_lowongan_magang_foreign")
@@index([id_mahasiswa], map: "lamaran_magang_id_mahasiswa_foreign")
@@index([id_lowongan_magang], map: "lamaran_magang_id_lowongan_magang_foreign")
@@index([batch])
@@map("lamaran_magang")
}
model LowonganMagang {
id String @id @db.VarChar(255)
posisi String @db.VarChar(255)
kelompok_peminatan String @db.VarChar(255)
image_path String @db.Text
jobdesk String @db.Text
lokasi String @db.VarChar(255)
kualifikasi String @db.Text
benefit String @db.Text
durasi_awal DateTime @db.Date
durasi_akhir DateTime @db.Date
id_batch Int
batch Batch @relation(fields: [id_batch], references: [id], map: "lowongan_magang_id_batch_foreign")
status_lowongan StatusLowongan
paid PaidStatus
created_at DateTime @default(now())
lamaran_magang LamaranMagang[]
@@index([id_batch], map: "lowongan_magang_id_batch_foreign")
@@map("lowongan_magang")
}
model Mahasiswa {
id Int @id @default(autoincrement())
nama_depan String @db.VarChar(255)
nama_belakang String? @db.VarChar(255)
email String @unique @db.VarChar(255)
kontak String @db.VarChar(255)
jurusan String @db.VarChar(255)
universitas String @db.VarChar(255)
negara String @db.VarChar(255)
role StudentRole @default(student)
cv_path String @db.Text
portofolio_path String @db.Text
motivasi String @db.Text
relevant_skills String @db.VarChar(255)
lamaran_magang LamaranMagang[]
@@map("mahasiswa")
}
model Partnership {
id Int @id @default(autoincrement())
nama_partner String @db.VarChar(255)
image_path String @db.Text
created_at DateTime @default(now())
@@map("partnership")
}
model Faq {
id Int @id @default(autoincrement())
pertanyaan String @db.Text
jawaban String @db.Text
created_at DateTime @default(now())
@@map("faq")
}
model RiwayatPelamar {
id Int @id @default(autoincrement())
batch Int
@@ -143,13 +125,14 @@ model RiwayatPelamar {
posisi String? @db.VarChar(255)
tahun Int
jumlah Int
pelamarDiterima Int @map("pelamar_diterima")
pelamarDitolak Int @map("pelamar_ditolak")
pelamarDiproses Int @map("pelamar_diproses")
presentaseDiterima Float @map("presentase_diterima")
presentaseDitolak Float @map("presentase_ditolak")
presentaseDiproses Float @map("presentase_diproses")
createdAt DateTime @default(now()) @map("created_at")
pelamar_diterima Int
pelamar_ditolak Int
pelamar_diproses Int
presentase_diterima Float
presentase_ditolak Float
presentase_diproses Float
created_at DateTime @default(now())
@@unique([batch, negara, posisi])
@@index([batch])
@@ -166,37 +149,47 @@ model Feedback {
batch Int
posisi String @db.VarChar(255)
tahun Int
imagePath String @map("image_path") @db.Text
createdAt DateTime @default(now()) @map("created_at")
image_path String @db.Text
created_at DateTime @default(now())
@@map("feedback")
}
model HasilResearch {
model Batch {
id Int @id @default(autoincrement())
imagePath String @map("image_path") @db.Text
namaProject String @map("nama_project") @db.VarChar(255)
deskripsi String @db.VarChar(255)
linkProject String @map("link_project") @db.Text
createdAt DateTime @default(now()) @map("created_at")
batch_number Int @unique
is_active Boolean @default(false)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
lowongan_magang LowonganMagang[]
lamaranMagangs LamaranMagang[]
@@map("hasil_research")
@@index([is_active])
@@index([batch_number])
@@map("batch")
}
model Partnership {
id Int @id @default(autoincrement())
namaPartner String @map("nama_partner") @db.VarChar(255)
imagePath String @map("image_path") @db.Text
createdAt DateTime @default(now()) @map("created_at")
@@map("partnership")
enum Role {
admin
}
model Faq {
id Int @id @default(autoincrement())
pertanyaan String @db.Text
jawaban String @db.Text
createdAt DateTime @default(now()) @map("created_at")
@@map("faq")
enum StudentRole {
student
}
enum StatusLamaran {
diterima
diproses
ditolak
}
enum StatusLowongan {
dibuka
ditutup
}
enum PaidStatus {
paid
unpaid
}
-48
View File
@@ -1,48 +0,0 @@
import 'dotenv/config';
import { PrismaClient } from '../src/generated/prisma/client';
import bcrypt from 'bcryptjs';
const prisma = new PrismaClient();
async function main() {
console.log('Start seeding Internify database...');
const adminEmail = 'admin@internify.com';
const adminPassword = 'AdminInternify123!';
const passwordHash = await bcrypt.hash(adminPassword, 10);
// Cek Apakah ada si Admin
const existingAdmin = await prisma.user.findUnique({
where: { email: adminEmail }
});
if (!existingAdmin) {
const adminUser = await prisma.user.create({
data: {
email: adminEmail,
passwordHash: passwordHash,
role: 'ADMIN',
admin: {
create: {
namaDepan: 'Super',
namaBelakang: 'Admin',
}
}
}
});
console.log(`Admin user created: ${adminEmail} (password: ${adminPassword})`);
} else {
console.log(`Admin user already exists: ${adminEmail}`);
}
console.log('Seeding finished');
}
main()
.catch((error) => {
console.error('Seeding failed:', error);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
-249
View File
@@ -1,249 +0,0 @@
import 'dotenv/config';
import { PrismaClient } from '../src/generated/prisma/client';
import bcrypt from 'bcryptjs';
const prisma = new PrismaClient();
async function main() {
console.log('=== STARTING DEVELOPER DUMMY SEEDING ===');
const adminEmail = 'admin@internify.com';
const adminPassword = 'AdminInternify123!';
const passwordHash = await bcrypt.hash(adminPassword, 10);
let adminUser = await prisma.user.findUnique({ where: { email: adminEmail } });
if (!adminUser) {
adminUser = await prisma.user.create({
data: {
email: adminEmail,
passwordHash,
role: 'ADMIN',
admin: {
create: {
namaDepan: 'Super',
namaBelakang: 'Admin'
}
}
}
});
console.log(' Admin created:', adminEmail);
} else {
console.log(' Admin already exists:', adminEmail);
}
console.log(' Seeding Batches...');
const batchesData = [
{ batchNumber: 1, isActive: false },
{ batchNumber: 2, isActive: false },
{ batchNumber: 3, isActive: true } // Active Batch
];
const batches: any[] = [];
for (const b of batchesData) {
let dbBatch = await prisma.batch.findUnique({ where: { batchNumber: b.batchNumber } });
if (!dbBatch) {
dbBatch = await prisma.batch.create({ data: b });
}
batches.push(dbBatch);
}
console.log(` Seeding Batches complete! Created/Found ${batches.length} batches.`);
const activeBatch = batches.find(b => b.isActive) || batches[2];
console.log(' Seeding Student Users...');
const studentsData = [
{
email: 'alice@example.com',
namaDepan: 'Alice',
namaBelakang: 'Smith',
kontak: '08111222333',
jurusan: 'Informatika',
universitas: 'Institut Teknologi Bandung',
negara: 'Indonesia',
cvPath: '/uploads/docs/alice-cv.pdf',
portofolioPath: '/uploads/docs/alice-portfolio.pdf',
motivasi: 'Saya sangat tertarik dengan UI/UX design dan ingin mempraktekkannya.',
relevantSkills: 'Figma, Adobe XD, User Research'
},
{
email: 'bob@example.com',
namaDepan: 'Bob',
namaBelakang: 'Johnson',
kontak: '08222333444',
jurusan: 'Sistem Informasi',
universitas: 'Universitas Indonesia',
negara: 'Indonesia',
cvPath: '/uploads/docs/bob-cv.pdf',
portofolioPath: '/uploads/docs/bob-portfolio.pdf',
motivasi: 'Ingin mengembangkan keahlian React dan Tailwind CSS.',
relevantSkills: 'React, HTML, CSS, JavaScript'
},
{
email: 'charlie@example.com',
namaDepan: 'Charlie',
namaBelakang: 'Brown',
kontak: '08333444555',
jurusan: 'Teknik Komputer',
universitas: 'Telkom University',
negara: 'Indonesia',
cvPath: '/uploads/docs/charlie-cv.pdf',
portofolioPath: '/uploads/docs/charlie-portfolio.pdf',
motivasi: 'Suka membangun server scalable dan merancang arsitektur API.',
relevantSkills: 'Node.js, Express, MySQL, TypeScript'
}
];
const mahasiswaList: any[] = [];
for (const s of studentsData) {
let user = await prisma.user.findUnique({
where: { email: s.email },
include: { mahasiswa: true }
});
if (!user) {
const hash = await bcrypt.hash('Student123!', 10);
user = await prisma.user.create({
data: {
email: s.email,
passwordHash: hash,
role: 'STUDENT',
mahasiswa: {
create: {
namaDepan: s.namaDepan,
namaBelakang: s.namaBelakang,
kontak: s.kontak,
jurusan: s.jurusan,
universitas: s.universitas,
negara: s.negara,
cvPath: s.cvPath,
portofolioPath: s.portofolioPath,
motivasi: s.motivasi,
relevantSkills: s.relevantSkills
}
}
},
include: { mahasiswa: true }
});
}
mahasiswaList.push(user.mahasiswa);
}
console.log(` Student seeding complete! Created/Found ${mahasiswaList.length} students.`);
console.log(' Seeding Lowongan Listings...');
const lowongansData = [
{
id: 'lowongan-ui-ux',
posisi: 'UI/UX Designer Intern',
kelompokPeminatan: 'Design & Creative',
imagePath: '/uploads/images/ui-ux-design.png',
jobdesk: '<p>Mendesain antarmuka aplikasi mobile dan web, melakukan user testing, dan wireframing.</p>',
lokasi: 'Onsite - Bandung',
kualifikasi: 'Menguasai Figma, memiliki portofolio desain UI/UX, kreatif.',
benefit: 'Sertifikat magang resmi, uang transport harian, mentoring 1-on-1.',
paid: 'PAID' as any,
statusLowongan: 'DIBUKA' as any
},
{
id: 'lowongan-frontend',
posisi: 'Frontend Developer Intern',
kelompokPeminatan: 'Web Engineering',
imagePath: '/uploads/images/frontend-dev.png',
jobdesk: '<p>Mengembangkan antarmuka responsif menggunakan React dan Tailwind CSS.</p>',
lokasi: 'Remote',
kualifikasi: 'Memahami React JS, ES6 JavaScript, HTML, CSS.',
benefit: 'Sertifikat magang resmi, jam kerja fleksibel, free access learning course.',
paid: 'UNPAID' as any,
statusLowongan: 'DIBUKA' as any
},
{
id: 'lowongan-backend',
posisi: 'Backend Developer Intern',
kelompokPeminatan: 'Web Engineering',
imagePath: '/uploads/images/backend-dev.png',
jobdesk: '<p>Membangun REST API, mengoptimalkan query database, dan integrasi backend services.</p>',
lokasi: 'Hybrid - Jakarta',
kualifikasi: 'Memahami Node.js, Express, dan Relational Database seperti MySQL.',
benefit: 'Sertifikat magang resmi, konversi SKS, coworking space access.',
paid: 'PAID' as any,
statusLowongan: 'DIBUKA' as any
}
];
const lowonganList: any[] = [];
for (const l of lowongansData) {
let dbLowongan = await prisma.lowonganMagang.findUnique({ where: { id: l.id } });
if (!dbLowongan) {
dbLowongan = await prisma.lowonganMagang.create({
data: {
id: l.id,
idBatch: activeBatch.id,
posisi: l.posisi,
kelompokPeminatan: l.kelompokPeminatan,
imagePath: l.imagePath,
jobdesk: l.jobdesk,
lokasi: l.lokasi,
kualifikasi: l.kualifikasi,
benefit: l.benefit,
durasiAwal: new Date(),
durasiAkhir: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days from now
statusLowongan: l.statusLowongan,
paid: l.paid
}
});
}
lowonganList.push(dbLowongan);
}
console.log(` Lowongan seeding complete! Created/Found ${lowonganList.length} listings.`);
console.log(' Seeding Lamaran Records...');
const lamaranData = [
{
idMahasiswa: mahasiswaList[0].id,
idLowonganMagang: lowonganList[0].id,
status: 'DIPROSES' as any
},
{
idMahasiswa: mahasiswaList[1].id,
idLowonganMagang: lowonganList[1].id,
status: 'DIPROSES' as any
},
{
idMahasiswa: mahasiswaList[2].id,
idLowonganMagang: lowonganList[2].id,
status: 'DIPROSES' as any
}
];
let lamaranCount = 0;
for (const lam of lamaranData) {
const existing = await prisma.lamaranMagang.findFirst({
where: {
idMahasiswa: lam.idMahasiswa,
idLowonganMagang: lam.idLowonganMagang
}
});
if (!existing) {
await prisma.lamaranMagang.create({
data: {
idMahasiswa: lam.idMahasiswa,
idLowonganMagang: lam.idLowonganMagang,
status: lam.status,
batch: activeBatch.id
}
});
lamaranCount++;
}
}
console.log(` Lamaran seeding complete! Created ${lamaranCount} new applications.`);
console.log('\n=== DEVELOPER SEEDING COMPLETED SUCCESSFULLY ===');
}
main()
.catch(e => {
console.error('Seeding failed:', e);
})
.finally(async () => {
await prisma.$disconnect();
});
-69
View File
@@ -1,69 +0,0 @@
import path from 'path';
import express, { Request, Response, NextFunction } from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import routes from './routes';
import { errorHandler, CustomError } from './middleware/errorHandler';
import { setupSwaggerDocs } from './docs/swagger';
const app = express();
app.use(helmet({
crossOriginResourcePolicy: { policy: 'cross-origin' }
}));
app.use(cors({
origin: true,
credentials: true,
}));
app.use(morgan('dev'));
// File upload statis biar bisa diakses lewat /uploads atau /api/uploads
app.use('/uploads', express.static(path.join(__dirname, '../uploads')));
app.use('/api/uploads', express.static(path.join(__dirname, '../uploads')));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
/**
* @swagger
* /:
* get:
* summary: Endpoint root API
* description: Endpoint dasar.
* responses:
* 200:
* description: Informasi umum API berhasil diambil
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* example: Welcome to the Internify API
* documentation:
* type: string
* example: Check /api/health untuk status
*/
app.get('/', (_req: Request, res: Response) => {
res.status(200).json({
message: 'Welcome to the Internify API',
documentation: 'Check /api/health untuk status',
});
});
app.use('/api', routes);
setupSwaggerDocs(app);
app.use((req: Request, _res: Response, next: NextFunction) => {
const error: CustomError = new Error(`Not Found - ${req.originalUrl}`);
error.statusCode = 404;
next(error);
});
app.use(errorHandler);
export default app;
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

+111
View File
@@ -0,0 +1,111 @@
CREATE TABLE `admin` (
`id` bigint(20) NOT NULL,
`nama_depan` varchar(255) NOT NULL,
`nama_belakang` varchar(255) NOT NULL,
`role` enum('admin') NOT NULL,
`email` varchar(255) NOT NULL,
`password` text NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `hasil_research` (
`id` bigint(20) NOT NULL,
`image_path` text NOT NULL,
`nama_project` varchar(255) NOT NULL,
`deskripsi` varchar(255) NOT NULL,
`link_project` text NOT NULL,
`created_at` datetime NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `lamaran_magang` (
`id` bigint(20) NOT NULL,
`id_mahasiswa` bigint(20) NOT NULL,
`id_lowongan_magang` varchar(255) NOT NULL,
`status` enum('diterima','diproses','ditolak') NOT NULL,
`created_at` datetime NOT NULL,
`update_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `lowongan_magang` (
`id` varchar(255) NOT NULL,
`posisi` varchar(255) NOT NULL,
`kelompok_peminatan` varchar(255) NOT NULL,
`image_path` text NOT NULL,
`jobdesk` text NOT NULL,
`lokasi` varchar(255) NOT NULL,
`kualifikasi` text NOT NULL,
`benefit` text NOT NULL,
`durasi_awal` date NOT NULL,
`durasi_akhir` date NOT NULL,
`status_lowongan` enum('dibuka','ditutup') NOT NULL,
`paid` enum('paid','unpaid') NOT NULL,
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `mahasiswa` (
`id` bigint(20) NOT NULL,
`nama_depan` varchar(255) NOT NULL,
`nama_belakang` varchar(255) DEFAULT NULL,
`email` varchar(255) NOT NULL,
`kontak` varchar(255) NOT NULL,
`jurusan` varchar(255) NOT NULL,
`role` enum('student') NOT NULL,
`cv_path` text NOT NULL,
`portofolio_path` text NOT NULL,
`motivasi` text NOT NULL,
`relevant_skills` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `partnership` (
`id` bigint(20) NOT NULL,
`nama_partner` varchar(255) NOT NULL,
`image_path` text NOT NULL,
`created_at` datetime NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
ALTER TABLE `admin`
ADD PRIMARY KEY (`id`);
ALTER TABLE `hasil_research`
ADD PRIMARY KEY (`id`);
ALTER TABLE `lamaran_magang`
ADD PRIMARY KEY (`id`),
ADD KEY `lamaran_magang_id_mahasiswa_foreign` (`id_mahasiswa`),
ADD KEY `lamaran_magang_id_lowongan_magang_foreign` (`id_lowongan_magang`);
ALTER TABLE `lowongan_magang`
ADD PRIMARY KEY (`id`);
ALTER TABLE `mahasiswa`
ADD PRIMARY KEY (`id`);
ALTER TABLE `partnership`
ADD PRIMARY KEY (`id`);
ALTER TABLE `admin`
MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
ALTER TABLE `hasil_research`
MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
ALTER TABLE `lamaran_magang`
MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
ALTER TABLE `mahasiswa`
MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
ALTER TABLE `partnership`
MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT;
ALTER TABLE `lamaran_magang`
ADD CONSTRAINT `lamaran_magang_id_lowongan_magang_foreign` FOREIGN KEY (`id_lowongan_magang`) REFERENCES `lowongan_magang` (`id`),
ADD CONSTRAINT `lamaran_magang_id_mahasiswa_foreign` FOREIGN KEY (`id_mahasiswa`) REFERENCES `mahasiswa` (`id`);
COMMIT;
+180
View File
@@ -0,0 +1,180 @@
const swaggerJsDoc = require("swagger-jsdoc");
const swaggerUi = require("swagger-ui-express");
const path = require('path');
const swaggerOptions = {
definition: {
openapi: "3.0.0",
info: {
title: "Internify by HUMIC Engineering APIs",
version: "1.2.0",
description: "Comprehensive REST API for managing internship applications, job postings, partnerships, and feedback for the Internify web application. This API provides endpoints for admin management, authentication, internship vacancy management, application processing, student data, partnerships, research results, FAQs, feedback, and batch management. Created by Ramadhana & Yohanes",
contact: {
name: "HUMIC Engineering",
url: "https://humic.dev"
}
},
servers: [
{
url: process.env.NODE_ENV === 'production'
? 'https://internify-ruddy.vercel.app/'
: `http://localhost:${process.env.EXPRESS_PORT || 9000}`,
description: process.env.NODE_ENV === 'production' ? 'Production server' : 'Development server'
}
],
tags: [
{
name: "Auth",
description: "Authentication endpoints for admin login and profile retrieval"
},
{
name: "Admin",
description: "Admin account management endpoints"
},
{
name: "Batch",
description: "Batch management for organizing internship cohorts"
},
{
name: "Lowongan Magang",
description: "Internship vacancy/job posting management endpoints"
},
{
name: "Lamaran Magang",
description: "Internship application management and statistics endpoints"
},
{
name: "Mahasiswa",
description: "Student data management endpoints"
},
{
name: "Partnership",
description: "Partner organization management endpoints"
},
{
name: "Hasil Research",
description: "Internship research results and project showcase endpoints"
},
{
name: "FAQ",
description: "Frequently Asked Questions management endpoints"
},
{
name: "Feedback",
description: "Internship feedback and testimonial management endpoints"
}
],
components: {
securitySchemes: {
bearerAuth: {
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
description: "JWT token obtained from the /auth-api/login endpoint"
},
},
responses: {
UnauthorizedError: {
description: "Access token is missing or invalid",
content: {
"application/json": {
schema: {
type: "object",
properties: {
message: {
type: "string",
example: "Unauthorized access"
}
}
}
}
}
},
ForbiddenError: {
description: "User does not have permission to access this resource",
content: {
"application/json": {
schema: {
type: "object",
properties: {
message: {
type: "string",
example: "Access denied: Admin only"
}
}
}
}
}
},
NotFoundError: {
description: "The requested resource was not found",
content: {
"application/json": {
schema: {
type: "object",
properties: {
message: {
type: "string",
example: "Resource not found"
}
}
}
}
}
},
ValidationError: {
description: "Request validation failed",
content: {
"application/json": {
schema: {
type: "object",
properties: {
message: {
type: "string",
example: "Validation error"
},
errors: {
type: "array",
items: {
type: "string"
}
}
}
}
}
}
},
InternalServerError: {
description: "Internal server error",
content: {
"application/json": {
schema: {
type: "object",
properties: {
message: {
type: "string",
example: "Internal server error"
}
}
}
}
}
}
}
},
},
apis: [
path.join(__dirname, '../routes/*.js'),
path.join(__dirname, '../controllers/*.js'),
],
};
const swaggerSpec = swaggerJsDoc(swaggerOptions);
const setupSwaggerDocs = (app) => {
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));
console.log("Swagger UI available at /api-docs");
};
module.exports = setupSwaggerDocs;
-211
View File
@@ -1,211 +0,0 @@
import path from 'path';
import type { Application } from 'express';
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const port = process.env.PORT || '5000';
const swaggerOptions = {
definition: {
openapi: '3.0.0',
info: {
title: 'Internify LMS API - HUMIC Engineering',
version: '1.0.0',
description: 'Dokumentasi REST API Internify LMS untuk autentikasi, pengelolaan lowongan magang, lamaran, dan konten platform.',
contact: {
name: 'HUMIC Engineering',
url: 'https://humic.telkomuniversity.ac.id/'
}
},
servers: [
{
url: process.env.NODE_ENV === 'production'
? 'https://your-production-domain.example.com'
: `http://localhost:${port}`,
description: process.env.NODE_ENV === 'production'
? 'Prod'
: 'Dev'
}
],
tags: [
{
name: 'Auth',
description: 'Endpoint autentikasi untuk login admin/peserta dan profil pengguna.'
},
{
name: 'Admin',
description: 'Endpoint utilitas dan pengelolaan sistem yang terkait admin.'
},
{
name: 'Batch',
description: 'Endpoint pengelolaan batch/angkatan magang.'
},
{
name: 'Lowongan Magang',
description: 'Endpoint pengelolaan lowongan magang.'
},
{
name: 'Lamaran Magang',
description: 'Endpoint pengelolaan lamaran magang.'
},
{
name: 'Mahasiswa',
description: 'Endpoint pengelolaan data mahasiswa/peserta.'
},
{
name: 'Partnership',
description: 'Endpoint pengelolaan data mitra/partnership.'
},
{
name: 'Hasil Research',
description: 'Endpoint pengelolaan hasil research/proyek.'
},
{
name: 'FAQ',
description: 'Endpoint pengelolaan FAQ (pertanyaan yang sering diajukan).'
},
{
name: 'Feedback',
description: 'Endpoint pengelolaan feedback/testimoni peserta.'
}
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'Token JWT yang diperoleh dari endpoint /api/auth/login'
}
},
responses: {
UnauthorizedError: {
description: 'Token akses tidak ada atau tidak valid',
content: {
'application/json': {
examples: {
tokenMissing: {
value: {
status: 'error',
statusCode: 401,
message: 'Not authorized to access this route, token is missing'
}
},
tokenInvalid: {
value: {
status: 'error',
statusCode: 401,
message: 'Not authorized to access this route, token is invalid or expired'
}
},
userNotExist: {
value: {
status: 'error',
statusCode: 401,
message: 'User belonging to this token no longer exists'
}
}
}
}
}
},
ForbiddenError: {
description: 'Pengguna tidak memiliki izin untuk mengakses resource ini',
content: {
'application/json': {
example: {
status: 'error',
statusCode: 403,
message: 'You do not have permission to perform this action'
}
}
}
},
NotFoundError: {
description: 'Resource yang diminta tidak ditemukan',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
status: {
type: 'string',
example: 'error'
},
statusCode: {
type: 'integer',
example: 404
},
message: {
type: 'string',
example: 'Not Found - /api/endpoint-yang-tidak-ada'
}
}
}
}
}
},
ValidationError: {
description: 'Validasi request gagal',
content: {
'application/json': {
example: {
status: 'error',
statusCode: 400,
message: 'Mohon lengkapi semua field wajib yang diperlukan'
}
}
}
},
InternalServerError: {
description: 'Terjadi kesalahan pada server internal',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
status: {
type: 'string',
example: 'error'
},
statusCode: {
type: 'integer',
example: 500
},
message: {
type: 'string',
example: 'Internal Server Error'
}
}
}
}
}
}
}
}
},
apis: [
path.join(__dirname, '../app.ts').replace(/\\/g, '/'),
path.join(__dirname, '../app.js').replace(/\\/g, '/'),
path.join(__dirname, '../routes/*.ts').replace(/\\/g, '/'),
path.join(__dirname, '../controllers/*.ts').replace(/\\/g, '/'),
path.join(__dirname, '../routes/*.js').replace(/\\/g, '/'),
path.join(__dirname, '../controllers/*.js').replace(/\\/g, '/')
]
};
const swaggerSpec = swaggerJsdoc(swaggerOptions);
export const setupSwaggerDocs = (app: Application) => {
const swaggerEnabledValue = process.env.SWAGGER_ENABLE ?? process.env.SWAGGER_ENABLE ?? 'true';
const isSwaggerEnabled = swaggerEnabledValue.toLowerCase() === 'true';
if (!isSwaggerEnabled) {
return;
}
app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
console.log('Swagger UI available at /api/docs');
};
export default setupSwaggerDocs;
+7
View File
@@ -0,0 +1,7 @@
const { PrismaClient } = require('../../generated/prisma');
const prisma = new PrismaClient({
log: ['query', 'info', 'warn', 'error'],
});
module.exports = prisma
+24
View File
@@ -0,0 +1,24 @@
const { PrismaClient } = require("generated/prisma/index.js");
const prisma = new PrismaClient({
log: [
{
emit: 'event',
level: 'query',
},
{
emit: 'stdout',
level: 'error',
},
{
emit: 'stdout',
level: 'info',
},
{
emit: 'stdout',
level: 'warn',
},
],
})
module.exports = { prisma };
+9
View File
@@ -0,0 +1,9 @@
const CommonError = require('./common_error');
class BadRequestError extends CommonError {
constructor(message) {
super(message || 'Bad Request');
}
}
module.exports = BadRequestError;
+7
View File
@@ -0,0 +1,7 @@
class CommonError extends Error {
constructor(message) {
super(message);
}
}
module.exports = CommonError;
+9
View File
@@ -0,0 +1,9 @@
const CommonError = require('./common_error');
class ConflictError extends CommonError {
constructor(message) {
super(message || 'Conflict');
}
}
module.exports = ConflictError;
+9
View File
@@ -0,0 +1,9 @@
const CommonError = require('./common_error');
class ExpectationFailedError extends CommonError {
constructor(message) {
super(message || 'Expectation Failed');
}
}
module.exports = ExpectationFailedError;
+9
View File
@@ -0,0 +1,9 @@
const CommonError = require('./common_error');
class ForbiddenError extends CommonError {
constructor(message) {
super(message || 'Forbidden');
}
}
module.exports = ForbiddenError;
+9
View File
@@ -0,0 +1,9 @@
const CommonError = require('./common_error');
class GatewayTimeoutError extends CommonError {
constructor(message) {
super(message || 'Gateway Timeout');
}
}
module.exports = GatewayTimeoutError;
+23
View File
@@ -0,0 +1,23 @@
const ConflictError = require('./conflict_error');
const ExpectationFailedError = require('./expectation_failed_error');
const ForbiddenError = require('./forbidden_error');
const InternalServerError = require('./internal_server_error');
const NotFoundError = require('./not_found_error');
const UnauthorizedError = require('./unauthorized_error');
const ServiceUnavailableError = require('./service_unavailable_error');
const GatewayTimeoutError = require('./gateway_timeout_error');
const BadRequestError = require('./bad_request_error');
const ValidationError = require('./validation_error');
module.exports = {
ConflictError,
ExpectationFailedError,
ForbiddenError,
InternalServerError,
NotFoundError,
UnauthorizedError,
ServiceUnavailableError,
GatewayTimeoutError,
BadRequestError,
ValidationError
};
+9
View File
@@ -0,0 +1,9 @@
const CommonError = require('./common_error');
class InternalServerError extends CommonError {
constructor(message) {
super(message || 'Internal Server Error');
}
}
module.exports = InternalServerError;
+9
View File
@@ -0,0 +1,9 @@
const CommonError = require('./common_error');
class NotFoundError extends CommonError {
constructor(message) {
super(message || 'Not Found');
}
}
module.exports = NotFoundError;
+9
View File
@@ -0,0 +1,9 @@
const CommonError = require('./common_error');
class ServiceUnavailableError extends CommonError {
constructor(message) {
super(message || 'Service Unavailable');
}
}
module.exports = ServiceUnavailableError;
+9
View File
@@ -0,0 +1,9 @@
const CommonError = require('./common_error');
class Unauthorized extends CommonError {
constructor(message) {
super(message || 'Unauthorized');
}
}
module.exports = Unauthorized;
+8
View File
@@ -0,0 +1,8 @@
class ValidationError extends Error {
constructor(message, errors) {
super(message || 'Validation Error');
this.errors = errors
}
}
module.exports = ValidationError;
+21
View File
@@ -0,0 +1,21 @@
const ERROR = {
'BAD_REQUEST': 400,
'NOT_FOUND': 404,
'INTERNAL_ERROR': 500,
'CONFLICT': 409,
'EXPECTATION_FAILED': 417,
'FORBIDDEN': 403,
'UNAUTHORIZED': 401,
'SERVICE_UNAVAILABLE': 503,
'GATEWAY_TIMEOUT': 504
};
const SUCCESS = {
'OK': 200,
'CREATED': 201
};
module.exports = {
ERROR,
SUCCESS
};
+30
View File
@@ -0,0 +1,30 @@
const dotnev = require('dotenv');
dotnev.config({ path: '.env' })
const config = {
port: process.env.EXPRESS_PORT,
jwtPrivateKey: process.env.JWT_PRIVATE_KEY,
jwtPublicKey: process.env.JWT_PUBLIC_KEY,
nodeEnv: process.env.NODE_ENV,
mysqlConfig: {
host: process.env.MYSQL_HOST,
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
database: process.env.MYSQL_DATABASE,
dialect: 'mysql',
logging: false
},
nodemailerConfig: {
email: process.env.NODEMAILER_EMAIL,
password: process.env.NODEMAILER_PASSWORD
},
cloudinaryConfig: {
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
secure: true
}
};
module.exports = { config };
+53
View File
@@ -0,0 +1,53 @@
const path = require('path');
const fs = require('fs').promises;
const fsSync = require('fs');
async function deleteFileIfExists(fileName) {
try {
const uploadDir = process.env.NODE_ENV === 'production'
? '/tmp/uploads'
: path.join(__dirname, '..', '..', 'uploads');
const fullPath = path.join(uploadDir, fileName);
await fs.access(fullPath);
await fs.unlink(fullPath);
console.log(`File dihapus: ${fullPath}`);
} catch (err) {
if (err.code === 'ENOENT') {
console.warn(`File tidak ditemukan: ${fileName}`);
} else {
console.error(`Gagal menghapus file (${fileName}):`, err.message);
}
}
}
function checkFileExists(filePath) {
try {
if (!filePath) return false;
const uploadDir = process.env.NODE_ENV === 'production'
? '/tmp/uploads'
: path.join(__dirname, '..', '..', 'uploads');
const fileName = path.basename(filePath);
const fullPath = path.join(uploadDir, fileName);
return fsSync.existsSync(fullPath);
} catch (err) {
console.error(`Error checking file existence: ${err.message}`);
return false;
}
}
function validateImagePath(imagePath) {
if (!imagePath) return null;
const fileExists = checkFileExists(imagePath);
return fileExists ? imagePath : null;
}
module.exports = {
deleteFileIfExists,
checkFileExists,
validateImagePath
};
+57
View File
@@ -0,0 +1,57 @@
const { BadRequestError } = require('../error');
class ImageUploadHelper {
uploadImage(file) {
if (!file) {
throw new BadRequestError('Please upload an image!');
}
return `/uploads/${file.filename}`;
}
uploadCV(file) {
if (!file) {
throw new BadRequestError('Please upload your CV');
}
return `/uploads/${file.filename}`;
}
uploadPortofolio(file) {
if (!file) {
throw new BadRequestError('Please upload your portfolio');
}
return `/uploads/${file.filename}`;
}
validateImage(file) {
if (!file) {
return false;
}
const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg', 'image/webp'];
return allowedTypes.includes(file.mimetype);
}
validatePDF(file) {
if (!file) {
return false;
}
const allowedTypes = ['application/pdf'];
return allowedTypes.includes(file.mimetype);
}
validateDocument(file) {
if (!file) {
return false;
}
const allowedTypes = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
];
return allowedTypes.includes(file.mimetype);
}
}
module.exports = new ImageUploadHelper();
+20
View File
@@ -0,0 +1,20 @@
require('dotenv').config();
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.AUTH_EMAIL,
pass: process.env.AUTH_PASSWORD,
},
});
transporter.verify((error) => {
if (error) {
console.log('Error setup email:', error);
} else {
console.log('Server siap mengirim email');
}
});
module.exports = transporter;
+25
View File
@@ -0,0 +1,25 @@
const wrapper = require('./wrapper');
const { ValidationError } = require('../error');
const isValidPayload = (payload, model) => {
const { value, error } = model.validate(payload, {
abortEarly: false,
allowUnknown: true,
stripUnknown: true,
});
if (error) {
let errors = {};
error.details.forEach(element => {
errors[element.path[0]] = element.message;
});
return wrapper.error(new ValidationError('Validation Error', errors));
}
return { err: null, data: value };
};
module.exports = {
isValidPayload,
};
+95
View File
@@ -0,0 +1,95 @@
const {
NotFoundError,
InternalServerError,
BadRequestError,
ConflictError,
ExpectationFailedError,
ForbiddenError,
GatewayTimeoutError,
ServiceUnavailableError,
UnauthorizedError,
ValidationError
} = require('../error');
const { ERROR: httpError } = require('../http-status/status_code');
const response = (res, type, result, message = '', code = 200) => {
let status = true;
let data = result?.data ?? null;
if (type === 'fail') {
status = false;
data = null;
message = result.err instanceof ValidationError ? result.err.errors : result.err?.message || message;
code = checkErrorCode(result.err);
}
res.status(code).json({
status,
data,
message,
code,
});
};
const paginationResponse = (res, type, result, message = '', code = 200) => {
let status = true;
let data = result?.data ?? null;
let meta = result?.meta ?? null;
if (type === 'fail') {
status = false;
data = '';
message = result.err instanceof ValidationError ? { errors: result.err.errors } : result.err?.message || message;
code = checkErrorCode(result.err);
}
console.log(typeof message)
res.status(code).json({
status: status,
data,
meta,
message,
code,
});
}
const checkErrorCode = (error) => {
switch (error.constructor) {
case BadRequestError:
return httpError.BAD_REQUEST;
case ConflictError:
return httpError.CONFLICT;
case ExpectationFailedError:
return httpError.EXPECTATION_FAILED;
case ForbiddenError:
return httpError.FORBIDDEN;
case GatewayTimeoutError:
return httpError.GATEWAY_TIMEOUT;
case InternalServerError:
return httpError.INTERNAL_ERROR;
case NotFoundError:
return httpError.NOT_FOUND;
case ServiceUnavailableError:
return httpError.SERVICE_UNAVAILABLE;
case UnauthorizedError:
return httpError.UNAUTHORIZED;
case ValidationError:
return httpError.BAD_REQUEST;
default:
return httpError.INTERNAL_ERROR;
}
};
const data = (data) => ({ err: null, data });
const paginationData = (data, meta) => ({ err: null, data, meta });
const error = (err) => ({ err, data: null });
module.exports = {
data,
paginationData,
error,
response,
paginationResponse
};
Executable
+78
View File
@@ -0,0 +1,78 @@
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const path = require('path');
const adminRoutes = require('./routes/admin.routes');
const authRoutes = require('./routes/auth.routes');
const lowonganMagangRoutes = require('./routes/lowonganMagang.routes');
const lamaranMagangRoutes = require('./routes/lamaranMagang.routes');
const mahasiswaRoutes = require('./routes/mahasiswa.routes');
const partnershipRoutes = require('./routes/partnership.routes');
const hasilResearchRoutes = require('./routes/hasilResearch.routes');
const faqRoutes = require('./routes/faq.routes');
const feedbackRoutes = require('./routes/feedback.routes');
const batchRoutes = require('./routes/batch.routes');
const setupSwaggerDocs = require('./docs/swagger');
const PORT = process.env.PORT;
const app = express();
const corsOptions = {
origin: true,
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE", "OPTIONS"],
credentials: true,
allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With", "Accept"],
exposedHeaders: ["Content-Range", "X-Content-Range"],
optionsSuccessStatus: 200,
};
const uploadDir = process.env.NODE_ENV === 'production'
? '/tmp/uploads'
: path.join(__dirname, 'uploads');
app.options('*', cors(corsOptions));
app.use(express.json());
app.use(cors(corsOptions));
app.use("/uploads", express.static(uploadDir), (req, res, next) => {
if (!res.headersSent) {
res.status(404).json({
status: false,
message: `File not found: ${req.path}`,
code: 404
});
}
});
app.use("/admin-api", adminRoutes);
app.use("/auth-api", authRoutes);
app.use("/lowongan-magang-api", lowonganMagangRoutes);
app.use("/lamaran-magang-api", lamaranMagangRoutes);
app.use("/mahasiswa-api", mahasiswaRoutes);
app.use("/partnership-api", partnershipRoutes);
app.use("/hasil-research-api", hasilResearchRoutes);
app.use("/faq-api", faqRoutes);
app.use("/feedback-api", feedbackRoutes);
app.use("/batch-api", batchRoutes);
setupSwaggerDocs(app);
app.use((req, res) => {
res.status(404).json({
status: false,
message: `Route not found: ${req.method} ${req.path}`,
code: 404
});
});
if (process.env.NODE_ENV !== 'production') {
app.listen(PORT, () => {
console.log(`listening at http://localhost:${PORT}`);
});
}
module.exports = app;
/*
NOTE : saran saya untuk penamaan variabel untuk setiap response dibuat konsisten menjadi inggris, tolong komunikasi aja ke fe 😁
*/
-31
View File
@@ -1,31 +0,0 @@
import dotenv from 'dotenv';
dotenv.config();
import app from './app';
const PORT = process.env.PORT || 5000;
const NODE_ENV = process.env.NODE_ENV || 'development';
const server = app.listen(PORT, () => {
console.log(`=========================================`);
console.log(` Server jalanin di ${PORT} `);
console.log(` Environment: ${NODE_ENV} `);
console.log(` API base: http://localhost:${PORT}/api `);
console.log(`=========================================`);
});
process.on('unhandledRejection', (err: Error) => {
console.error(`[Unhandled Rejection] Shutting down...`);
console.error(err.name, err.message);
server.close(() => {
process.exit(1);
});
});
process.on('uncaughtException', (err: Error) => {
console.error(`[Uncaught Exception] Shutting down...`);
console.error(err.name, err.message);
server.close(() => {
process.exit(1);
});
});
-5
View File
@@ -1,5 +0,0 @@
import { PrismaClient } from '../generated/prisma/client';
const prisma = new PrismaClient();
export default prisma;
-67
View File
@@ -1,67 +0,0 @@
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import prisma from '../lib/prisma';
import { CustomError } from './errorHandler';
export interface AuthenticatedRequest extends Request {
user?: {
id: string;
email: string;
role: 'ADMIN' | 'STUDENT';
};
}
export const protect = async (req: AuthenticatedRequest, _res: Response, next: NextFunction) => {
let token: string | undefined;
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}
if (!token) {
const error: CustomError = new Error('Not authorized to access this route, token is missing');
error.statusCode = 401;
return next(error);
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'internify_secret_key_2026_dev') as {
id: string;
email: string;
role: 'ADMIN' | 'STUDENT';
};
const user = await prisma.user.findUnique({
where: { id: decoded.id },
select: {
id: true,
email: true,
role: true,
},
});
if (!user) {
const error: CustomError = new Error('User belonging to this token no longer exists');
error.statusCode = 401;
return next(error);
}
req.user = user as { id: string; email: string; role: 'ADMIN' | 'STUDENT' };
next();
} catch (err) {
const error: CustomError = new Error('Not authorized to access this route, token is invalid or expired');
error.statusCode = 401;
return next(error);
}
};
export const restrictTo = (...roles: ('ADMIN' | 'STUDENT')[]) => {
return (req: AuthenticatedRequest, _res: Response, next: NextFunction) => {
if (!req.user || !roles.includes(req.user.role)) {
const error: CustomError = new Error('You do not have permission to perform this action');
error.statusCode = 403;
return next(error);
}
next();
};
};
-27
View File
@@ -1,27 +0,0 @@
import { Request, Response, NextFunction } from 'express';
export interface CustomError extends Error {
statusCode?: number;
}
export const errorHandler = (
err: CustomError,
_req: Request,
res: Response,
_next: NextFunction
) => {
const statusCode = err.statusCode || 500;
const message = err.message || 'Internal Server Error';
console.error(`[Error] ${statusCode} - ${message}`);
if (err.stack) {
console.error(err.stack);
}
res.status(statusCode).json({
status: 'error',
statusCode,
message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
});
};
+11
View File
@@ -0,0 +1,11 @@
const { UnauthorizedError } = require('../helpers/error')
const { response } = require('../helpers/utils/wrapper');
const { ERROR } = require('../helpers/http-status/status_code');
module.exports = function isAdmin(req, res, next) {
if (req.role && req.role === 'admin') {
return next();
}
return response(res, 'fail', new UnauthorizedError('Akses ditolak: Hanya admin yang dapat mengakses ini'), 'Akses ditolak: Hanya admin yang dapat mengakses ini', ERROR.UNAUTHORIZED);
}
+124
View File
@@ -0,0 +1,124 @@
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { response } = require('../helpers/utils/wrapper')
const uploadDir = process.env.NODE_ENV === 'production'
? '/tmp/uploads'
: path.join(__dirname, '../uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
const storage = multer.diskStorage({
destination: (_req, _file, cb) => cb(null, uploadDir),
filename: (_req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, uniqueSuffix + path.extname(file.originalname));
},
});
const fileFilter = (req, file, cb) => {
const allowedMimeTypes = [
'image/jpeg',
'image/jpg',
'image/png',
'application/pdf'
];
if (allowedMimeTypes.includes(file.mimetype)) {
cb(null, true);
} else {
const errMsg = 'Can only upload images (jpeg, jpg, png) or PDF files';
req.fileValidationError = errMsg;
cb(new Error(errMsg), false);
}
};
const upload = multer({
storage: storage,
fileFilter: fileFilter,
});
// middleware untuk ngechek ukuran file
const checkFileSizes = (req, res, next) => {
const maxSize = 5 * 1024 * 1024; // 5MB
const errors = {};
const filesToDelete = [];
const allFiles = [];
// ini untuk 1 file
if (req.file) allFiles.push(req.file);
// ini untuk multiple files
if (req.files) {
if (Array.isArray(req.files)) {
allFiles.push(...req.files);
} else {
Object.values(req.files).flat().forEach(file => allFiles.push(file));
}
}
allFiles.forEach(file => {
if (file.size > maxSize) {
errors[file.fieldname] = `Ukuran file ${file.fieldname} terlalu besar. Maksimal 5MB`;
}
filesToDelete.push(file.path);
});
// ini untuk jika ada error maka file yang terupload dihapus
if (Object.keys(errors).length > 0) {
filesToDelete.forEach(filePath => {
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
});
return res.status(413).json({
status: false,
data: null,
message: errors,
code: 413,
});
}
next();
};
// middleware untuk error multer
const multerErrorHandler = (err, req, res, next) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_UNEXPECTED_FILE') {
return res.status(400).json({
status: false,
data: null,
message: {
[err.field]: `Field '${err.field}' tidak boleh memiliki lebih dari 1 file.`,
},
code: 400,
});
}
return res.status(400).json({
status: false,
data: null,
message: err.message,
code: 400,
});
}
if (err) {
return res.status(400).json({
status: false,
data: null,
message: err.message || 'Error saat upload file',
code: 400,
});
}
next();
};
module.exports = upload;
module.exports.multerErrorHandler = multerErrorHandler;
module.exports.checkFileSizes = checkFileSizes;
+44
View File
@@ -0,0 +1,44 @@
const axios = require('axios');
const path = require('path');
const { deleteFileIfExists } = require('../helpers/utils/fileHelper');
const { response } = require('../helpers/utils/wrapper');
const { SUCCESS, ERROR } = require('../helpers/http-status/status_code');
const verifyRecaptcha = async (req, res, next) => {
const recaptchaToken = req.body?.['g-recaptcha-response'] || req.query?.['g-recaptcha-response'];
const cv = req.files.cv?.[0]?.filename;
const pdf = req.files.portofolio?.[0]?.filename;
const files = [cv, pdf];
if (!recaptchaToken) {
await deleteFiles(files);
return response(res, "fail", { err: 'Recaptcha token is missing' }, "Recaptcha Verification Failed", ERROR.BAD_REQUEST);
}
try {
const secretKey = process.env.RECAPTCHA_SECRET_KEY;
const response = await axios.post(
`https://www.google.com/recaptcha/api/siteverify?secret=${secretKey}&response=${recaptchaToken}`
)
if (!response.data.success) {
await deleteFiles(files);
return res.status(400).json({ message: 'Recaptcha verification failed' });
}
return next();
} catch (error) {
console.error('Recaptcha verification error:', error);
res.status(500).json({ message: 'Recaptcha verification failed' });
}
};
async function deleteFiles(files) {
for (const file of files) {
if (file) {
const fileName = path.basename(`/uploads/${file}`);
await deleteFileIfExists(fileName);
}
}
}
module.exports = verifyRecaptcha;
-46
View File
@@ -1,46 +0,0 @@
import multer from 'multer';
import path from 'path';
import fs from 'fs';
const storage = multer.diskStorage({
destination: (_req, file, cb) => {
let folder = 'uploads/';
if (file.fieldname === 'cv' || file.fieldname === 'portofolio') {
folder += 'docs/';
} else {
folder += 'images/';
}
fs.mkdirSync(folder, { recursive: true });
cb(null, folder);
},
filename: (_req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
const ext = path.extname(file.originalname);
cb(null, `${file.fieldname}-${uniqueSuffix}${ext}`);
}
});
const fileFilter = (_req: any, file: any, cb: any) => {
if (file.fieldname === 'cv' || file.fieldname === 'portofolio') {
if (file.mimetype === 'application/pdf') {
cb(null, true);
} else {
cb(new Error('CV dan Portofolio harus dalam format PDF!'), false);
}
} else {
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('File upload harus berupa gambar!'), false);
}
}
};
export const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 10 * 1024 * 1024, // Maksimal 10MB yaa, nanti paling bisa diganti oleh admin melalui website
}
});
+72
View File
@@ -0,0 +1,72 @@
const jwt = require("jsonwebtoken");
const { config } = require('../helpers/infra/global_config');
const { response } = require('../helpers/utils/wrapper');
const { UnauthorizedError } = require('../helpers/error');
const { UNAUTHORIZED } = require('../helpers/http-status/status_code');
const privateKey = config.jwtPrivateKey ? config.jwtPrivateKey.replace(/\\n/g, '\n') : null;
const publicKey = config.jwtPublicKey ? config.jwtPublicKey.replace(/\\n/g, '\n') : null;
const createToken = (data) => {
if (!privateKey) {
throw new Error('JWT private key not configured');
}
const accessToken = jwt.sign(
{
id: data.id,
email: data.email,
role: data.role,
signature: data.signature
},
privateKey,
{ algorithm: 'RS256', expiresIn: '1d' }
);
return accessToken;
};
const verifyJWT = async (req, res, next) => {
try {
const { authorization } = req.headers;
if (!authorization || !authorization.startsWith('Bearer ')) {
const err = new UnauthorizedError('Token is required and must be in Bearer format');
return response(res, "fail", { err }, "Unauthorized", UNAUTHORIZED);
}
const token = authorization.split(' ')[1];
if (!publicKey) {
const err = new UnauthorizedError('JWT public key not configured');
return response(res, "fail", { err }, "Server Configuration Error", UNAUTHORIZED);
}
jwt.verify(token, publicKey, { algorithms: ['RS256'] }, (err, decoded) => {
if (err) {
let unauthorizedError;
if (err instanceof jwt.TokenExpiredError) {
unauthorizedError = new UnauthorizedError('Token has expired', err);
} else if (err instanceof jwt.JsonWebTokenError) {
unauthorizedError = new UnauthorizedError('Invalid Token', err);
} else {
unauthorizedError = new UnauthorizedError('Token verification failed', err);
}
return response(res, "fail", { err: unauthorizedError }, "Unauthorized", UNAUTHORIZED);
}
req.id = decoded.id;
req.role = decoded.role;
req.signature = decoded.signature;
req.email = decoded.email;
next();
});
} catch (err) {
const unauthorizedError = new UnauthorizedError(err.message);
return response(res, "fail", { err: unauthorizedError }, "Unauthorized", UNAUTHORIZED);
}
};
module.exports = { verifyJWT, createToken };
+114
View File
@@ -0,0 +1,114 @@
const adminService = require('../services/admin.service');
const { response, data, error } = require('../../../helpers/utils/wrapper');
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
const { ForbiddenError, BadRequestError, NotFoundError, InternalServerError } = require('../../../helpers/error');
const { isValidPayload } = require('../../../helpers/utils/validator');
const { createAdminModel, updateAdminModel } = require('../models/admin.model');
class AdminController {
async addAdmin(req, res) {
try {
const validatePayload = isValidPayload(req.body, createAdminModel);
if (validatePayload.err) {
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const result = await adminService.createAdmin(validatePayload.data);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', data(), 'Admin berhasil dibuat', SUCCESS.CREATED);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getAdmins(req, res) {
try {
const result = await adminService.getAllAdmins();
if (result.err) {
return response(res, 'fail', result);
}
if (!result.data || result.data.length === 0) {
return response(res, 'fail', error(new NotFoundError('Tidak ada admin ditemukan')));
}
return response(res, 'success', result, 'Admin berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getAdminById(req, res) {
try {
const { id } = req.params;
const result = await adminService.getAdminById(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Admin berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async updateAdmin(req, res) {
try {
const { id } = req.params;
const validatePayload = isValidPayload(req.body, updateAdminModel);
if (validatePayload.err) {
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const result = await adminService.updateAdmin(id, validatePayload.data);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', data(), 'Admin berhasil diperbarui', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async deleteAdmin(req, res) {
try {
const { id } = req.params;
const result = await adminService.deleteAdmin(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', data(), 'Admin berhasil dihapus', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
}
module.exports = new AdminController();
+9
View File
@@ -0,0 +1,9 @@
const adminController = require('./controllers/admin.controller');
const adminService = require('./services/admin.service');
const adminRepository = require('./repositories/admin.repository');
module.exports = {
adminController,
adminService,
adminRepository
};
+75
View File
@@ -0,0 +1,75 @@
const joi = require('joi');
const createAdminModel = joi.object().keys({
nama_depan: joi.string().max(255).required().messages({
'string.base': 'Nama depan harus berupa teks.',
'string.empty': 'Nama depan tidak boleh kosong.',
'string.max': 'Nama depan maksimal {#limit} karakter.',
'any.required': 'Nama depan wajib diisi.',
}),
nama_belakang: joi.string().max(255).required().messages({
'string.base': 'Nama belakang harus berupa teks.',
'string.empty': 'Nama belakang tidak boleh kosong.',
'string.max': 'Nama belakang maksimal {#limit} karakter.',
'any.required': 'Nama belakang wajib diisi.',
}),
email: joi.string().email({ tlds: { allow: false } }).max(255).required().messages({
'string.base': 'Email harus berupa teks.',
'string.empty': 'Email tidak boleh kosong.',
'string.email': 'Format email tidak valid.',
'string.max': 'Email maksimal {#limit} karakter.',
'any.required': 'Email wajib diisi.',
}),
password: joi.string().min(8).required().messages({
'string.base': 'Password harus berupa teks.',
'string.empty': 'Password tidak boleh kosong.',
'string.min': 'Password minimal {#limit} karakter.',
'any.required': 'Password wajib diisi.',
}),
role: joi.string().valid('admin').default('admin').messages({
'string.base': 'Role harus berupa teks.',
'any.only': 'Role hanya boleh "admin".',
}),
signature: joi.string().optional().allow('', null).messages({
'string.base': 'Signature harus berupa teks.',
}),
});
const updateAdminModel = joi.object().keys({
nama_depan: joi.string().max(255).optional().messages({
'string.base': 'Nama depan harus berupa teks.',
'string.empty': 'Nama depan tidak boleh kosong.',
'string.max': 'Nama depan maksimal {#limit} karakter.',
}),
nama_belakang: joi.string().max(255).optional().messages({
'string.base': 'Nama belakang harus berupa teks.',
'string.empty': 'Nama belakang tidak boleh kosong.',
'string.max': 'Nama belakang maksimal {#limit} karakter.',
}),
email: joi.string().email({ tlds: { allow: false } }).max(255).optional().messages({
'string.base': 'Email harus berupa teks.',
'string.email': 'Format email tidak valid.',
'string.max': 'Email maksimal {#limit} karakter.',
}),
password: joi.string().min(8).optional().messages({
'string.base': 'Password harus berupa teks.',
'string.min': 'Password minimal {#limit} karakter.',
}),
signature: joi.string().optional().allow('', null).messages({
'string.base': 'Signature harus berupa teks.',
}),
});
module.exports = {
createAdminModel,
updateAdminModel,
};
+40
View File
@@ -0,0 +1,40 @@
const prisma = require('../../../helpers/db/db_connection');
class AdminRepository {
async create(adminData) {
return prisma.admin.create({
data: adminData
});
}
async findAll() {
return prisma.admin.findMany();
}
async findByEmail(email) {
return prisma.admin.findUnique({
where: { email }
});
}
async findById(id) {
return prisma.admin.findUnique({
where: { id: parseInt(id) }
});
}
async updateById(id, adminData) {
return prisma.admin.update({
where: { id: parseInt(id) },
data: adminData
});
}
async deleteById(id) {
return prisma.admin.delete({
where: { id: parseInt(id) }
});
}
}
module.exports = new AdminRepository();
+114
View File
@@ -0,0 +1,114 @@
const bcrypt = require('bcrypt');
const adminRepository = require('../repositories/admin.repository');
const { data, error } = require('../../../helpers/utils/wrapper');
const { BadRequestError, NotFoundError } = require('../../../helpers/error');
class AdminService {
async createAdmin(adminData) {
try {
const { nama_depan, nama_belakang, email, password, role } = adminData;
const existingAdmin = await adminRepository.findByEmail(email);
if (existingAdmin) {
return error(new BadRequestError('Email sudah terdaftar'));
}
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);
const newAdminData = {
nama_depan,
nama_belakang,
email,
password: hashedPassword,
role,
signature: `${nama_depan}-${Date.now()}`,
};
await adminRepository.create(newAdminData);
return data();
} catch (err) {
return error(err);
}
}
async getAllAdmins() {
try {
const admins = await adminRepository.findAll();
if (!admins || admins.length === 0) {
return error(new NotFoundError('Tidak ada admin ditemukan'));
}
return data(admins);
} catch (err) {
return error(err);
}
}
async getAdminById(id) {
try {
const admin = await adminRepository.findById(id);
if (!admin) {
return error(new NotFoundError('Admin tidak ditemukan'));
}
return data(admin);
} catch (err) {
return error(err);
}
}
async getAdminByEmail(email) {
try {
const admin = await adminRepository.findByEmail(email);
return data(admin);
} catch (err) {
return error(err);
}
}
async verifyPassword(plainPassword, hashedPassword) {
try {
const isValid = await bcrypt.compare(plainPassword, hashedPassword);
return data(isValid);
} catch (err) {
return error(err);
}
}
async updateAdmin(id, adminData) {
try {
const adminResult = await this.getAdminById(id);
if (adminResult.err) {
return adminResult;
}
if (adminData.email) {
const existingAdmin = await adminRepository.findByEmail(adminData.email);
if (existingAdmin && existingAdmin.id !== id) {
return error(new BadRequestError('Email sudah terdaftar'));
}
}
await adminRepository.updateById(id, adminData);
return data();
} catch (err) {
return error(err);
}
}
async deleteAdmin(id) {
try {
const adminResult = await this.getAdminById(id);
if (adminResult.err) {
return adminResult;
}
await adminRepository.deleteById(id);
return data();
} catch (err) {
return error(err);
}
}
}
module.exports = new AdminService();
+72
View File
@@ -0,0 +1,72 @@
const authService = require('../services/auth.service');
const { response, data, error } = require('../../../helpers/utils/wrapper');
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
const { BadRequestError, UnauthorizedError, InternalServerError } = require('../../../helpers/error');
const { isValidPayload } = require('../../../helpers/utils/validator');
const { loginModel } = require('../models/auth.model');
class AuthController {
async login(req, res) {
try {
const validatePayload = isValidPayload(req.body, loginModel);
if (validatePayload.err) {
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const { email, password } = validatePayload.data;
const result = await authService.login(email, password);
if (result.err) {
return response(res, 'fail', result);
}
console.log(result.data.token)
res.cookie("token", result.data.token, {
httpOnly: true,
secure: true,
sameSite: 'None',
maxAge: 7 * 24 * 60 * 60,
});
return response(res, 'success', result, 'Login berhasil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async me(req, res) {
try {
const id = req.id;
if (!id) {
return response(res, 'fail', error(new UnauthorizedError('Tidak diotorisasi: ID pengguna tidak ditemukan')));
}
const result = await authService.getCurrentUser(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Pengguna berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async logout(req, res) {
try {
return response(res, 'success', data(), 'Logout berhasil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
}
module.exports = new AuthController();
+7
View File
@@ -0,0 +1,7 @@
const authController = require('./controllers/auth.controller');
const authService = require('./services/auth.service');
module.exports = {
authController,
authService
};
+74
View File
@@ -0,0 +1,74 @@
const joi = require('joi');
const loginModel = joi.object().keys({
email: joi.string().email({ tlds: { allow: false } }).required().messages({
'string.base': 'Email harus berupa teks.',
'string.empty': 'Email tidak boleh kosong.',
'string.email': 'Format email tidak valid.',
'any.required': 'Email wajib diisi.',
}),
password: joi.string().required().messages({
'string.base': 'Password harus berupa teks.',
'string.empty': 'Password tidak boleh kosong.',
'string.min': 'Password minimal {#limit} karakter.',
'any.required': 'Password wajib diisi.',
}),
});
const registerModel = joi.object().keys({
email: joi.string().email({ tlds: { allow: false } }).required().messages({
'string.base': 'Email harus berupa teks.',
'string.empty': 'Email tidak boleh kosong.',
'string.email': 'Format email tidak valid.',
'any.required': 'Email wajib diisi.',
}),
password: joi.string()
.min(8)
.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
.required()
.messages({
'string.base': 'Password harus berupa teks.',
'string.empty': 'Password tidak boleh kosong.',
'string.min': 'Password minimal {#limit} karakter.',
'string.pattern.base': 'Password harus mengandung huruf besar, huruf kecil, dan angka.',
'any.required': 'Password wajib diisi.',
}),
});
const forgotPasswordModel = joi.object().keys({
email: joi.string().email({ tlds: { allow: false } }).required().messages({
'string.base': 'Email harus berupa teks.',
'string.empty': 'Email tidak boleh kosong.',
'string.email': 'Format email tidak valid.',
'any.required': 'Email wajib diisi.',
}),
});
const resetPasswordModel = joi.object().keys({
token: joi.string().required().messages({
'string.base': 'Token harus berupa teks.',
'string.empty': 'Token tidak boleh kosong.',
'any.required': 'Token wajib diisi.',
}),
password: joi.string()
.min(8)
.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
.required()
.messages({
'string.base': 'Password harus berupa teks.',
'string.empty': 'Password tidak boleh kosong.',
'string.min': 'Password minimal {#limit} karakter.',
'string.pattern.base': 'Password harus mengandung huruf besar, huruf kecil, dan angka.',
'any.required': 'Password wajib diisi.',
}),
});
module.exports = {
loginModel,
registerModel,
forgotPasswordModel,
resetPasswordModel,
};
+72
View File
@@ -0,0 +1,72 @@
const bcrypt = require('bcrypt');
const adminService = require('../../admin/services/admin.service');
const { createToken } = require('../../../middleware/verifyJWT');
const { data, error } = require('../../../helpers/utils/wrapper');
const { UnauthorizedError, NotFoundError } = require('../../../helpers/error');
class AuthService {
async login(email, password) {
try {
const result = await adminService.getAdminByEmail(email);
if (result.err || !result.data) {
return error(new UnauthorizedError('Email atau password tidak valid'));
}
const admin = result.data;
const isPasswordValid = await bcrypt.compare(password, admin.password);
if (!isPasswordValid) {
return error(new UnauthorizedError('Email atau password tidak valid'));
}
const token = createToken({
id: admin.id.toString(),
email: admin.email,
role: admin.role,
signature: admin.signature
});
return data({
token,
admin: {
id: admin.id.toString(),
nama_depan: admin.nama_depan,
nama_belakang: admin.nama_belakang,
email: admin.email,
role: admin.role,
signature: admin.signature,
},
});
} catch (err) {
return error(err);
}
}
async getCurrentUser(userId) {
try {
const result = await adminService.getAdminById(userId);
if (result.err) {
return result;
}
const { password, ...adminWithoutPassword } = result.data;
return data(adminWithoutPassword);
} catch (err) {
return error(new NotFoundError('Pengguna tidak ditemukan'));
}
}
generateToken(payload) {
try {
const token = createToken(payload);
return data(token);
} catch (err) {
return error(err);
}
}
}
module.exports = new AuthService();
+130
View File
@@ -0,0 +1,130 @@
const batchService = require('../services/batch.service');
const { response, error } = require('../../../helpers/utils/wrapper');
const { BadRequestError, InternalServerError } = require('../../../helpers/error');
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
const { isValidPayload } = require('../../../helpers/utils/validator');
const { createBatchModel, updateBatchModel } = require('../models/batch.model');
class BatchController {
async createBatch(req, res) {
try {
const validatePayload = isValidPayload(req.body, createBatchModel);
if (validatePayload.err) {
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const result = await batchService.createBatch(validatePayload.data);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Batch berhasil dibuat', SUCCESS.CREATED);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getAllBatches(req, res) {
try {
const result = await batchService.getAllBatches();
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Berhasil mengambil semua batch', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async updateBatch(req, res) {
try {
const id = parseInt(req.params.id);
const validatePayload = isValidPayload(req.body, updateBatchModel);
if (validatePayload.err) {
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const result = await batchService.updateBatch(id, validatePayload.data);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Batch berhasil diperbarui', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async deleteBatch(req, res) {
try {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return response(
res,
'fail',
error(new BadRequestError('ID batch harus berupa angka')),
'ID batch tidak valid',
ERROR.BAD_REQUEST
);
}
const result = await batchService.deleteBatch(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Batch berhasil dihapus', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async switchBatch(req, res) {
try {
const batch_number = parseInt(req.params.batch_number);
if (isNaN(batch_number)) {
return response(
res,
'fail',
error(new BadRequestError('Nomor batch harus berupa angka')),
'Nomor batch tidak valid',
ERROR.BAD_REQUEST
);
}
const result = await batchService.switchBatch(batch_number);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Batch berhasil diaktifkan', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
}
module.exports = new BatchController();
+9
View File
@@ -0,0 +1,9 @@
const batchController = require('./controllers/batch.controller');
const batchService = require('./services/batch.service');
const batchRepository = require('./repositories/batch.repository');
module.exports = {
batchController,
batchService,
batchRepository,
};
+30
View File
@@ -0,0 +1,30 @@
const joi = require('joi');
const createBatchModel = joi.object().keys({
batch_number: joi.number().integer().positive().messages({
'number.base': 'Nomor batch harus berupa angka.',
'number.integer': 'Nomor batch harus berupa bilangan bulat.',
'number.positive': 'Nomor batch harus berupa angka positif.',
}),
is_active: joi.boolean().default(true).messages({
'boolean.base': 'Status aktif harus berupa boolean.',
}),
});
const updateBatchModel = joi.object().keys({
batch_number: joi.number().integer().positive().messages({
'number.base': 'Nomor batch harus berupa angka.',
'number.integer': 'Nomor batch harus berupa bilangan bulat.',
'number.positive': 'Nomor batch harus berupa angka positif.',
}),
is_active: joi.boolean().messages({
'boolean.base': 'Status aktif harus berupa boolean.',
}),
});
module.exports = {
createBatchModel,
updateBatchModel,
};
+85
View File
@@ -0,0 +1,85 @@
const prisma = require('../../../helpers/db/db_connection');
class BatchRepository {
async create(batchData) {
return prisma.batch.create({
data: batchData,
});
}
async findAll() {
return prisma.batch.findMany({
orderBy: {
batch_number: 'desc',
},
});
}
async findById(id) {
return prisma.batch.findUnique({
where: { id: id },
});
}
async findByBatchNumber(batch_number) {
return prisma.batch.findUnique({
where: { batch_number: batch_number },
});
}
async findActiveBatch() {
return prisma.batch.findFirst({
where: { is_active: true },
});
}
async findLatestBatch() {
return prisma.batch.findFirst({
orderBy: {
batch_number: 'desc',
},
});
}
async updateById(id, batchData) {
return prisma.batch.update({
where: { id: id },
data: batchData,
});
}
async deleteById(id) {
return prisma.batch.delete({
where: { id: id },
});
}
async deactivateAllBatches() {
return prisma.batch.updateMany({
data: { is_active: false },
});
}
async countBatches() {
return prisma.batch.count();
}
async findByStatus(is_active) {
return prisma.batch.findMany({
where: { is_active: is_active },
orderBy: {
batch_number: 'desc',
},
});
}
async getBatchById(batchId) {
if (!batchId) {
return null;
}
return this.findById(parseInt(batchId));
}
}
module.exports = new BatchRepository();
+119
View File
@@ -0,0 +1,119 @@
const batchRepository = require('../repositories/batch.repository');
const { data, error } = require('../../../helpers/utils/wrapper');
const { NotFoundError, ConflictError } = require('../../../helpers/error');
class BatchService {
async createBatch(batchData) {
try {
let batchNumber;
if (batchData.batch_number) {
const existingBatch = await batchRepository.findByBatchNumber(batchData.batch_number);
if (existingBatch) {
return error(new ConflictError('Nomor batch sudah digunakan'));
}
batchNumber = batchData.batch_number;
} else {
const latestBatch = await batchRepository.findLatestBatch();
batchNumber = latestBatch ? latestBatch.batch_number + 1 : 1;
}
await batchRepository.deactivateAllBatches();
const newBatchData = {
...batchData,
batch_number: batchNumber,
is_active: true,
};
const result = await batchRepository.create(newBatchData);
return data(result);
} catch (err) {
return error(err);
}
}
async getAllBatches() {
try {
const batchList = await batchRepository.findAll();
return data(batchList);
} catch (err) {
return error(err);
}
}
async switchBatch(batch_number) {
try {
const batch = await batchRepository.findByBatchNumber(batch_number);
if (!batch) {
return error(new NotFoundError('Batch tidak ditemukan'));
}
await batchRepository.deactivateAllBatches();
const result = await batchRepository.updateById(batch.id, { is_active: true });
return data(result);
} catch (err) {
return error(err);
}
}
async getBatchById(id) {
try {
const batch = await batchRepository.findById(id);
if (!batch) {
return error(new NotFoundError('Batch tidak ditemukan'));
}
return data(batch);
} catch (err) {
return error(err);
}
}
async updateBatch(id, batchData) {
try {
const checkResult = await this.getBatchById(id);
if (checkResult.err) {
return checkResult;
}
console.log(batchData, id);
const existingBatch = checkResult.data;
if (batchData.batch_number && batchData.batch_number !== existingBatch.batch_number) {
const duplicateBatch = await batchRepository.findByBatchNumber(batchData.batch_number);
if (duplicateBatch) {
return error(new ConflictError('Nomor batch sudah digunakan'));
}
}
if (batchData.is_active === true) {
await batchRepository.deactivateAllBatches();
}
const result = await batchRepository.updateById(id, batchData);
return data(result);
} catch (err) {
return error(err);
}
}
async deleteBatch(id) {
try {
const checkResult = await this.getBatchById(id);
if (checkResult.err) {
return checkResult;
}
const result = await batchRepository.deleteById(id);
return data(result);
} catch (err) {
return error(err);
}
}
}
module.exports = new BatchService();
+173
View File
@@ -0,0 +1,173 @@
const faqService = require('../services/faq.services');
const { createFaqModel, updateFaqModel } = require('../models/faq.models');
const { response } = require('../../../helpers/utils/wrapper');
const { isValidPayload } = require('../../../helpers/utils/validator');
const { InternalServerError } = require('../../../helpers/error');
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
class FaqController {
async addFaq(req, res) {
try {
const payload = req.body;
const validatePayload = isValidPayload(payload, createFaqModel);
if (validatePayload.err) {
return response(
res,
'fail',
validatePayload,
'Validasi gagal',
ERROR.EXPECTATION_FAILED
);
}
const result = await faqService.createFaq(validatePayload.data);
if (result.err) {
return response(res, 'fail', result);
}
return response(
res,
'success',
result,
'FAQ berhasil ditambahkan',
SUCCESS.CREATED
);
} catch (err) {
return response(
res,
'fail',
{ err: new InternalServerError(err.message) },
'Terjadi kesalahan yang tidak terduga',
ERROR.INTERNAL_ERROR
);
}
}
async getAllFaq(req, res) {
try {
const result = await faqService.getAllFaq();
if (result.err) {
return response(res, 'fail', result);
}
return response(
res,
'success',
result,
'Data FAQ berhasil diambil',
SUCCESS.OK
);
} catch (err) {
return response(
res,
'fail',
{ err: new InternalServerError(err.message) },
'Terjadi kesalahan yang tidak terduga',
ERROR.INTERNAL_ERROR
);
}
}
async getFaqById(req, res) {
try {
const { id } = req.params;
const result = await faqService.getFaqById(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(
res,
'success',
result,
'Data FAQ berhasil diambil',
SUCCESS.OK
);
} catch (err) {
return response(
res,
'fail',
{ err: new InternalServerError(err.message) },
'Terjadi kesalahan yang tidak terduga',
ERROR.INTERNAL_ERROR
);
}
}
async updateFaq(req, res) {
try {
const { id } = req.params;
const payload = req.body;
const validatePayload = isValidPayload(payload, updateFaqModel);
if (validatePayload.err) {
return response(
res,
'fail',
validatePayload,
'Validasi gagal',
ERROR.EXPECTATION_FAILED
);
}
const result = await faqService.updateFaq(id, validatePayload.data);
if (result.err) {
return response(res, 'fail', result);
}
return response(
res,
'success',
result,
'FAQ berhasil diperbarui',
SUCCESS.OK
);
} catch (err) {
return response(
res,
'fail',
{ err: new InternalServerError(err.message) },
'Terjadi kesalahan yang tidak terduga',
ERROR.INTERNAL_ERROR
);
}
}
async deleteFaq(req, res) {
try {
const { id } = req.params;
const result = await faqService.deleteFaq(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(
res,
'success',
result,
'FAQ berhasil dihapus',
SUCCESS.OK
);
} catch (err) {
return response(
res,
'fail',
{ err: new InternalServerError(err.message) },
'Terjadi kesalahan yang tidak terduga',
ERROR.INTERNAL_ERROR
);
}
}
}
module.exports = new FaqController();
+5
View File
@@ -0,0 +1,5 @@
const faqController = require('./controllers/faq.controllers');
module.exports = {
faqController,
};
+26
View File
@@ -0,0 +1,26 @@
const joi = require('joi');
const createFaqModel = joi.object({
pertanyaan: joi.string().required().messages({
'string.base': 'Pertanyaan harus berupa teks.',
'any.required': 'Pertanyaan wajib diisi.',
}),
jawaban: joi.string().required().messages({
'string.base': 'Jawaban harus berupa teks.',
'any.required': 'Jawaban wajib diisi.',
}),
});
const updateFaqModel = joi.object({
pertanyaan: joi.string().messages({
'string.base': 'Pertanyaan harus berupa teks.',
}),
jawaban: joi.string().messages({
'string.base': 'Jawaban harus berupa teks.',
}),
});
module.exports = {
createFaqModel,
updateFaqModel,
};
+38
View File
@@ -0,0 +1,38 @@
const prisma = require('../../../helpers/db/db_connection');
class FaqRepository {
async create(data) {
return prisma.faq.create({
data: data,
});
}
async findAll() {
return prisma.faq.findMany({
orderBy: {
created_at: 'desc',
},
});
}
async findById(id) {
return prisma.faq.findUnique({
where: { id: parseInt(id) },
});
}
async updateById(id, data) {
return prisma.faq.update({
where: { id: parseInt(id) },
data: data,
});
}
async deleteById(id) {
return prisma.faq.delete({
where: { id: parseInt(id) },
});
}
}
module.exports = new FaqRepository();
+67
View File
@@ -0,0 +1,67 @@
const faqRepository = require('../repositories/faq.repositories');
const { data, error } = require('../../../helpers/utils/wrapper');
const { NotFoundError, BadRequestError } = require('../../../helpers/error');
class FaqService {
async createFaq(faqData) {
try {
const result = await faqRepository.create(faqData);
return data(result);
} catch (err) {
return error(err);
}
}
async getAllFaq() {
try {
const results = await faqRepository.findAll();
return data(results);
} catch (err) {
return error(err);
}
}
async getFaqById(id) {
try {
const result = await faqRepository.findById(id);
if (!result) {
return error(new NotFoundError('FAQ Tidak Ditemukan'));
}
return data(result);
} catch (err) {
return error(err);
}
}
async updateFaq(id, faqData) {
try {
const checkResult = await this.getFaqById(id);
if (checkResult.err) {
return checkResult;
}
const result = await faqRepository.updateById(id, faqData);
return data(result);
} catch (err) {
return error(err);
}
}
async deleteFaq(id) {
try {
const checkResult = await this.getFaqById(id);
if (checkResult.err) {
return checkResult;
}
const result = await faqRepository.deleteById(id);
return data(result);
} catch (err) {
return error(err);
}
}
}
module.exports = new FaqService();
+138
View File
@@ -0,0 +1,138 @@
const feedbackService = require('../services/feedback.service');
const { response, error } = require('../../../helpers/utils/wrapper');
const { BadRequestError, InternalServerError } = require('../../../helpers/error');
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
const { isValidPayload } = require('../../../helpers/utils/validator');
const { createFeedbackModel, updateFeedbackModel, getAllFeedbackModel } = require('../models/feedback.model');
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
class FeedbackController {
async addFeedback(req, res) {
try {
const image_path = req.file;
const validatePayload = isValidPayload(req.body, createFeedbackModel);
if (validatePayload.err) {
if (image_path?.filename) {
await deleteFileIfExists(image_path.filename);
}
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const result = await feedbackService.createFeedback(validatePayload.data, image_path);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Feedback berhasil dibuat', SUCCESS.CREATED);
} catch (err) {
if (req.file?.filename) {
await deleteFileIfExists(req.file.filename);
}
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getAllFeedback(req, res) {
try {
const validatePayload = isValidPayload(req.query, getAllFeedbackModel);
if (validatePayload.err) {
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Query parameter tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const result = await feedbackService.getAllFeedback(validatePayload.data);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Berhasil mengambil semua feedback', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getFeedbackById(req, res) {
try {
const { id } = req.params;
const result = await feedbackService.getFeedbackById(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Berhasil mengambil feedback berdasarkan ID', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async updateFeedback(req, res) {
try {
const { id } = req.params;
const image_path = req.file;
const validatePayload = isValidPayload(req.body, updateFeedbackModel);
if (validatePayload.err) {
if (image_path?.filename) {
await deleteFileIfExists(image_path.filename);
}
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const result = await feedbackService.updateFeedback(id, validatePayload.data, image_path);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Feedback berhasil diperbarui', SUCCESS.OK);
} catch (err) {
if (req.file?.filename) {
await deleteFileIfExists(req.file.filename);
}
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async deleteFeedback(req, res) {
try {
const { id } = req.params;
const result = await feedbackService.deleteFeedback(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Feedback berhasil dihapus', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
}
module.exports = new FeedbackController();
+5
View File
@@ -0,0 +1,5 @@
const feedbackController = require('./controllers/feedback.controller');
module.exports = {
feedbackController,
};
+108
View File
@@ -0,0 +1,108 @@
const joi = require('joi');
const createFeedbackModel = joi.object().keys({
nama: joi.string().max(255).required().messages({
'string.base': 'Nama harus berupa teks.',
'string.empty': 'Nama tidak boleh kosong.',
'string.max': 'Nama maksimal {#limit} karakter.',
'any.required': 'Nama wajib diisi.',
}),
universitas: joi.string().max(255).required().messages({
'string.base': 'Universitas harus berupa teks.',
'string.empty': 'Universitas tidak boleh kosong.',
'string.max': 'Universitas maksimal {#limit} karakter.',
'any.required': 'Universitas wajib diisi.',
}),
pesan: joi.string().required().messages({
'string.base': 'Pesan harus berupa teks.',
'string.empty': 'Pesan tidak boleh kosong.',
'any.required': 'Pesan wajib diisi.',
}),
batch: joi.number().integer().min(1).required().messages({
'number.base': 'Batch harus berupa angka.',
'number.integer': 'Batch harus berupa bilangan bulat.',
'number.min': 'Batch minimal {#limit}.',
'any.required': 'Batch wajib diisi.',
}),
posisi: joi.string().max(255).required().messages({
'string.base': 'Posisi harus berupa teks.',
'string.empty': 'Posisi tidak boleh kosong.',
'string.max': 'Posisi maksimal {#limit} karakter.',
'any.required': 'Posisi wajib diisi.',
}),
tahun: joi.number().integer().min(2000).max(2100).required().messages({
'number.base': 'Tahun harus berupa angka.',
'number.integer': 'Tahun harus berupa bilangan bulat.',
'number.min': 'Tahun minimal {#limit}.',
'number.max': 'Tahun maksimal {#limit}.',
'any.required': 'Tahun wajib diisi.',
}),
image_path: joi.string().optional().allow('', null).messages({
'string.base': 'Path gambar harus berupa teks.',
}),
});
const updateFeedbackModel = joi.object().keys({
nama: joi.string().max(255).optional().messages({
'string.base': 'Nama harus berupa teks.',
'string.max': 'Nama maksimal {#limit} karakter.',
}),
universitas: joi.string().max(255).optional().messages({
'string.base': 'Universitas harus berupa teks.',
'string.max': 'Universitas maksimal {#limit} karakter.',
}),
pesan: joi.string().optional().messages({
'string.base': 'Pesan harus berupa teks.',
}),
batch: joi.number().integer().min(1).optional().messages({
'number.base': 'Batch harus berupa angka.',
'number.integer': 'Batch harus berupa bilangan bulat.',
'number.min': 'Batch minimal {#limit}.',
}),
posisi: joi.string().max(255).optional().messages({
'string.base': 'Posisi harus berupa teks.',
'string.max': 'Posisi maksimal {#limit} karakter.',
}),
tahun: joi.number().integer().min(2000).max(2100).optional().messages({
'number.base': 'Tahun harus berupa angka.',
'number.integer': 'Tahun harus berupa bilangan bulat.',
'number.min': 'Tahun minimal {#limit}.',
'number.max': 'Tahun maksimal {#limit}.',
}),
image_path: joi.string().optional().allow('', null).messages({
'string.base': 'Path gambar harus berupa teks.',
}),
});
const getAllFeedbackModel = joi.object().keys({
batch: joi.number().integer().min(1).optional().messages({
'number.base': 'Batch harus berupa angka.',
'number.integer': 'Batch harus berupa bilangan bulat.',
'number.min': 'Batch minimal {#limit}.',
}),
tahun: joi.number().integer().min(2000).max(2100).optional().messages({
'number.base': 'Tahun harus berupa angka.',
'number.integer': 'Tahun harus berupa bilangan bulat.',
'number.min': 'Tahun minimal {#limit}.',
'number.max': 'Tahun maksimal {#limit}.',
}),
});
module.exports = {
createFeedbackModel,
updateFeedbackModel,
getAllFeedbackModel,
};
+60
View File
@@ -0,0 +1,60 @@
const prisma = require('../../../helpers/db/db_connection');
class FeedbackRepository {
async create(feedbackData) {
return prisma.feedback.create({
data: {
...feedbackData,
created_at: new Date(),
},
});
}
async findAll() {
return prisma.feedback.findMany({
orderBy: {
created_at: 'desc',
},
});
}
async findAllWithFilters({ batch, tahun }) {
const where = {};
if (batch) {
where.batch = parseInt(batch);
}
if (tahun) {
where.tahun = parseInt(tahun);
}
return prisma.feedback.findMany({
where,
orderBy: {
created_at: 'desc',
},
});
}
async findById(id) {
return prisma.feedback.findUnique({
where: { id: parseInt(id) },
});
}
async updateById(id, feedbackData) {
return prisma.feedback.update({
where: { id: parseInt(id) },
data: feedbackData,
});
}
async deleteById(id) {
return prisma.feedback.delete({
where: { id: parseInt(id) },
});
}
}
module.exports = new FeedbackRepository();
+120
View File
@@ -0,0 +1,120 @@
const path = require('path');
const feedbackRepository = require('../repositories/feedback.repository');
const imageUploadHelper = require('../../../helpers/utils/imageUpload.helper');
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
const { data, error } = require('../../../helpers/utils/wrapper');
const { NotFoundError } = require('../../../helpers/error');
class FeedbackService {
async createFeedback(feedbackData, imageFile) {
try {
const imagePath = imageUploadHelper.uploadImage(imageFile);
const newFeedbackData = {
...feedbackData,
image_path: imagePath,
};
const result = await feedbackRepository.create(newFeedbackData);
return data(result);
} catch (err) {
if (imageFile?.filename) {
try {
await deleteFileIfExists(imageFile.filename);
} catch (cleanupErr) {
console.error('Failed to cleanup file:', cleanupErr);
}
}
return error(err);
}
}
async getAllFeedback(query = {}) {
try {
const feedbackList = await feedbackRepository.findAllWithFilters(query);
if (feedbackList.length === 0) {
return data([]);
}
return data(feedbackList);
} catch (err) {
return error(err);
}
}
async getFeedbackById(id) {
try {
const feedback = await feedbackRepository.findById(id);
if (!feedback) {
return error(new NotFoundError('Feedback tidak ditemukan'));
}
return data(feedback);
} catch (err) {
return error(err);
}
}
async updateFeedback(id, feedbackData, imageFile) {
let oldImagePath = null;
let newImageUploaded = false;
try {
const checkResult = await this.getFeedbackById(id);
if (checkResult.err) {
return checkResult;
}
const existingFeedback = checkResult.data;
if (imageFile) {
oldImagePath = existingFeedback.image_path;
feedbackData.image_path = imageUploadHelper.uploadImage(imageFile);
newImageUploaded = true;
if (oldImagePath) {
const oldFileName = path.basename(oldImagePath);
await deleteFileIfExists(oldFileName);
}
}
const result = await feedbackRepository.updateById(id, feedbackData);
return data(result);
} catch (err) {
if (newImageUploaded && imageFile?.filename) {
try {
await deleteFileIfExists(imageFile.filename);
} catch (cleanupErr) {
console.error('Failed to cleanup new file:', cleanupErr);
}
}
return error(err);
}
}
async deleteFeedback(id) {
try {
const checkResult = await this.getFeedbackById(id);
if (checkResult.err) {
return checkResult;
}
const feedback = checkResult.data;
if (feedback.image_path) {
const fileName = path.basename(feedback.image_path);
await deleteFileIfExists(fileName);
}
const result = await feedbackRepository.deleteById(id);
return data(result);
} catch (err) {
return error(err);
}
}
}
module.exports = new FeedbackService();
@@ -0,0 +1,126 @@
const hasilResearchService = require('../services/hasilResearch.service');
const { response, error } = require('../../../helpers/utils/wrapper');
const { BadRequestError, InternalServerError } = require('../../../helpers/error');
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
const { isValidPayload } = require('../../../helpers/utils/validator');
const { createHasilResearchModel, updateHasilResearchModel } = require('../models/hasilResearch.model');
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
class HasilResearchController {
async addHasilResearch(req, res) {
try {
const image = req.file;
const validatePayload = isValidPayload(req.body, createHasilResearchModel);
if (validatePayload.err) {
if (image?.filename) {
await deleteFileIfExists(image.filename);
}
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const result = await hasilResearchService.createHasilResearch(validatePayload.data, image);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Data hasil research berhasil ditambahkan', SUCCESS.CREATED);
} catch (err) {
if (req.file?.filename) {
await deleteFileIfExists(req.file.filename);
}
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getHasilResearch(req, res) {
try {
const result = await hasilResearchService.getAllHasilResearch();
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Data research berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getHasilResearchById(req, res) {
try {
const { id } = req.params;
const result = await hasilResearchService.getHasilResearchById(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Data research berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async updateHasilResearch(req, res) {
try {
const { id } = req.params;
const image = req.file;
const validatePayload = isValidPayload(req.body, updateHasilResearchModel);
if (validatePayload.err) {
if (image?.filename) {
await deleteFileIfExists(image.filename);
}
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const result = await hasilResearchService.updateHasilResearch(id, validatePayload.data, image);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Data hasil research berhasil diperbarui', SUCCESS.OK);
} catch (err) {
if (req.file?.filename) {
await deleteFileIfExists(req.file.filename);
}
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async deleteHasilResearch(req, res) {
try {
const { id } = req.params;
const result = await hasilResearchService.deleteHasilResearch(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Data hasil research berhasil dihapus', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
}
module.exports = new HasilResearchController();
+9
View File
@@ -0,0 +1,9 @@
const hasilResearchController = require('./controllers/hasilResearch.controller');
const hasilResearchService = require('./services/hasilResearch.service');
const hasilResearchRepository = require('./repositories/hasilResearch.repository');
module.exports = {
hasilResearchController,
hasilResearchService,
hasilResearchRepository
};
+52
View File
@@ -0,0 +1,52 @@
joi = require('joi');
const urlRegex = /^(https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/;
const createHasilResearchModel = joi.object().keys({
nama_project: joi.string().max(255).required().messages({
'string.base': 'Nama project harus berupa teks.',
'string.empty': 'Nama project tidak boleh kosong.',
'string.max': 'Nama project maksimal {#limit} karakter.',
'any.required': 'Nama project wajib diisi.',
}),
deskripsi: joi.string().required().messages({
'string.base': 'Deskripsi harus berupa teks.',
'string.empty': 'Deskripsi tidak boleh kosong.',
'any.required': 'Deskripsi wajib diisi.',
}),
link_project: joi.string().pattern(urlRegex).optional().allow('', null).messages({
'string.base': 'Link project harus berupa teks.',
'string.pattern.base': 'Format URL tidak valid.',
}),
image_path: joi.string().optional().allow('', null).messages({
'string.base': 'Path gambar harus berupa teks.',
}),
});
const updateHasilResearchModel = joi.object().keys({
nama_project: joi.string().max(255).optional().messages({
'string.base': 'Nama project harus berupa teks.',
'string.max': 'Nama project maksimal {#limit} karakter.',
}),
deskripsi: joi.string().optional().messages({
'string.base': 'Deskripsi harus berupa teks.',
}),
link_project: joi.string().pattern(urlRegex).optional().allow('', null).messages({
'string.base': 'Link project harus berupa teks.',
'string.pattern.base': 'Format URL tidak valid.',
}),
image_path: joi.string().optional().allow('', null).messages({
'string.base': 'Path gambar harus berupa teks.',
}),
});
module.exports = {
createHasilResearchModel,
updateHasilResearchModel,
};
@@ -0,0 +1,34 @@
const prisma = require('../../../helpers/db/db_connection');
class HasilResearchRepository {
async create(researchData) {
return prisma.hasilResearch.create({
data: researchData,
});
}
async findAll() {
return prisma.hasilResearch.findMany();
}
async findById(id) {
return prisma.hasilResearch.findUnique({
where: { id: parseInt(id) },
});
}
async updateById(id, researchData) {
return prisma.hasilResearch.update({
where: { id: parseInt(id) },
data: researchData,
});
}
async deleteById(id) {
return prisma.hasilResearch.delete({
where: { id: parseInt(id) },
});
}
}
module.exports = new HasilResearchRepository();
+121
View File
@@ -0,0 +1,121 @@
const hasilResearchRepository = require('../repositories/hasilResearch.repository');
const path = require('path');
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
const imageUploadHelper = require('../../../helpers/utils/imageUpload.helper');
const { data, error } = require('../../../helpers/utils/wrapper');
const { NotFoundError } = require('../../../helpers/error');
class HasilResearchService {
async createHasilResearch(researchData, imageFile) {
try {
const imagePath = imageUploadHelper.uploadImage(imageFile);
const newResearchData = {
...researchData,
image_path: imagePath,
};
const result = await hasilResearchRepository.create(newResearchData);
return data(result);
} catch (err) {
if (imageFile?.filename) {
try {
await deleteFileIfExists(imageFile.filename);
} catch (cleanupErr) {
console.error('Failed to cleanup file:', cleanupErr);
}
}
return error(err);
}
}
async getAllHasilResearch() {
try {
const researchList = await hasilResearchRepository.findAll();
return data(researchList);
} catch (err) {
return error(err);
}
}
async getHasilResearchById(id) {
try {
const research = await hasilResearchRepository.findById(id);
if (!research) {
return error(new NotFoundError('Hasil Research Tidak Ditemukan'));
}
return data(research);
} catch (err) {
return error(err);
}
}
async updateHasilResearch(id, researchData, imageFile) {
let oldImagePath = null;
let newImageUploaded = false;
try {
const checkResult = await this.getHasilResearchById(id);
if (checkResult.err) {
return checkResult;
}
const existingResearch = checkResult.data;
let imagePath = existingResearch.image_path;
if (imageFile) {
oldImagePath = existingResearch.image_path;
imagePath = imageUploadHelper.uploadImage(imageFile);
newImageUploaded = true;
if (oldImagePath) {
const oldFileName = path.basename(oldImagePath);
await deleteFileIfExists(oldFileName);
}
}
const updatedResearchData = {
...researchData,
image_path: imagePath,
};
const result = await hasilResearchRepository.updateById(id, updatedResearchData);
return data(result);
} catch (err) {
if (newImageUploaded && imageFile?.filename) {
try {
await deleteFileIfExists(imageFile.filename);
} catch (cleanupErr) {
console.error('Failed to cleanup new file:', cleanupErr);
}
}
return error(err);
}
}
async deleteHasilResearch(id) {
try {
const checkResult = await this.getHasilResearchById(id);
if (checkResult.err) {
return checkResult;
}
const research = checkResult.data;
if (research.image_path) {
const fileName = path.basename(research.image_path);
await deleteFileIfExists(fileName);
}
const result = await hasilResearchRepository.deleteById(id);
return data(result);
} catch (err) {
return error(err);
}
}
}
module.exports = new HasilResearchService();
@@ -0,0 +1,312 @@
const lamaranMagangService = require('../services/lamaranMagang.service');
const { response, error, paginationResponse } = require('../../../helpers/utils/wrapper');
const { InternalServerError } = require('../../../helpers/error');
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
const { isValidPayload } = require('../../../helpers/utils/validator');
const { createLamaranMagangModel, updateLamaranStatusModel, getAllLamaranMagangModel, statisticModel } = require('../models/lamaranMagang.model');
const { cleanupUploadedFiles } = require('../helpers/cleanUpFiles');
class LamaranMagangController {
async addLamaranMagang(req, res) {
try {
const { id_lowongan_magang } = req.params;
const uploadedFiles = {
cv: req.files?.cv?.[0],
portofolio: req.files?.portofolio?.[0],
};
const payloadWithFiles = {
...req.body,
cv: uploadedFiles.cv?.filename,
portofolio: uploadedFiles.portofolio?.filename,
id_lowongan_magang,
};
const validatePayload = isValidPayload(payloadWithFiles, createLamaranMagangModel);
if (validatePayload.err) {
await cleanupUploadedFiles(uploadedFiles);
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const { id_lowongan_magang: validatedLowonganId, cv, portofolio, ...mahasiswaFormData } = validatePayload.data;
const result = await lamaranMagangService.createLamaranMagang(
validatedLowonganId,
mahasiswaFormData,
uploadedFiles
);
if (result.err) {
await cleanupUploadedFiles(uploadedFiles);
return response(res, 'fail', result, 'Gagal mengajukan lamaran magang', ERROR.INTERNAL_ERROR);
}
return response(res, 'success', result, 'Lamaran magang berhasil diajukan', SUCCESS.CREATED);
} catch (err) {
await cleanupUploadedFiles({
cv: req.files?.cv?.[0],
portofolio: req.files?.portofolio?.[0],
});
return response(
res,
'fail',
error(new InternalServerError(err.message)),
'Terjadi kesalahan yang tidak terduga',
ERROR.INTERNAL_ERROR
);
}
}
async getAllLamaranMagang(req, res) {
try {
const validatePayload = isValidPayload(req.query, getAllLamaranMagangModel);
if (validatePayload.err) {
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const query = validatePayload.data;
const result = await lamaranMagangService.getAllLamaranMagang(query);
if (result.err) {
return response(res, 'fail', result);
}
return paginationResponse(res, 'success', result, 'Lamaran magang berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getLamaranByID(req, res) {
try {
const { id_lamaran_magang } = req.params;
const result = await lamaranMagangService.getLamaranById(id_lamaran_magang);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Lamaran magang berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getDetailLamaranById(req, res) {
try {
const { id } = req.params;
const result = await lamaranMagangService.getDetailLamaranById(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Detail lamaran magang berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async updateStatusLamaran(req, res) {
try {
const { id_lamaran_magang } = req.params;
const validatePayload = isValidPayload(req.body, updateLamaranStatusModel);
if (validatePayload.err) {
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const { status } = validatePayload.data;
const result = await lamaranMagangService.updateLamaranStatus(id_lamaran_magang, status);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Status lamaran berhasil diperbarui', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async deleteLamaran(req, res) {
try {
const { id } = req.params;
const result = await lamaranMagangService.deleteLamaran(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Lamaran magang berhasil dihapus', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async exportLamaranToExcel(req, res) {
try {
const result = await lamaranMagangService.exportToExcel();
if (result.err) {
return response(res, 'fail', result);
}
const workbook = result.data;
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
res.setHeader(
'Content-Disposition',
'attachment; filename=lamaran_magang.xlsx'
);
await workbook.xlsx.write(res);
res.end();
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getDashboardStatistics(req, res) {
try {
const validateQuery = isValidPayload(req.query, statisticModel);
if (validateQuery.err) {
return response(
res,
'fail',
{ err: validateQuery.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const query = validateQuery.data
const result = await lamaranMagangService.getDashboardStatistics(query);
if (result.err) {
return response(res, 'fail', result, 'Gagal mengambil statistik dashboard', ERROR.INTERNAL_ERROR);
}
return response(res, 'success', result, 'Statistik dashboard berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getStatisticsByPosition(req, res) {
try {
const validateQuery = isValidPayload(req.query, statisticModel);
if (validateQuery.err) {
return response(
res,
'fail',
{ err: validateQuery.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const query = validateQuery.data;
const result = await lamaranMagangService.getStatisticsByPosition(query);
if (result.err) {
return response(res, 'fail', result, 'Gagal mengambil statistik berdasarkan posisi', ERROR.INTERNAL_ERROR);
}
return response(res, 'success', result, 'Statistik berdasarkan posisi berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getStatisticsByCountry(req, res) {
try {
const validateQuery = isValidPayload(req.query, statisticModel);
if (validateQuery.err) {
return response(
res,
'fail',
{ err: validateQuery.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const query = validateQuery.data;
const result = await lamaranMagangService.getStatisticsByCountry(query);
if (result.err) {
return response(res, 'fail', result, 'Gagal mengambil statistik berdasarkan negara', ERROR.INTERNAL_ERROR);
}
return response(res, 'success', result, 'Statistik berdasarkan negara berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getStatisticsByUniversity(req, res) {
try {
const validateQuery = isValidPayload(req.query, statisticModel);
if (validateQuery.err) {
return response(
res,
'fail',
{ err: validateQuery.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const query = validateQuery.data;
const result = await lamaranMagangService.getStatisticsByUniversity(query);
if (result.err) {
return response(res, 'fail', result, 'Gagal mengambil statistik berdasarkan universitas', ERROR.INTERNAL_ERROR);
}
return response(res, 'success', result, 'Statistik berdasarkan universitas berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
}
module.exports = new LamaranMagangController();
+14
View File
@@ -0,0 +1,14 @@
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
const cleanupUploadedFiles = async (files) => {
if (files?.cv?.filename) {
await deleteFileIfExists(files.cv.filename);
}
if (files?.portofolio?.filename) {
await deleteFileIfExists(files.portofolio.filename);
}
};
module.exports = {
cleanupUploadedFiles,
};
+102
View File
@@ -0,0 +1,102 @@
const transporter = require('../../../helpers/utils/mail');
const sendConfirmationEmail = async (mahasiswa, lowongan) => {
try {
const { nama_depan, nama_belakang, email } = mahasiswa;
const { posisi } = lowongan;
const fullName = `${nama_depan}${nama_belakang ? ' ' + nama_belakang : ''}`;
const mailOptions = {
from: process.env.MAIL_USER,
to: email,
subject: 'Konfirmasi Penerimaan Lamaran Anda',
headers: {
'X-Priority': '1',
'X-MSMail-Priority': 'High',
Importance: 'high',
},
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: auto; padding: 20px; border: 1px solid #e0e0e0; border-radius: 10px;">
<h2 style="color: #333;">Lamaran Anda Telah Kami Terima</h2>
<p>Halo ${fullName},</p>
<p>Terima kasih telah mengirimkan lamaran untuk posisi <strong>${posisi}</strong> di <strong>Humic Engineering</strong>.</p>
<p>Kami telah menerima dokumen dan informasi yang Anda kirimkan, dan saat ini tim rekrutmen kami sedang meninjaunya dengan seksama.</p>
<p>Jika profil Anda sesuai dengan kebutuhan kami, tim kami akan menghubungi Anda untuk tahapan seleksi berikutnya. Proses ini dapat memakan waktu hingga beberapa minggu.</p>
<p>Kami sangat menghargai minat Anda untuk bergabung bersama tim kami.</p>
<p>Salam hangat,</p>
<p><strong>Tim Rekrutmen Humic Engineering</strong></p>
<hr style="margin: 30px 0;">
<p style="font-size: 12px; color: #888;">Email ini dikirim secara otomatis. Jika Anda memiliki pertanyaan lebih lanjut, silakan hubungi kami melalui email resmi yang tersedia di website kami.</p>
</div>
`,
};
await transporter.sendMail(mailOptions);
} catch (error) {
console.error('Error sending confirmation email:', error);
}
};
const sendStatusEmail = async (lamaran, status) => {
try {
const { mahasiswa, lowongan_magang } = lamaran;
const { nama_depan, nama_belakang, email } = mahasiswa;
const { posisi } = lowongan_magang;
const isAccepted = status === 'diterima';
const fullName = `${nama_depan}${nama_belakang ? ' ' + nama_belakang : ''}`;
const subject = isAccepted
? 'Selamat! Lamaran Anda Diterima'
: 'Pemberitahuan Status Lamaran Anda';
const message = isAccepted
? `
<p>Halo ${fullName},</p>
<p>Selamat! Lamaran Anda untuk posisi <strong>${posisi}</strong> di <strong>Humic Engineering</strong> telah diterima.</p>
<p>Tim kami sangat terkesan dengan profil dan kualifikasi Anda. Kami akan segera menghubungi Anda terkait tahapan selanjutnya.</p>
<p>Terima kasih telah melamar dan kami menantikan kerja sama yang luar biasa bersama Anda.</p>
`
: `
<p>Halo ${fullName},</p>
<p>Terima kasih atas lamaran Anda untuk posisi <strong>${posisi}</strong> di <strong>Humic Engineering</strong>.</p>
<p>Setelah mempertimbangkan secara seksama, kami memutuskan untuk tidak melanjutkan proses lamaran Anda ke tahap berikutnya.</p>
<p>Jangan berkecil hati—kami sangat menghargai waktu dan usaha Anda. Kami mendorong Anda untuk terus mencoba dan semoga sukses dalam perjalanan karier Anda.</p>
`;
const mailOptions = {
from: process.env.MAIL_USER,
to: email,
subject,
headers: {
'X-Priority': '1',
'X-MSMail-Priority': 'High',
Importance: 'high',
},
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: auto; padding: 20px; border: 1px solid #e0e0e0; border-radius: 10px;">
${message}
<p>Salam hangat,</p>
<p><strong>Tim Rekrutmen Humic Engineering</strong></p>
<hr style="margin: 30px 0;">
<p style="font-size: 12px; color: #888;">Email ini dikirim secara otomatis. Untuk informasi lebih lanjut, silakan hubungi kami melalui kontak resmi.</p>
</div>
`,
};
await transporter.sendMail(mailOptions);
} catch (error) {
console.error('Gagal mengirim email status lamaran:', error);
}
};
module.exports = {
sendConfirmationEmail,
sendStatusEmail,
};

Some files were not shown because too many files have changed in this diff Show More