refactor: rewrite ke codebase lama
This commit is contained in:
Regular → Executable
+16
-10
@@ -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=**********
|
||||
|
||||
Executable
+83
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
Executable
+220
@@ -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.
|
||||
Executable
+45
@@ -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' });
|
||||
}
|
||||
};
|
||||
Executable
+70
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
};
|
||||
Executable
+21
@@ -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
@@ -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,
|
||||
};
|
||||
Executable
+36
@@ -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]);
|
||||
}
|
||||
|
||||
|
||||
Executable
+28
@@ -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]);
|
||||
}
|
||||
Executable
+123
@@ -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');
|
||||
}
|
||||
Executable
+55
@@ -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]);
|
||||
}
|
||||
Executable
+45
@@ -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');
|
||||
}
|
||||
Executable
+28
@@ -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]);
|
||||
}
|
||||
+2016
-2097
File diff suppressed because it is too large
Load Diff
Regular → Executable
+27
-33
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
Regular → Executable
+110
-63
@@ -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;
|
||||
+5
@@ -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;
|
||||
Regular → Executable
Executable
+9
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from '@prisma/client';
|
||||
|
||||
export default defineConfig({
|
||||
datasources: {
|
||||
db: {
|
||||
url: process.env.DATABASE_URL,
|
||||
},
|
||||
},
|
||||
});
|
||||
Regular → Executable
+131
-138
@@ -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
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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
@@ -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;
|
||||
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 412 KiB |
Executable
+111
@@ -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;
|
||||
|
||||
|
||||
Executable
+180
@@ -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;
|
||||
@@ -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;
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
const { PrismaClient } = require('../../generated/prisma');
|
||||
|
||||
const prisma = new PrismaClient({
|
||||
log: ['query', 'info', 'warn', 'error'],
|
||||
});
|
||||
|
||||
module.exports = prisma
|
||||
Executable
+24
@@ -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 };
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
const CommonError = require('./common_error');
|
||||
|
||||
class BadRequestError extends CommonError {
|
||||
constructor(message) {
|
||||
super(message || 'Bad Request');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BadRequestError;
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
class CommonError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CommonError;
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
const CommonError = require('./common_error');
|
||||
|
||||
class ConflictError extends CommonError {
|
||||
constructor(message) {
|
||||
super(message || 'Conflict');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ConflictError;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
const CommonError = require('./common_error');
|
||||
|
||||
class ExpectationFailedError extends CommonError {
|
||||
constructor(message) {
|
||||
super(message || 'Expectation Failed');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ExpectationFailedError;
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
const CommonError = require('./common_error');
|
||||
|
||||
class ForbiddenError extends CommonError {
|
||||
constructor(message) {
|
||||
super(message || 'Forbidden');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ForbiddenError;
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
const CommonError = require('./common_error');
|
||||
|
||||
class GatewayTimeoutError extends CommonError {
|
||||
constructor(message) {
|
||||
super(message || 'Gateway Timeout');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GatewayTimeoutError;
|
||||
Executable
+23
@@ -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
|
||||
};
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
const CommonError = require('./common_error');
|
||||
|
||||
class InternalServerError extends CommonError {
|
||||
constructor(message) {
|
||||
super(message || 'Internal Server Error');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InternalServerError;
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
const CommonError = require('./common_error');
|
||||
|
||||
class NotFoundError extends CommonError {
|
||||
constructor(message) {
|
||||
super(message || 'Not Found');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NotFoundError;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
const CommonError = require('./common_error');
|
||||
|
||||
class ServiceUnavailableError extends CommonError {
|
||||
constructor(message) {
|
||||
super(message || 'Service Unavailable');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ServiceUnavailableError;
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
const CommonError = require('./common_error');
|
||||
|
||||
class Unauthorized extends CommonError {
|
||||
constructor(message) {
|
||||
super(message || 'Unauthorized');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Unauthorized;
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
class ValidationError extends Error {
|
||||
constructor(message, errors) {
|
||||
super(message || 'Validation Error');
|
||||
this.errors = errors
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ValidationError;
|
||||
Executable
+21
@@ -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
|
||||
};
|
||||
Executable
+30
@@ -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 };
|
||||
Executable
+53
@@ -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
|
||||
};
|
||||
Executable
+57
@@ -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();
|
||||
Executable
+20
@@ -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;
|
||||
Executable
+25
@@ -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,
|
||||
};
|
||||
Executable
+95
@@ -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
@@ -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 😁
|
||||
*/
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
import { PrismaClient } from '../generated/prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export default prisma;
|
||||
@@ -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();
|
||||
};
|
||||
};
|
||||
@@ -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 }),
|
||||
});
|
||||
};
|
||||
Executable
+11
@@ -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);
|
||||
}
|
||||
Executable
+124
@@ -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;
|
||||
Executable
+44
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
Executable
+72
@@ -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
@@ -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();
|
||||
Executable
+9
@@ -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
|
||||
};
|
||||
Executable
+75
@@ -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
@@ -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();
|
||||
Executable
+114
@@ -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
@@ -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();
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
const authController = require('./controllers/auth.controller');
|
||||
const authService = require('./services/auth.service');
|
||||
|
||||
module.exports = {
|
||||
authController,
|
||||
authService
|
||||
};
|
||||
Executable
+74
@@ -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,
|
||||
};
|
||||
Executable
+72
@@ -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
@@ -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();
|
||||
Executable
+9
@@ -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,
|
||||
};
|
||||
Executable
+30
@@ -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
@@ -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();
|
||||
Executable
+119
@@ -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
@@ -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();
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
const faqController = require('./controllers/faq.controllers');
|
||||
|
||||
module.exports = {
|
||||
faqController,
|
||||
};
|
||||
Executable
+26
@@ -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
@@ -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();
|
||||
Executable
+67
@@ -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
@@ -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();
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
const feedbackController = require('./controllers/feedback.controller');
|
||||
|
||||
module.exports = {
|
||||
feedbackController,
|
||||
};
|
||||
+108
@@ -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,
|
||||
};
|
||||
@@ -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
@@ -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();
|
||||
Executable
+9
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
Reference in New Issue
Block a user