diff --git a/.env.example b/.env.example old mode 100644 new mode 100755 index c5130b8..4e8d86e --- a/.env.example +++ b/.env.example @@ -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 \ No newline at end of file + +// 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=********** diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100755 index 0000000..a39ad33 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -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 }} diff --git a/.github/workflows/dev-deploy_internify-backend.yml b/.github/workflows/dev-deploy_internify-backend.yml new file mode 100755 index 0000000..b523a4e --- /dev/null +++ b/.github/workflows/dev-deploy_internify-backend.yml @@ -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: . diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index 1bf3c8a..677bd26 --- a/.gitignore +++ b/.gitignore @@ -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 -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* -*.log - -# Editor & IDE settings -.vscode/ -.idea/ -.zed/ -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? - -# OS metadata -.DS_Store -Thumbs.db - -# Prisma -/src/generated/prisma \ No newline at end of file +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# 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/ + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# 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 diff --git a/.vercelignore b/.vercelignore new file mode 100755 index 0000000..fd82c49 --- /dev/null +++ b/.vercelignore @@ -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 diff --git a/README.md b/README.md old mode 100644 new mode 100755 index a04c188..71d1b16 --- a/README.md +++ b/README.md @@ -1 +1,1822 @@ -# internify-be \ No newline at end of file +# Internify API Documentation + +[![Express Logo](https://i.cloudup.com/zfY6lL7eFa-3000x3000.png)](http://expressjs.com/) + +Backend API untuk Website Penerimaan Magang HUMIC Engineering - Dibangun dengan Express.js, Prisma ORM, dan MySQL. + +--- + +## ๐Ÿ“‘ Table of Contents + +1. [Introduction](#introduction) +2. [Quick Start](#-quick-start) + - [Prerequisites](#prerequisites) + - [Installation](#installation) + - [Environment Setup](#environment-setup) + - [Database Setup](#database-setup) + - [Running the Server](#running-the-server) +3. [Project Structure](#-project-structure) +4. [Architecture](#-architecture) + - [Technology Stack](#technology-stack) + - [Module Architecture](#module-architecture-4-layer-pattern) + - [Request Flow](#request-flow) +5. [Development Guide](#-development-guide) + - [Creating a New Module](#creating-a-new-module) + - [Using Wrapper Functions](#using-wrapper-functions) + - [Validation with Joi](#validation-with-joi) + - [File Upload Handling](#file-upload-handling) +6. [API Documentation](#-api-documentation) +7. [Testing](#-testing) +8. [Deployment](#-deployment) +9. [Contributing](#-contributing) +10. [Troubleshooting](#-troubleshooting) + +--- + +## Introduction + +Project intern HUMIC Batch 4 & 5 - Website Penerimaan Magang + +**Framework:** [Express.js](https://expressjs.com/) +**ORM:** [Prisma](https://www.prisma.io/) +**Database:** MySQL +**Authentication:** JWT (RS256) + +### ๐Ÿ‘ฅ Development Team + +**Team Humic Intern Batch 4** +|**Nama**| **Role** | +|:-------|:-----------:| +|REINHARD EFRAIM SITUMEANG |UI/UX WEBSITE| +|SHAFA SALMA PERMANA|UI/UX MOBILE| +|MUHAMMAD FARIED GUNAWAN|FRONT END DEVELOPER| +|YOHANES JANUARICO ANDIAWAN|BACK END DEVELOPER | +|REIHAN RAMADHANA ANWARI |BACK END DEVELOPER | +|DELKANO MARZUKI BERUTU|MOBILE DEVELOPER | + +**Team Humic Intern Batch 5** +|**Nama**| **Role** | +|:-------|:-----------:| +|BILLY MARCELO |FRONTEND DEVELOPER| +|MAHESA BAGUS RADITYA |BACK END DEVELOPER| +|SASI AFRILIA |UI/UX WEBSITE| + +### ๐Ÿ—„๏ธ Database Structure + +![Entity Relationship Diagram](https://raw.githubusercontent.com/Ditrogen/backend-intern-humic/main/src/docs/ERD.png) +The database consists of the following tables: + +1. **mahasiswa**: Menyimpan data lengkap mahasiswa yang mendaftar magang. +2. **admin**: Digunakan untuk login dan mengelola lowongan. +3. **lowongan_magang**: Informasi detail tentang posisi magang yang tersedia. +4. **lamaran_magang**: Data lamaran yang diajukan mahasiswa. +5. **hasil_research**: Menyimpan hasil riset proyek dari mahasiswa atau tim. +6. **partnership**: Data partner/institusi yang bekerja sama. +7. **faq**: Data pertanyaan dan jawaban yang sering ditanyakan (Frequently Asked Questions). + +--- + +## ๐Ÿš€ Quick Start + +### Prerequisites + +Pastikan Anda sudah menginstall: + +- **Node.js** (v16 atau lebih tinggi) +- **npm** atau **yarn** +- **MySQL** (v8 atau lebih tinggi) +- **Git** + +### Installation + +#### 1. Clone Repository + +```bash +git clone https://github.com/Ditrogen/backend-intern-humic.git +cd backend-intern-humic +``` + +#### 2. Install Dependencies + +```bash +npm install +``` + +### Environment Setup + +#### 3. Buat File .env + +Salin file `.env.example` menjadi `.env`: + +```bash +cp .env.example .env +``` + +#### 4. Konfigurasi Environment Variables + +Edit file `.env` dan isi dengan konfigurasi Anda: + +```env +# Database Configuration +DATABASE_URL="mysql://username:password@localhost:3306/internify_db" + +# Server Configuration +PORT=9000 + +# Email Configuration (untuk notifikasi) +AUTH_EMAIL='your-email@gmail.com' +AUTH_PASSWORD='your-app-specific-password' + +# JWT Configuration +JWT_PRIVATE_KEY=-----BEGIN RSA PRIVATE KEY-----\nYOUR_PRIVATE_KEY\n-----END RSA PRIVATE KEY----- +JWT_PUBLIC_KEY=-----BEGIN PUBLIC KEY-----\nYOUR_PUBLIC_KEY\n-----END PUBLIC KEY----- + +# ReCaptcha Configuration +RECAPTCHA_SECRET_KEY=your-recaptcha-secret-key +RECAPTCHA_SITE_KEY=your-recaptcha-site-key +``` + +> **๐Ÿ’ก Tips:** Untuk generate RSA key pair, gunakan OpenSSL: +> +> ```bash +> # Generate private key +> openssl genrsa -out private.pem 2048 +> # Generate public key +> openssl rsa -in private.pem -pubout -out public.pem +> ``` + +### Database Setup + +#### 5. Buat Database MySQL + +```bash +mysql -u root -p +CREATE DATABASE internify_db; +EXIT; +``` + +#### 6. Generate Prisma Client + +```bash +npx prisma generate +``` + +#### 7. Run Database Migration + +```bash +npx prisma migrate dev +``` + +> Ini akan membuat semua tabel yang diperlukan berdasarkan schema Prisma. + +#### 8. (Optional) Seed Database + +Jika ada data seeder: + +```bash +npx prisma db seed +``` + +### Running the Server + +#### Development Mode + +```bash +npm run dev +``` + +Server akan berjalan di `http://localhost:9000` + +#### Production Mode + +```bash +npm start +``` + +> **โš ๏ธ Perhatian:** +> +> - Gunakan `npm run dev` untuk development +> - Gunakan `npm start` untuk production + +### Testing the API + +Setelah server berjalan, buka Swagger UI untuk testing: + +``` +http://localhost:9000/api-docs +``` + +> **๐Ÿ“ Note:** Swagger hanya tersedia jika `NODE_ENV` bukan `production` dan `SWAGGER_ENABLED` bernilai `true` + +--- + +## ๐Ÿ“š Project Structure + +``` +backend-intern-humic/ +โ”œโ”€โ”€ prisma/ +โ”‚ โ”œโ”€โ”€ schema.prisma # Prisma schema definition +โ”‚ โ”œโ”€โ”€ prisma.config.js # Prisma configuration +โ”‚ โ””โ”€โ”€ migrations/ # Database migrations +โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ index.js # Main application entry point +โ”‚ โ”œโ”€โ”€ docs/ +โ”‚ โ”‚ โ”œโ”€โ”€ swagger.js # Swagger/OpenAPI configuration +โ”‚ โ”‚ โ””โ”€โ”€ ERD.png # Entity Relationship Diagram +โ”‚ โ”œโ”€โ”€ helpers/ +โ”‚ โ”‚ โ”œโ”€โ”€ db/ # Database connection (Prisma client) +โ”‚ โ”‚ โ”œโ”€โ”€ error/ # Custom error classes +โ”‚ โ”‚ โ”œโ”€โ”€ http-status/ # HTTP status codes +โ”‚ โ”‚ โ”œโ”€โ”€ infra/ # Infrastructure config +โ”‚ โ”‚ โ””โ”€โ”€ utils/ # Utility functions +โ”‚ โ”‚ โ”œโ”€โ”€ validator.js # Joi validation helpers +โ”‚ โ”‚ โ”œโ”€โ”€ wrapper.js # Response wrapper functions +โ”‚ โ”‚ โ””โ”€โ”€ fileHelper.js # File handling utilities +โ”‚ โ”œโ”€โ”€ middleware/ +โ”‚ โ”‚ โ”œโ”€โ”€ multer.js # File upload configuration +โ”‚ โ”‚ โ”œโ”€โ”€ recaptcha.js # ReCaptcha verification +โ”‚ โ”‚ โ””โ”€โ”€ verifyJWT.js # JWT authentication +โ”‚ โ”œโ”€โ”€ modules/ # Feature modules (MVC pattern) +โ”‚ โ”‚ โ”œโ”€โ”€ admin/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ controllers/ # Handle HTTP requests +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ services/ # Business logic +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ repositories/ # Database operations +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ models/ # Joi validation schemas +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ index.js # Module exports +โ”‚ โ”‚ โ”œโ”€โ”€ auth/ +โ”‚ โ”‚ โ”œโ”€โ”€ batch/ +โ”‚ โ”‚ โ”œโ”€โ”€ faq/ +โ”‚ โ”‚ โ”œโ”€โ”€ feedback/ +โ”‚ โ”‚ โ”œโ”€โ”€ hasilResearch/ +โ”‚ โ”‚ โ”œโ”€โ”€ lamaranMagang/ +โ”‚ โ”‚ โ”œโ”€โ”€ lowonganMagang/ +โ”‚ โ”‚ โ”œโ”€โ”€ mahasiswa/ +โ”‚ โ”‚ โ””โ”€โ”€ partnership/ +โ”‚ โ”œโ”€โ”€ routes/ # API route definitions +โ”‚ โ”‚ โ”œโ”€โ”€ admin.routes.js +โ”‚ โ”‚ โ”œโ”€โ”€ auth.routes.js +โ”‚ โ”‚ โ”œโ”€โ”€ ... +โ”‚ โ””โ”€โ”€ uploads/ # File upload directory (development) +โ”œโ”€โ”€ .env # Environment variables (not committed) +โ”œโ”€โ”€ .env.example # Environment variables template +โ”œโ”€โ”€ package.json # Node.js dependencies and scripts +โ”œโ”€โ”€ vercel.json # Vercel deployment configuration +โ””โ”€โ”€ README.md # This file +``` + +### Folder Explanation + +| Folder/File | Purpose | +| ----------------- | ----------------------------------------------- | +| `prisma/` | Prisma ORM schema, config, dan migrations | +| `src/index.js` | Entry point Express application | +| `src/helpers/` | Utility functions, error classes, DB connection | +| `src/middleware/` | Express middleware (auth, upload, validation) | +| `src/modules/` | Feature modules following 4-layer architecture | +| `src/routes/` | API endpoint definitions | +| `.env` | Environment variables (credentials, config) | +| `vercel.json` | Serverless deployment configuration | + +--- + +## ๐Ÿ›๏ธ Architecture + +### Technology Stack + +| Technology | Version | Purpose | +| -------------- | ------- | -------------------------------- | +| **Node.js** | v16+ | Runtime environment | +| **Express.js** | 4.x | Web framework | +| **Prisma** | 6.16.2 | ORM (Object-Relational Mapping) | +| **MySQL** | 8.x | Relational database | +| **JWT** | - | Authentication (RS256 signature) | +| **Joi** | - | Schema validation | +| **Multer** | - | File upload handling | +| **Nodemailer** | - | Email service | +| **Swagger** | - | API documentation | +| **Vercel** | - | Serverless deployment | + +### Module Architecture (4-Layer Pattern) + +Setiap module di folder `src/modules/` mengikuti **arsitektur 4 layer** untuk separation of concerns: + +``` +module/ +โ”œโ”€โ”€ controllers/ โ†’ Layer 1: Menangani HTTP request/response +โ”œโ”€โ”€ services/ โ†’ Layer 2: Business logic dan pemrosesan data +โ”œโ”€โ”€ repositories/ โ†’ Layer 3: Operasi database (Prisma queries) +โ”œโ”€โ”€ models/ โ†’ Layer 4: Validasi input (Joi schemas) +โ””โ”€โ”€ index.js โ†’ Module exports +``` + +#### Layer Responsibilities + +**1. Controllers** (`controllers/`) + +- Menerima HTTP request dari route +- Validasi input menggunakan Joi models +- Memanggil service layer +- Mengembalikan HTTP response dengan wrapper +- Error handling + +**2. Services** (`services/`) + +- Business logic utama +- Validasi business rules +- Memanggil repository untuk database operations +- Data transformation +- Orchestration antar repositories + +**3. Repositories** (`repositories/`) + +- Database operations (CRUD) menggunakan Prisma +- Query construction +- Data mapping +- Tidak ada business logic + +**4. Models** (`models/`) + +- Joi schema definitions +- Input validation rules +- Error messages (Indonesian) + +### Request Flow + +```mermaid +graph LR + A[Client] -->|HTTP Request| B[Route] + B --> C[Controller] + C -->|Validate| D[Joi Model] + D -->|Valid| E[Service] + E -->|Business Logic| E + E --> F[Repository] + F -->|Prisma Query| G[MySQL Database] + G -->|Data| F + F --> E + E --> C + C -->|Wrapper| H[HTTP Response] + H --> A +``` + +**Step-by-step:** + +1. **Client** mengirim HTTP request +2. **Route** menerima request dan memanggil controller +3. **Controller** validasi input menggunakan **Joi Model** +4. **Controller** memanggil **Service** +5. **Service** memproses business logic +6. **Service** memanggil **Repository** +7. **Repository** query database menggunakan **Prisma** +8. **Database** mengembalikan data ke **Repository** +9. **Repository** mengembalikan data ke **Service** +10. **Service** mengembalikan data ke **Controller** +11. **Controller** mengirim response dengan **wrapper** +12. **Client** menerima HTTP response + +### Key Features + +โœ… **JWT Authentication (RS256)** - RSA signature-based tokens dengan token expiry 1 day +โœ… **Joi Validation** - Schema validation untuk semua input dengan pesan error dalam Bahasa Indonesia +โœ… **File Upload Management** - Automatic file cleanup on errors, support multiple file types +โœ… **Automatic Status Updates** - Lowongan magang status auto-update berdasarkan tanggal +โœ… **CORS Configuration** - Configurable CORS untuk cross-origin requests +โœ… **Centralized Error Handling** - Custom error classes dengan pesan konsisten +โœ… **Response Wrapper** - Standardized API response format +โœ… **Swagger Documentation** - Interactive API documentation dengan Swagger UI + +--- + +## ๐Ÿ› ๏ธ Development Guide + +### Creating a New Module + +Ikuti langkah-langkah berikut untuk membuat module baru: + +## Cara Membuat Module (Langkah demi Langkah) + +### Langkah 1: Buat Struktur Module + +```bash +modules/ + yourModule/ + โ”œโ”€โ”€ index.js # Export semua komponen + โ”œโ”€โ”€ controllers/ + โ”‚ โ””โ”€โ”€ yourModule.controller.js + โ”œโ”€โ”€ services/ + โ”‚ โ””โ”€โ”€ yourModule.service.js + โ”œโ”€โ”€ repositories/ + โ”‚ โ””โ”€โ”€ yourModule.repository.js + โ””โ”€โ”€ models/ + โ””โ”€โ”€ yourModule.model.js +``` + +### Langkah 2: Buat Validation Model (models/) + +**File:** `models/yourModule.model.js` + +```javascript +const joi = require("joi"); + +// Validasi untuk membuat record baru +const createYourModuleModel = joi.object({ + name: joi.string().required().messages({ + "string.base": "Nama harus berupa teks.", + "any.required": "Nama wajib diisi.", + }), + email: joi.string().email().required().messages({ + "string.email": "Format email tidak valid.", + "any.required": "Email wajib diisi.", + }), + age: joi.number().min(18).messages({ + "number.base": "Umur harus berupa angka.", + "number.min": "Umur minimal 18 tahun.", + }), +}); + +// Validasi untuk update record +const updateYourModuleModel = joi.object({ + name: joi.string().messages({ + "string.base": "Nama harus berupa teks.", + }), + email: joi.string().email().messages({ + "string.email": "Format email tidak valid.", + }), + age: joi.number().min(18).messages({ + "number.base": "Umur harus berupa angka.", + "number.min": "Umur minimal 18 tahun.", + }), +}); + +module.exports = { + createYourModuleModel, + updateYourModuleModel, +}; +``` + +### Langkah 3: Buat Repository (repositories/) + +**File:** `repositories/yourModule.repository.js` + +```javascript +const prisma = require("../../../helpers/db/db_connection"); + +class YourModuleRepository { + // Buat record baru + async create(data) { + return prisma.yourModel.create({ + data: data, + }); + } + + // Ambil semua record + async findAll() { + return prisma.yourModel.findMany(); + } + + // Ambil berdasarkan ID + async findById(id) { + return prisma.yourModel.findUnique({ + where: { id: parseInt(id) }, + }); + } + + // Update berdasarkan ID + async updateById(id, data) { + return prisma.yourModel.update({ + where: { id: parseInt(id) }, + data: data, + }); + } + + // Hapus berdasarkan ID + async deleteById(id) { + return prisma.yourModel.delete({ + where: { id: parseInt(id) }, + }); + } +} + +module.exports = new YourModuleRepository(); +``` + +### Langkah 4: Buat Service (services/) + +**File:** `services/yourModule.service.js` + +```javascript +const yourModuleRepository = require("../repositories/yourModule.repository"); +const { data, error } = require("../../../helpers/utils/wrapper"); +const { NotFoundError, BadRequestError } = require("../../../helpers/error"); + +class YourModuleService { + // Buat record baru + async createYourModule(moduleData) { + try { + // Business logic di sini (contoh: cek duplikat) + const existing = await yourModuleRepository.findByEmail(moduleData.email); + + if (existing) { + return error(new BadRequestError("Email Sudah Terdaftar")); + } + + const result = await yourModuleRepository.create(moduleData); + return data(result); + } catch (err) { + return error(err); + } + } + + // Ambil semua record + async getAllYourModule() { + try { + const results = await yourModuleRepository.findAll(); + return data(results); + } catch (err) { + return error(err); + } + } + + // Ambil berdasarkan ID + async getYourModuleById(id) { + try { + const result = await yourModuleRepository.findById(id); + + if (!result) { + return error(new NotFoundError("Data Tidak Ditemukan")); + } + + return data(result); + } catch (err) { + return error(err); + } + } + + // Update berdasarkan ID + async updateYourModule(id, moduleData) { + try { + // Cek apakah data ada + const checkResult = await this.getYourModuleById(id); + if (checkResult.err) { + return checkResult; + } + + const result = await yourModuleRepository.updateById(id, moduleData); + return data(result); + } catch (err) { + return error(err); + } + } + + // Hapus berdasarkan ID + async deleteYourModule(id) { + try { + // Cek apakah data ada + const checkResult = await this.getYourModuleById(id); + if (checkResult.err) { + return checkResult; + } + + const result = await yourModuleRepository.deleteById(id); + return data(result); + } catch (err) { + return error(err); + } + } +} + +module.exports = new YourModuleService(); +``` + +### Langkah 5: Buat Controller (controllers/) + +**File:** `controllers/yourModule.controller.js` + +```javascript +const yourModuleService = require("../services/yourModule.service"); +const { + createYourModuleModel, + updateYourModuleModel, +} = require("../models/yourModule.model"); +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 YourModuleController { + // CREATE - POST /your-module-api/add + async addYourModule(req, res) { + try { + const payload = req.body; + + // Validasi input + const validatePayload = isValidPayload(payload, createYourModuleModel); + + if (validatePayload.err) { + return response( + res, + "fail", + validatePayload, + "Validasi gagal", + ERROR.EXPECTATION_FAILED, + ); + } + + // Panggil service + const result = await yourModuleService.createYourModule( + validatePayload.data, + ); + + if (result.err) { + return response(res, "fail", result); + } + + return response( + res, + "success", + result, + "Data berhasil ditambahkan", + SUCCESS.CREATED, + ); + } catch (err) { + return response( + res, + "fail", + { err: new InternalServerError(err.message) }, + "Terjadi kesalahan yang tidak terduga", + ERROR.INTERNAL_ERROR, + ); + } + } + + // READ ALL - GET /your-module-api/get + async getAllYourModule(req, res) { + try { + const result = await yourModuleService.getAllYourModule(); + + if (result.err) { + return response(res, "fail", result); + } + + return response( + res, + "success", + result, + "Data berhasil diambil", + SUCCESS.OK, + ); + } catch (err) { + return response( + res, + "fail", + { err: new InternalServerError(err.message) }, + "Terjadi kesalahan yang tidak terduga", + ERROR.INTERNAL_ERROR, + ); + } + } + + // READ ONE - GET /your-module-api/get/:id + async getYourModuleById(req, res) { + try { + const { id } = req.params; + + const result = await yourModuleService.getYourModuleById(id); + + if (result.err) { + return response(res, "fail", result); + } + + return response( + res, + "success", + result, + "Data berhasil diambil", + SUCCESS.OK, + ); + } catch (err) { + return response( + res, + "fail", + { err: new InternalServerError(err.message) }, + "Terjadi kesalahan yang tidak terduga", + ERROR.INTERNAL_ERROR, + ); + } + } + + // UPDATE - PATCH /your-module-api/update/:id + async updateYourModule(req, res) { + try { + const { id } = req.params; + const payload = req.body; + + // Validasi input + const validatePayload = isValidPayload(payload, updateYourModuleModel); + + if (validatePayload.err) { + return response( + res, + "fail", + validatePayload, + "Validasi gagal", + ERROR.EXPECTATION_FAILED, + ); + } + + // Panggil service + const result = await yourModuleService.updateYourModule( + id, + validatePayload.data, + ); + + if (result.err) { + return response(res, "fail", result); + } + + return response( + res, + "success", + result, + "Data berhasil diperbarui", + SUCCESS.OK, + ); + } catch (err) { + return response( + res, + "fail", + { err: new InternalServerError(err.message) }, + "Terjadi kesalahan yang tidak terduga", + ERROR.INTERNAL_ERROR, + ); + } + } + + // DELETE - DELETE /your-module-api/delete/:id + async deleteYourModule(req, res) { + try { + const { id } = req.params; + + const result = await yourModuleService.deleteYourModule(id); + + if (result.err) { + return response(res, "fail", result); + } + + return response( + res, + "success", + result, + "Data 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 YourModuleController(); +``` + +### Langkah 6: Buat Index (index.js) + +**File:** `index.js` + +```javascript +const yourModuleController = require("./controllers/yourModule.controller"); + +module.exports = { + yourModuleController, +}; +``` + +### Langkah 7: Buat Routes + +**File:** `routes/yourModule.routes.js` + +```javascript +const express = require("express"); +const { yourModuleController } = require("../modules/yourModule"); +const { verifyJWT } = require("../middleware/verifyJWT"); + +const router = express.Router(); + +// Route publik +router.get("/get", yourModuleController.getAllYourModule); +router.get("/get/:id", yourModuleController.getYourModuleById); + +// Route yang dilindungi (butuh JWT) +router.post("/add", verifyJWT, yourModuleController.addYourModule); +router.patch("/update/:id", verifyJWT, yourModuleController.updateYourModule); +router.delete("/delete/:id", verifyJWT, yourModuleController.deleteYourModule); + +module.exports = router; +``` + +### Langkah 8: Daftarkan Routes di index.js + +**File:** `src/index.js` + +```javascript +const yourModuleRoutes = require("./routes/yourModule.routes"); + +// ... kode lainnya + +app.use("/your-module-api", yourModuleRoutes); +``` + +--- + +### Using Wrapper Functions + +### Response Wrapper + +Fungsi `response` menstandarisasi semua response API. + +**Lokasi:** `helpers/utils/wrapper.js` + +**Penggunaan di Controller:** + +```javascript +const { response } = require("../../../helpers/utils/wrapper"); +const { SUCCESS, ERROR } = require("../../../helpers/http-status/status_code"); + +// Response sukses +return response( + res, // Object response Express + "success", // Status: 'success' atau 'fail' + result, // Object data dari service + "Pesan sukses custom", // Pesan opsional + SUCCESS.OK, // HTTP status code (200) +); + +// Response error +return response( + res, + "fail", + result, // Berisi { err: ErrorObject } + "Pesan error custom", // Pesan opsional + ERROR.NOT_FOUND, // HTTP status code (404) +); +``` + +### Data/Error Wrapper (Layer Service) + +Fungsi `data` dan `error` membungkus hasil service. + +**Lokasi:** `helpers/utils/wrapper.js` + +**Penggunaan di Service:** + +```javascript +const { data, error } = require('../../../helpers/utils/wrapper'); +const { NotFoundError, BadRequestError } = require('../../../helpers/error'); + +// Kembalikan data sukses +async getSomething() { + try { + const result = await repository.find(); + return data(result); // Return: { err: null, data: result } + } catch (err) { + return error(err); // Return: { err: err, data: null } + } +} + +// Kembalikan custom error +async checkSomething() { + try { + const item = await repository.findById(id); + + if (!item) { + return error(new NotFoundError('Data Tidak Ditemukan')); + } + + return data(item); + } catch (err) { + return error(err); + } +} +``` + +### Class Error yang Tersedia + +**Lokasi:** `helpers/error/` + +```javascript +const { + BadRequestError, // 400 - Input tidak valid + UnauthorizedError, // 401 - Tidak terautentikasi + ForbiddenError, // 403 - Tidak ada izin + NotFoundError, // 404 - Resource tidak ditemukan + ConflictError, // 409 - Data duplikat + ExpectationFailedError, // 417 - Validasi gagal + InternalServerError, // 500 - Server error + ServiceUnavailableError, // 503 - Service mati + GatewayTimeoutError, // 504 - Timeout +} = require("../../../helpers/error"); + +// Contoh penggunaan +return error(new BadRequestError("Email Sudah Terdaftar")); +return error(new NotFoundError("Mahasiswa Tidak Ditemukan")); +return error(new ForbiddenError("Akses Ditolak")); +``` + +### HTTP Status Code + +**Lokasi:** `helpers/http-status/status_code.js` + +```javascript +const { SUCCESS, ERROR } = require("../../../helpers/http-status/status_code"); + +// Success codes +SUCCESS.OK; // 200 +SUCCESS.CREATED; // 201 + +// Error codes +ERROR.BAD_REQUEST; // 400 +ERROR.UNAUTHORIZED; // 401 +ERROR.FORBIDDEN; // 403 +ERROR.NOT_FOUND; // 404 +ERROR.CONFLICT; // 409 +ERROR.EXPECTATION_FAILED; // 417 +ERROR.INTERNAL_ERROR; // 500 +ERROR.SERVICE_UNAVAILABLE; // 503 +ERROR.GATEWAY_TIMEOUT; // 504 +``` + +--- + +## Contoh Lengkap: Tambah Record Baru + +### 1. Client mengirim request + +```javascript +POST /your-module-api/add +Headers: { Authorization: "Bearer JWT_TOKEN" } +Body: { "name": "John", "email": "john@example.com", "age": 25 } +``` + +### 2. Route menerima dan memanggil controller + +```javascript +router.post("/add", verifyJWT, yourModuleController.addYourModule); +``` + +### 3. Controller validasi input + +```javascript +const validatePayload = isValidPayload(payload, createYourModuleModel); + +if (validatePayload.err) { + // Return validation error dengan status 417 + return response(res, "fail", validatePayload, "Validasi gagal", 417); +} +``` + +### 4. Controller memanggil service + +```javascript +const result = await yourModuleService.createYourModule(validatePayload.data); +``` + +### 5. Service memproses business logic + +```javascript +// Cek duplikat +const existing = await repository.findByEmail(data.email); +if (existing) { + return error(new BadRequestError("Email Sudah Terdaftar")); +} + +// Buat record +const result = await repository.create(data); +return data(result); +``` + +### 6. Repository query database + +```javascript +return prisma.yourModel.create({ data: data }); +``` + +### 7. Response mengalir kembali + +**Sukses:** + +```javascript +// Service return: { err: null, data: createdRecord } +// Controller cek: if (result.err) โ†’ false, lanjut +// Controller return response dengan status 201 + +Response: { + "message": "Data berhasil ditambahkan", + "data": { "id": 1, "name": "John", "email": "john@example.com" } +} +``` + +**Error:** + +```javascript +// Service return: { err: BadRequestError, data: null } +// Controller cek: if (result.err) โ†’ true +// Controller return error response + +Response: { + "message": "Email Sudah Terdaftar", + "error": "BadRequestError" +} +``` + +--- + +## Contoh Validasi + +### Validasi Input dengan Joi + +```javascript +const validatePayload = isValidPayload(payload, createYourModuleModel); + +// Sukses +validatePayload = { + err: null, + data: { name: "John", email: "john@example.com", age: 25 }, +}; + +// Validasi gagal +validatePayload = { + err: { + details: [ + { message: "Email wajib diisi" }, + { message: "Umur minimal 18 tahun" }, + ], + }, + data: null, +}; +``` + +### Response untuk Validation Error + +```javascript +if (validatePayload.err) { + return response( + res, + 'fail', + validatePayload, + 'Validasi gagal', + ERROR.EXPECTATION_FAILED // 417 + ); +} + +// Client menerima: +{ + "message": "Validasi gagal", + "errors": [ + "Email wajib diisi", + "Umur minimal 18 tahun" + ] +} +``` + +--- + +## Contoh Upload File + +### Controller dengan Upload File + +```javascript +const multer = require('../../../middleware/multer'); +const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper'); + +// Route dengan multer +router.post('/add', verifyJWT, multer.single('image'), controller.add); + +// Controller +async add(req, res) { + try { + const payload = req.body; + const imageFile = req.file; // Ambil file yang diupload + + // Validasi + const validatePayload = isValidPayload(payload, createModel); + + if (validatePayload.err) { + // Hapus file yang diupload jika validasi gagal + if (imageFile?.filename) { + await deleteFileIfExists(imageFile.filename); + } + return response(res, 'fail', validatePayload, 'Validasi gagal', 417); + } + + // Panggil service dengan file + const result = await service.create(validatePayload.data, imageFile); + + if (result.err) { + // Hapus file yang diupload jika service gagal + if (imageFile?.filename) { + await deleteFileIfExists(imageFile.filename); + } + return response(res, 'fail', result); + } + + return response(res, 'success', result, 'Data berhasil ditambahkan', 201); + } catch (err) { + // Hapus file yang diupload saat terjadi error tidak terduga + if (req.file?.filename) { + await deleteFileIfExists(req.file.filename); + } + return response(res, 'fail', { err: new InternalServerError(err.message) }); + } +} +``` + +--- + +## ๐Ÿงช Testing + +### 1. Test dengan Swagger UI + +Buka Swagger documentation di browser: + +``` +http://localhost:9000/api-docs +``` + +Swagger UI menyediakan: + +- Interactive API testing +- Request/response examples +- Schema definitions +- Authentication testing + +### 2. Test dengan curl + +**Contoh Testing Endpoints:** + +```bash +# Create +curl -X POST http://localhost:9000/your-module-api/add \ + -H "Authorization: Bearer YOUR_JWT_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"name":"John","email":"john@example.com","age":25}' + +# Get all +curl http://localhost:9000/your-module-api/get + +# Get by ID +curl http://localhost:9000/your-module-api/get/1 + +# Update +curl -X PATCH http://localhost:9000/your-module-api/update/1 \ + -H "Authorization: Bearer YOUR_JWT_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"name":"John Updated"}' + +# Delete +curl -X DELETE http://localhost:9000/your-module-api/delete/1 \ + -H "Authorization: Bearer YOUR_JWT_TOKEN" +``` + +--- + +## ๐Ÿ“ API Documentation + +### Database Schema + +![Entity Relationship Diagram](https://raw.githubusercontent.com/Ditrogen/backend-intern-humic/main/src/docs/ERD.png) + +Database terdiri dari 7 tabel utama: + +1. **mahasiswa**: Data lengkap mahasiswa yang mendaftar magang +2. **admin**: Data admin untuk login dan mengelola lowongan +3. **lowongan_magang**: Informasi detail posisi magang yang tersedia +4. **lamaran_magang**: Data lamaran yang diajukan mahasiswa +5. **hasil_research**: Hasil riset proyek dari mahasiswa atau tim +6. **partnership**: Data partner/institusi yang bekerja sama +7. **faq**: Frequently Asked Questions + +### API Modules + +## Project Architecture + +### Technology Stack + +- **Runtime**: Node.js +- **Framework**: Express.js 4.x +- **ORM**: Prisma 6.16.2 +- **Database**: MySQL +- **Authentication**: JWT (RS256 with signature) +- **Validation**: Joi +- **File Upload**: Multer +- **Email Service**: Nodemailer +- **Documentation**: Swagger/OpenAPI +- **Deployment**: Vercel (Serverless) + +### Project Structure + +``` +src/ +โ”œโ”€โ”€ index.js # Main application entry point +โ”œโ”€โ”€ config/ # Configuration files +โ”œโ”€โ”€ docs/ # Documentation and ERD +โ”œโ”€โ”€ helpers/ # Helper utilities +โ”‚ โ”œโ”€โ”€ db/ # Database connection (Prisma) +โ”‚ โ”œโ”€โ”€ error/ # Custom error classes +โ”‚ โ”œโ”€โ”€ http-status/ # HTTP status codes +โ”‚ โ”œโ”€โ”€ infra/ # Infrastructure config +โ”‚ โ””โ”€โ”€ utils/ # Utility functions (validator, wrapper, file helper) +โ”œโ”€โ”€ middleware/ # Express middleware +โ”‚ โ”œโ”€โ”€ multer.js # File upload configuration +โ”‚ โ”œโ”€โ”€ recaptcha.js # ReCaptcha verification +โ”‚ โ””โ”€โ”€ verifyJWT.js # JWT authentication +โ”œโ”€โ”€ modules/ # Feature modules (MVC pattern) +โ”‚ โ”œโ”€โ”€ admin/ +โ”‚ โ”œโ”€โ”€ auth/ +โ”‚ โ”œโ”€โ”€ hasilResearch/ +โ”‚ โ”œโ”€โ”€ lamaranMagang/ +โ”‚ โ”œโ”€โ”€ lowonganMagang/ +โ”‚ โ”œโ”€โ”€ mahasiswa/ +โ”‚ โ””โ”€โ”€ partnership/ +โ”œโ”€โ”€ routes/ # API route definitions +โ””โ”€โ”€ uploads/ # File upload directory (local) +``` + +### Module Architecture (MVC Pattern) + +Each module follows this structure: + +``` +module/ +โ”œโ”€โ”€ index.js # Module exports +โ”œโ”€โ”€ controllers/ # Handle HTTP requests/responses +โ”œโ”€โ”€ services/ # Business logic layer +โ”œโ”€โ”€ repositories/ # Database access layer (Prisma) +โ”œโ”€โ”€ models/ # Joi validation schemas +โ””โ”€โ”€ helpers/ (optional) # Module-specific helpers +``` + +**Data Flow:** + +``` +Route โ†’ Controller โ†’ Service โ†’ Repository โ†’ Database + โ†“ +Route โ† Controller โ† Service โ† Repository +``` + +--- + +## Key Features + +### 1. **JWT Authentication (RS256)** + +- Uses RSA signature-based tokens +- Token expiry: 1 day +- Signature field for additional security +- Middleware: `verifyJWT` + +### 2. **Joi Validation** + +- All API inputs validated with Joi schemas +- Indonesian error messages +- Located in `models/` directory of each module + +### 3. **File Upload Management** + +- **Development**: Files stored in `src/uploads/` +- **Production**: Files stored in `/tmp/uploads/` (Vercel) +- Automatic file cleanup on validation/service errors +- Supports: Images (JPG, PNG), PDFs +- Max file size: 4.5MB + +### 4. **Automatic Status Updates** + +- `lowonganMagang` status automatically updates based on dates: + - `akan-datang`: Before start date + - `aktif`: Between start and end date + - `selesai`: After end date + +### 5. **CORS Configuration** + +- Open to all origins (configurable) +- Supports credentials +- Pre-flight request handling + +### 6. **Error Handling** + +- Centralized error classes +- Indonesian error messages +- Consistent error response format + +--- + +## API Modules + +### 1. **Admin Module** + +Manage admin accounts and authentication. + +**Endpoints:** + +- `POST /admin-api/add` - Create admin (requires JWT) +- `GET /admin-api/get` - Get all admins (requires JWT) + +### 2. **Auth Module** + +Authentication and authorization. + +**Endpoints:** + +- `POST /auth-api/login` - Admin login +- `GET /auth-api/me` - Get authenticated admin details (requires JWT) + +### 3. **Lowongan Magang Module** + +Manage internship vacancies. + +**Endpoints:** + +- `POST /lowongan-magang-api/add` - Create vacancy (requires JWT) +- `GET /lowongan-magang-api/get` - Get all vacancies +- `GET /lowongan-magang-api/get/id/:id` - Get vacancy by ID +- `GET /lowongan-magang-api/get/kelompok/:kelompok_peminatan` - Get by specialization +- `GET /lowongan-magang-api/get/kelompok-all` - Get all specializations +- `PATCH /lowongan-magang-api/update/:id` - Update vacancy (requires JWT) +- `DELETE /lowongan-magang-api/delete/:id` - Delete vacancy (requires JWT) + +**Features:** + +- Automatic status calculation +- Image upload support +- Filter by specialization group + +### 4. **Lamaran Magang Module** + +Manage internship applications. + +**Endpoints:** + +- `POST /lamaran-magang-api/add/:id_lowongan_magang` - Submit application (with ReCaptcha) +- `POST /lamaran-magang-api/add-mobile/:id_lowongan_magang` - Submit from mobile (no ReCaptcha) +- `GET /lamaran-magang-api/get` - Get all applications (requires JWT) +- `GET /lamaran-magang-api/get/:id_lamaran_magang` - Get application by ID (requires JWT) +- `PATCH /lamaran-magang-api/update/:id_lamaran_magang` - Update status (requires JWT) +- `GET /lamaran-magang-api/export` - Export to Excel (requires JWT) +- `DELETE /lamaran-magang-api/delete/:id` - Delete application (requires JWT) + +**Features:** + +- CV and portfolio upload (multiple files) +- Email confirmation on submission +- Excel export functionality +- Automatic file cleanup on errors + +### 5. **Mahasiswa Module** + +Manage student data. + +**Endpoints:** + +- `GET /mahasiswa-api/get` - Get all students (requires JWT) + +**Note:** Students are automatically created when submitting applications. + +### 6. **Partnership Module** + +Manage partner organizations. + +**Endpoints:** + +- `POST /partnership-api/add` - Add partner (requires JWT) +- `GET /partnership-api/get` - Get all partners +- `GET /partnership-api/get/:id` - Get partner by ID +- `PATCH /partnership-api/update/:id` - Update partner (requires JWT) +- `DELETE /partnership-api/delete/:id` - Delete partner (requires JWT) + +**Features:** + +- Logo image upload +- Automatic file cleanup on errors + +### 7. **Hasil Research Module** + +Manage research results/publications. + +**Endpoints:** + +- `POST /hasil-research-api/add` - Add research (requires JWT) +- `GET /hasil-research-api/get` - Get all research +- `GET /hasil-research-api/get/:id` - Get research by ID +- `PATCH /hasil-research-api/update/:id` - Update research (requires JWT) +- `DELETE /hasil-research-api/delete/:id` - Delete research (requires JWT) + +**Features:** + +- Image upload support +- Automatic file cleanup on errors + +### 8. **FAQ Module** + +Manage Frequently Asked Questions. + +**Endpoints:** + +- `POST /faq-api/add` - Add FAQ (requires JWT - Admin only) +- `GET /faq-api/get` - Get all FAQs (Public) +- `GET /faq-api/get/:id` - Get FAQ by ID (Public) +- `PATCH /faq-api/update/:id` - Update FAQ (requires JWT - Admin only) +- `DELETE /faq-api/delete/:id` - Delete FAQ (requires JWT - Admin only) + +--- + +### Environment Variables Reference + +Berikut penjelasan lengkap setiap environment variable: + +| Variable | Type | Required | Description | Example | +| ---------------------- | ------ | -------- | --------------------------------- | -------------------------------- | +| `DATABASE_URL` | string | Yes | MySQL connection string | `mysql://user:pass@host:3306/db` | +| `PORT` | number | Yes | Port server akan berjalan | `9000` | +| `AUTH_EMAIL` | string | Yes | Email untuk mengirim notifikasi | `admin@example.com` | +| `AUTH_PASSWORD` | string | Yes | App-specific password Gmail | `xxxx xxxx xxxx xxxx` | +| `JWT_PRIVATE_KEY` | string | Yes | RSA private key untuk signing JWT | See .env.example | +| `JWT_PUBLIC_KEY` | string | Yes | RSA public key untuk verify JWT | See .env.example | +| `RECAPTCHA_SECRET_KEY` | string | Yes | Google ReCaptcha secret key | `6Lxxxxxxx...` | +| `RECAPTCHA_SITE_KEY` | string | Yes | Google ReCaptcha site key | `6Lxxxxxxx...` | + +> **๐Ÿ“ Note:** +> +> - Untuk `AUTH_PASSWORD`, gunakan [App Password](https://support.google.com/accounts/answer/185833) dari Google +> - Untuk generate JWT keys, gunakan OpenSSL (lihat Quick Start section) +> - Untuk ReCaptcha keys, daftar di [Google ReCaptcha](https://www.google.com/recaptcha/admin) + +--- + +### Database Migration Commands + +Berikut command untuk mengelola database migrations: + +### Initial Setup + +```bash +npx prisma generate +npx prisma migrate dev --name init +``` + +### Reset Database + +```bash +npx prisma migrate reset +``` + +### Deploy to Production + +```bash +npx prisma migrate deploy +``` + +--- + +## ๐Ÿš€ Deployment + +### Deployment ke Vercel + +### 1. **Vercel Configuration** + +Create `vercel.json`: + +```json +{ + "version": 2, + "builds": [ + { + "src": "src/index.js", + "use": "@vercel/node" + } + ], + "routes": [ + { + "src": "/(.*)", + "dest": "src/index.js" + } + ] +} +``` + +### 2. **Environment Variables** + +Set all `.env` variables in Vercel dashboard. + +### 3. **Database Connection** + +Ensure `DATABASE_URL` points to your production MySQL instance. + +### 4. **Build Command** + +```bash +npm run vercel-build +``` + +### 5. **Important Notes** + +- Files uploaded to `/tmp/uploads/` (temporary) +- Serverless functions have 10-second timeout +- Cold start may affect first request + +--- + +## CI/CD Pipeline (GitHub Actions) + +Automatic migration on push to main branch. + +**Workflow:** + +1. Push to main branch +2. GitHub Actions triggers +3. Runs `prisma migrate deploy` +4. Database updated automatically + +--- + +## ๐Ÿ‘ฅ Contributing + +### Development Workflow + +### 1. **Adding a New Feature** + +```bash +# 1. Create feature branch +git checkout -b feature/new-feature + +# 2. Create module structure +modules/ + newModule/ + โ”œโ”€โ”€ index.js + โ”œโ”€โ”€ controllers/ + โ”œโ”€โ”€ services/ + โ”œโ”€โ”€ repositories/ + โ”œโ”€โ”€ models/ + +# 3. Update Prisma schema (if needed) +# Edit prisma/schema.prisma + +# 4. Run migration +npx prisma migrate dev --name add_new_feature + +# 5. Create route file +routes/newModule.routes.js + +# 6. Register route in src/index.js +app.use("/new-module-api", newModuleRoutes); + +# 7. Test locally +npm run dev + +# 8. Commit and push +git add . +git commit -m "feat: add new feature" +git push origin feature/new-feature +``` + +### 2. **Testing API Endpoints** + +Use Swagger UI: `http://localhost:9000/api-docs` + +Or use tools like: + +- Postman +- Thunder Client +- curl + +### 3. **File Upload Testing** + +```bash +# Test with curl +curl -X POST http://localhost:9000/lowongan-magang-api/add \ + -H "Authorization: Bearer YOUR_JWT_TOKEN" \ + -F "image=@/path/to/image.jpg" \ + -F "posisi=Backend Developer" \ + -F "kelompok_peminatan=Software Engineering" \ + -F "paid=paid" +``` + +--- + +--- + +## ๐Ÿ”ง Troubleshooting + +### Common Issues & Solutions + +### 1. **CORS Error** + +**Problem:** Frontend can't access API +**Solution:** Check CORS configuration in `src/index.js` + +### 2. **File Upload Fails** + +**Problem:** "File tidak ditemukan" +**Solution:** Check `fileHelper.js` upload directory configuration + +### 3. **JWT Token Invalid** + +**Problem:** "Invalid or expired token" +**Solution:** + +- Check token expiry (1 day) +- Verify JWT_PUBLIC_KEY in .env +- Ensure signature field matches + +### 4. **Prisma Client Error** + +**Problem:** "PrismaClient is unable to run in this browser environment" +**Solution:** + +```bash +npx prisma generate +``` + +### 5. **Express 5.x Route Error** + +**Problem:** "Missing parameter name" +**Solution:** Downgrade to Express 4.x (already fixed in package.json) + +--- + +--- + +### API Response Format + +### Success Response + +```json +{ + "message": "Success message", + "data": { ... } +} +``` + +### Error Response + +```json +{ + "message": "Error message", + "error": "Detailed error description" +} +``` + +### Validation Error + +```json +{ + "message": "Validation failed", + "errors": ["Field 'email' wajib diisi", "Field 'password' minimal 8 karakter"] +} +``` + +--- + +--- + +### Security Best Practices + +1. **Never commit `.env` file** +2. **Use strong JWT secrets** +3. **Validate all inputs with Joi** +4. **Sanitize file uploads** +5. **Limit file upload size (4.5MB)** +6. **Use HTTPS in production** +7. **Enable ReCaptcha for public endpoints** +8. **Implement rate limiting (recommended)** + +--- + +--- + +### Performance Optimization + +1. **File Cleanup**: Automatically removes orphaned files +2. **Index Optimization**: Database indexes on frequently queried fields +3. **Connection Pooling**: Prisma handles connection pooling +4. **Status Caching**: Status calculations cached in helpers + +--- + +--- + +### Contributing Guidelines + +### Code Style + +- Use CommonJS (`require`/`module.exports`) +- Follow existing module structure +- Use Indonesian for error messages +- Add JSDoc comments for functions +- Use meaningful variable names + +### Commit Messages + +Follow conventional commits: + +``` +feat: add new feature +fix: fix bug +docs: update documentation +refactor: refactor code +test: add tests +chore: update dependencies +``` + +### Pull Request Process + +1. Create feature branch +2. Make changes +3. Test locally +4. Update documentation +5. Create pull request +6. Wait for review +7. Merge after approval + +--- + +### Changelog + +### Version 2.0.0 (Current) + +- โœ… Prisma ORM migration +- โœ… JWT RS256 authentication +- โœ… Joi validation with Indonesian messages +- โœ… File upload with automatic cleanup +- โœ… Vercel deployment configuration +- โœ… Swagger documentation +- โœ… CI/CD with GitHub Actions +- โœ… CORS configuration +- โœ… Express 4.x compatibility + +--- + +## ๐Ÿ“š Quick Reference + +### Useful Commands + +```bash +# Development +npm run dev # Start development server +npm start # Start production server + +# Database +npx prisma generate # Generate Prisma client +npx prisma migrate dev # Create and apply migration +npx prisma migrate deploy # Deploy migrations to production +npx prisma migrate reset # Reset database +npx prisma studio # Open Prisma Studio GUI + +# Testing +curl http://localhost:9000/api-docs # Open API documentation + +# Git +git checkout -b feature/name # Create new feature branch +git add . # Stage changes +git commit -m "message" # Commit changes +git push origin branch-name # Push to remote +``` + +### Important URLs + +| Service | URL | Purpose | +| ----------------- | -------------------------------- | ------------------- | +| **Local API** | `http://localhost:9000` | Development server | +| **Swagger Docs** | `http://localhost:9000/api-docs` | API documentation | +| **Prisma Studio** | `http://localhost:5555` | Database GUI | +| **Production** | Your Vercel URL | Production endpoint | + +### Project Links + +- ๐ŸŒ **Repository**: [GitHub - Ditrogen/backend-intern-humic](https://github.com/Ditrogen/backend-intern-humic) +- ๐Ÿ“– **Prisma Docs**: [https://www.prisma.io/docs](https://www.prisma.io/docs) +- ๐Ÿ“˜ **Express Docs**: [https://expressjs.com](https://expressjs.com) +- ๐Ÿ” **JWT Info**: [https://jwt.io](https://jwt.io) + +--- + +## ๐Ÿ“„ License + +This API is licensed under the [MIT License](https://opensource.org/licenses/MIT). + +--- + +## ๐Ÿ’ฌ Support + +Jika ada pertanyaan atau masalah: + +1. Buka issue di [GitHub Issues](https://github.com/Ditrogen/backend-intern-humic/issues) +2. Hubungi team development +3. Baca dokumentasi di README ini + +--- + +**Made with โค๏ธ by HUMIC Engineering Intern Team** diff --git a/docs/internify-backend.md b/docs/internify-backend.md new file mode 100755 index 0000000..258f7fc --- /dev/null +++ b/docs/internify-backend.md @@ -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. diff --git a/oldCode/controllers/admin.controller.js b/oldCode/controllers/admin.controller.js new file mode 100755 index 0000000..216876b --- /dev/null +++ b/oldCode/controllers/admin.controller.js @@ -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' }); + } +}; diff --git a/oldCode/controllers/auth.controller.js b/oldCode/controllers/auth.controller.js new file mode 100755 index 0000000..e30febf --- /dev/null +++ b/oldCode/controllers/auth.controller.js @@ -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, +} \ No newline at end of file diff --git a/oldCode/controllers/hasilResearch.controller.js b/oldCode/controllers/hasilResearch.controller.js new file mode 100755 index 0000000..86ff71e --- /dev/null +++ b/oldCode/controllers/hasilResearch.controller.js @@ -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 +} \ No newline at end of file diff --git a/oldCode/controllers/lamaranMagang.controller.js b/oldCode/controllers/lamaranMagang.controller.js new file mode 100755 index 0000000..eb6dfdb --- /dev/null +++ b/oldCode/controllers/lamaranMagang.controller.js @@ -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: ` +
+

Lamaran Anda Telah Kami Terima

+

Halo ${fullName},

+ +

Terima kasih telah mengirimkan lamaran untuk posisi ${dataPekerjaan.posisi} di Humic Engineering.

+ +

Kami telah menerima dokumen dan informasi yang Anda kirimkan, dan saat ini tim rekrutmen kami sedang meninjaunya dengan seksama.

+ +

Jika profil Anda sesuai dengan kebutuhan kami, tim kami akan menghubungi Anda untuk tahapan seleksi berikutnya. Proses ini dapat memakan waktu hingga beberapa minggu.

+ +

Kami sangat menghargai minat Anda untuk bergabung bersama tim kami.

+ +

Salam hangat,

+

Tim Rekrutmen Humic Engineering

+ +
+

Email ini dikirim secara otomatis. Jika Anda memiliki pertanyaan lebih lanjut, silakan hubungi kami melalui email resmi yang tersedia di website kami.

+
+ `, + }; + + 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 + ? ` +

Halo ${fullName},

+

Selamat! Lamaran Anda untuk posisi ${dataLamaran.posisi} di Humic Engineering telah diterima.

+

Tim kami sangat terkesan dengan profil dan kualifikasi Anda. Kami akan segera menghubungi Anda terkait tahapan selanjutnya.

+

Terima kasih telah melamar dan kami menantikan kerja sama yang luar biasa bersama Anda.

+ ` + : ` +

Halo ${fullName},

+

Terima kasih atas lamaran Anda untuk posisi ${dataLamaran.posisi} di Humic Engineering.

+

Setelah mempertimbangkan secara seksama, kami memutuskan untuk tidak melanjutkan proses lamaran Anda ke tahap berikutnya.

+

Jangan berkecil hatiโ€”kami sangat menghargai waktu dan usaha Anda. Kami mendorong Anda untuk terus mencoba dan semoga sukses dalam perjalanan karier Anda.

+ `; + + const mailOptions = { + from: process.env.AUTH_EMAIL, + to: dataLamaran.email, + subject, + headers: { + "X-Priority": "1", + "X-MSMail-Priority": "High", + Importance: "high", + }, + html: ` +
+ ${messageContent} +

Salam hangat,

+

Tim Rekrutmen Humic Engineering

+
+

Email ini dikirim secara otomatis. Untuk informasi lebih lanjut, silakan hubungi kami melalui kontak resmi.

+
+ `, + }; + + 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 +}; diff --git a/oldCode/controllers/lowonganMagang.controller.js b/oldCode/controllers/lowonganMagang.controller.js new file mode 100755 index 0000000..da65aa0 --- /dev/null +++ b/oldCode/controllers/lowonganMagang.controller.js @@ -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, +}; diff --git a/oldCode/controllers/mahasiswa.controller.js b/oldCode/controllers/mahasiswa.controller.js new file mode 100755 index 0000000..3d9a0f7 --- /dev/null +++ b/oldCode/controllers/mahasiswa.controller.js @@ -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, +}; diff --git a/oldCode/controllers/partnership.controller.js b/oldCode/controllers/partnership.controller.js new file mode 100755 index 0000000..f7385fa --- /dev/null +++ b/oldCode/controllers/partnership.controller.js @@ -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, +}; diff --git a/oldCode/models/admin.js b/oldCode/models/admin.js new file mode 100755 index 0000000..d01749e --- /dev/null +++ b/oldCode/models/admin.js @@ -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]); +} + + diff --git a/oldCode/models/hasilResearch.js b/oldCode/models/hasilResearch.js new file mode 100755 index 0000000..36560f9 --- /dev/null +++ b/oldCode/models/hasilResearch.js @@ -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]); +} \ No newline at end of file diff --git a/oldCode/models/lamaranMagang.js b/oldCode/models/lamaranMagang.js new file mode 100755 index 0000000..e0098c1 --- /dev/null +++ b/oldCode/models/lamaranMagang.js @@ -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'); +} diff --git a/oldCode/models/lowonganMagang.js b/oldCode/models/lowonganMagang.js new file mode 100755 index 0000000..2a7df4d --- /dev/null +++ b/oldCode/models/lowonganMagang.js @@ -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]); +} diff --git a/oldCode/models/mahasiswa.js b/oldCode/models/mahasiswa.js new file mode 100755 index 0000000..cf66b07 --- /dev/null +++ b/oldCode/models/mahasiswa.js @@ -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'); +} diff --git a/oldCode/models/partnership.js b/oldCode/models/partnership.js new file mode 100755 index 0000000..a97d930 --- /dev/null +++ b/oldCode/models/partnership.js @@ -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]); +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json old mode 100644 new mode 100755 index f33759f..eb989bd --- a/package-lock.json +++ b/package-lock.json @@ -1,56 +1,50 @@ { - "name": "internify-be", + "name": "be", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "internify-be", + "name": "be", "version": "1.0.0", + "hasInstallScript": true, "license": "ISC", "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" } }, "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.1.tgz", - "integrity": "sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.15", + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", "js-yaml": "^4.1.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/philsturgeon" } }, "node_modules/@apidevtools/openapi-schemas": { @@ -69,538 +63,141 @@ "license": "MIT" }, "node_modules/@apidevtools/swagger-parser": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-12.1.0.tgz", - "integrity": "sha512-e5mJoswsnAX0jG+J09xHFYQXb/bUc5S3pLpMxUuRUA2H8T2kni3yEoyz2R3Dltw5f4A6j6rPNMpWTK+iVDFlng==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", "license": "MIT", "dependencies": { - "@apidevtools/json-schema-ref-parser": "14.0.1", - "@apidevtools/openapi-schemas": "^2.1.0", + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", "@apidevtools/swagger-methods": "^3.0.2", - "ajv": "^8.17.1", - "ajv-draft-04": "^1.0.0", - "call-me-maybe": "^1.0.2" + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" }, "peerDependencies": { "openapi-types": ">=7" } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, + "node_modules/@fast-csv/format/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", "license": "MIT" }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, + "node_modules/@fast-csv/parse/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, "engines": { - "node": ">=14" + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.3.tgz", + "integrity": "sha512-QIvUMB5VZ8HMLZF9A2oWr3AFM430QC8oGd0L35y2jHpuW6bIIca6x/xL7zUf4J7L9WJ3qjz+iJII8ncaeMbpSg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" } }, "node_modules/@prisma/client": { - "version": "6.19.3", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.3.tgz", - "integrity": "sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==", + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.2.tgz", + "integrity": "sha512-E00PxBcalMfYO/TWnXobBVUai6eW/g5OsifWQsQDzJYm7yaY+IRLo7ZLsaefi0QkTpxfuhFcQ/w180i6kX3iJw==", "hasInstallScript": true, "license": "Apache-2.0", "engines": { @@ -620,66 +217,66 @@ } }, "node_modules/@prisma/config": { - "version": "6.19.3", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.3.tgz", - "integrity": "sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==", + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.16.2.tgz", + "integrity": "sha512-mKXSUrcqXj0LXWPmJsK2s3p9PN+aoAbyMx7m5E1v1FufofR1ZpPoIArjjzOIm+bJRLLvYftoNYLx1tbHgF9/yg==", "devOptional": true, "license": "Apache-2.0", "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", - "effect": "3.21.0", + "effect": "3.16.12", "empathic": "2.0.0" } }, "node_modules/@prisma/debug": { - "version": "6.19.3", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.3.tgz", - "integrity": "sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==", + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.16.2.tgz", + "integrity": "sha512-bo4/gA/HVV6u8YK2uY6glhNsJ7r+k/i5iQ9ny/3q5bt9ijCj7WMPUwfTKPvtEgLP+/r26Z686ly11hhcLiQ8zA==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines": { - "version": "6.19.3", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.3.tgz", - "integrity": "sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==", + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.16.2.tgz", + "integrity": "sha512-7yf3AjfPUgsg/l7JSu1iEhsmZZ/YE00yURPjTikqm2z4btM0bCl2coFtTGfeSOWbQMmq45Jab+53yGUIAT1sjA==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.19.3", - "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", - "@prisma/fetch-engine": "6.19.3", - "@prisma/get-platform": "6.19.3" + "@prisma/debug": "6.16.2", + "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "@prisma/fetch-engine": "6.16.2", + "@prisma/get-platform": "6.16.2" } }, "node_modules/@prisma/engines-version": { - "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", - "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", + "version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43.tgz", + "integrity": "sha512-ThvlDaKIVrnrv97ujNFDYiQbeMQpLa0O86HFA2mNoip4mtFqM7U5GSz2ie1i2xByZtvPztJlNRgPsXGeM/kqAA==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { - "version": "6.19.3", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.3.tgz", - "integrity": "sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==", + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.16.2.tgz", + "integrity": "sha512-wPnZ8DMRqpgzye758ZvfAMiNJRuYpz+rhgEBZi60ZqDIgOU2694oJxiuu3GKFeYeR/hXxso4/2oBC243t/whxQ==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.19.3", - "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", - "@prisma/get-platform": "6.19.3" + "@prisma/debug": "6.16.2", + "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "@prisma/get-platform": "6.16.2" } }, "node_modules/@prisma/get-platform": { - "version": "6.19.3", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.3.tgz", - "integrity": "sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==", + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.16.2.tgz", + "integrity": "sha512-U/P36Uke5wS7r1+omtAgJpEB94tlT4SdlgaeTc6HVTTT93pXj7zZ+B/cZnmnvjcNPfWddgoDx8RLjmQwqGDYyA==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.19.3" + "@prisma/debug": "6.16.2" } }, "node_modules/@scarf/scarf": { @@ -690,52 +287,15 @@ "license": "Apache-2.0" }, "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", - "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/bcryptjs": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", - "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", - "dev": true, + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", "license": "MIT" }, "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", "license": "MIT", "dependencies": { "@types/connect": "*", @@ -746,40 +306,26 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cors": { - "version": "2.8.19", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", - "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/express": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", - "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", - "dev": true, + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", + "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", "license": "MIT", "dependencies": { "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "^1" + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.8", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", - "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", - "dev": true, + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", "license": "MIT", "dependencies": { "@types/node": "*", @@ -789,10 +335,9 @@ } }, "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "license": "MIT" }, "node_modules/@types/json-schema": { @@ -801,122 +346,75 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "license": "MIT" }, - "node_modules/@types/jsonwebtoken": { - "version": "9.0.10", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", - "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/ms": "*", - "@types/node": "*" - } - }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, "license": "MIT" }, - "node_modules/@types/morgan": { - "version": "1.9.10", - "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", - "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/multer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.1.0.tgz", - "integrity": "sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, "node_modules/@types/node": { - "version": "20.19.41", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", - "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", - "dev": true, + "version": "22.15.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", + "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } }, "node_modules/@types/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", - "dev": true, + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" - } - }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "dev": true, + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", "license": "MIT", "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, - "node_modules/@types/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", - "dev": true, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/swagger-jsdoc": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz", + "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==", "license": "MIT" }, - "node_modules/@types/strip-json-comments": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", - "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", - "dev": true, - "license": "MIT" + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" }, "node_modules/accepts": { "version": "1.3.8", @@ -931,103 +429,31 @@ "node": ">= 0.6" } }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "dev": true, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "license": "MIT", "dependencies": { - "acorn": "^8.11.0" + "debug": "4" }, "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/adler-32": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", - "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-draft-04": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", - "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", - "license": "MIT", - "peerDependencies": { - "ajv": "^8.5.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } + "node": ">= 6.0.0" } }, "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=8" } }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -1043,12 +469,92 @@ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "license": "MIT" }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC" + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } }, "node_modules/argparse": { "version": "2.0.1", @@ -1062,45 +568,104 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, - "node_modules/basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, "license": "MIT", "dependencies": { - "safe-buffer": "5.1.2" + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 10.0.0" } }, - "node_modules/basic-auth/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } }, - "node_modules/bcryptjs": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", - "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", - "license": "BSD-3-Clause", - "bin": { - "bcrypt": "bin/bcrypt" + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" } }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -1109,45 +674,117 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/body-parser": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", - "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "license": "MIT", "dependencies": { - "bytes": "~3.1.2", + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.15.1", - "raw-body": "~2.5.3", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", "type-is": "~1.6.18", - "unpipe": "~1.0.0" + "unpipe": "1.0.0" }, "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -1156,6 +793,39 @@ "node": ">=8" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -1168,6 +838,23 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -1217,6 +904,36 @@ } } }, + "node_modules/c12/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/c12/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1252,33 +969,49 @@ "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", "license": "MIT" }, - "node_modules/cfb": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", - "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", - "license": "Apache-2.0", + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "license": "MIT/X11", "dependencies": { - "adler-32": "~1.3.0", - "crc-32": "~1.2.0" + "traverse": ">=0.3.0 <0.4" }, "engines": { - "node": ">=0.8" + "node": "*" } }, "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "devOptional": true, + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "license": "MIT", "dependencies": { - "readdirp": "^4.0.1" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" }, "engines": { - "node": ">= 14.16.0" + "node": ">= 8.10.0" }, "funding": { "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" } }, "node_modules/citty": { @@ -1291,35 +1024,27 @@ "consola": "^3.2.3" } }, - "node_modules/codepage": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", - "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "delayed-stream": "~1.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">= 0.8" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/commander": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", @@ -1329,32 +1054,60 @@ "node": ">= 6" } }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "engines": [ - "node >= 6.0" + "node >= 0.8" ], "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", - "readable-stream": "^3.0.2", + "readable-stream": "^2.2.2", "typedarray": "^0.0.6" } }, "node_modules/confbox": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", - "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", "devOptional": true, "license": "MIT" }, @@ -1368,6 +1121,12 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -1390,24 +1149,30 @@ } }, "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "license": "MIT", "dependencies": { "object-assign": "^4", @@ -1415,10 +1180,6 @@ }, "engines": { "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" } }, "node_modules/crc-32": { @@ -1433,34 +1194,54 @@ "node": ">=0.8" } }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", "license": "MIT", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" }, "engines": { - "node": ">= 8" + "node": ">= 10" } }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", "dependencies": { - "ms": "2.0.0" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/deepmerge-ts": { @@ -1474,12 +1255,36 @@ } }, "node_modules/defu": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", - "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", "devOptional": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1506,14 +1311,13 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/diff": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", - "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", - "dev": true, - "license": "BSD-3-Clause", + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", "engines": { - "node": ">=0.3.1" + "node": ">=8" } }, "node_modules/doctrine": { @@ -1554,23 +1358,15 @@ "node": ">= 0.4" } }, - "node_modules/dynamic-dedupe": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", - "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", - "dev": true, - "license": "MIT", + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", "dependencies": { - "xtend": "^4.0.0" + "readable-stream": "^2.0.2" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -1587,9 +1383,9 @@ "license": "MIT" }, "node_modules/effect": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/effect/-/effect-3.21.0.tgz", - "integrity": "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==", + "version": "3.16.12", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.16.12.tgz", + "integrity": "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -1598,10 +1394,9 @@ } }, "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/empathic": { @@ -1623,6 +1418,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1642,9 +1446,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -1653,46 +1457,19 @@ "node": ">= 0.4" } }, - "node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", - "dev": true, - "hasInstallScript": true, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" + "node": ">= 0.4" } }, "node_modules/escape-html": { @@ -1719,41 +1496,83 @@ "node": ">= 0.6" } }, - "node_modules/express": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", - "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "node_modules/exceljs": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", + "license": "MIT", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/exceljs/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/exceljs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "~1.20.5", - "content-disposition": "~0.5.4", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "~2.4.1", + "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", - "qs": "~6.15.1", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", - "statuses": "~2.0.1", + "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -1766,10 +1585,48 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-recaptcha": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express-recaptcha/-/express-recaptcha-5.1.0.tgz", + "integrity": "sha512-L9BSaHbwVzEAf38LNnFwEbk4qpX1LiD1z8viBGYBuxyBWWVKMgBrKDb1mNLq6uSxubJzZbyIz+0ldgTSXfxsvQ==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/exsolve": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", - "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", "devOptional": true, "license": "MIT" }, @@ -1796,33 +1653,23 @@ "node": ">=8.0.0" } }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "license": "MIT", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -1832,37 +1679,71 @@ } }, "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "license": "MIT", "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", - "on-finished": "~2.4.1", + "on-finished": "2.4.1", "parseurl": "~1.3.3", - "statuses": "~2.0.2", + "statuses": "2.0.1", "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">= 6" } }, "node_modules/forwarded": { @@ -1874,15 +1755,6 @@ "node": ">= 0.6" } }, - "node_modules/frac": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", - "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -1892,18 +1764,46 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -1914,6 +1814,35 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -1923,6 +1852,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1979,22 +1938,21 @@ } }, "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "license": "ISC", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2004,7 +1962,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -2025,6 +1982,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -2037,10 +2009,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -2049,53 +2042,84 @@ "node": ">= 0.4" } }, - "node_modules/helmet": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", - "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "license": "MIT", "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" }, "engines": { "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "engines": { + "node": ">= 6" } }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "license": "ISC" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -2121,7 +2145,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -2130,27 +2153,10 @@ "node": ">=8" } }, - "node_modules/is-core-module": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", - "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -2160,7 +2166,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2170,7 +2175,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -2183,58 +2187,55 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" }, "node_modules/jiti": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz", + "integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==", "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/joi": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.1.tgz", + "integrity": "sha512-IiQpRyypSnLisQf3PwuN2eIHAsAIGZIrLZkd4zdvIar2bDyhM91ubRjy8a3eYablXsh9BeI/c7dmPYHca5qtoA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.0.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -2243,19 +2244,13 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, "node_modules/jsonwebtoken": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", - "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", "license": "MIT", "dependencies": { - "jws": "^4.0.1", + "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", @@ -2271,33 +2266,103 @@ "npm": ">=6" } }, - "node_modules/jsonwebtoken/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } }, "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "^1.0.1", + "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", "license": "MIT", "dependencies": { - "jwa": "^2.0.1", + "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "license": "ISC" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "license": "MIT" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -2310,12 +2375,31 @@ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "license": "MIT" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "license": "MIT" + }, "node_modules/lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", "license": "MIT" }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", + "license": "MIT" + }, "node_modules/lodash.isnumber": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", @@ -2334,6 +2418,12 @@ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "license": "MIT" }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==", + "license": "MIT" + }, "node_modules/lodash.mergewith": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", @@ -2346,19 +2436,71 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "license": "MIT" }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/lru.min": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz", + "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } }, "node_modules/math-intrinsics": { "version": "1.1.0", @@ -2430,104 +2572,126 @@ } }, "node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "*" } }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "license": "BlueOak-1.0.0", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, "bin": { "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/morgan": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", - "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", - "license": "MIT", - "dependencies": { - "basic-auth": "~2.0.1", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-finished": "~2.3.0", - "on-headers": "~1.1.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/morgan/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" } }, "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/multer": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", - "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", - "busboy": "^1.6.0", - "concat-stream": "^2.0.0", - "type-is": "^1.6.18" + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" }, "engines": { - "node": ">= 10.16.0" + "node": ">= 6.0.0" + } + }, + "node_modules/mysql2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.0.tgz", + "integrity": "sha512-8eMhmG6gt/hRkU1G+8KlGOdQi2w+CgtNoD1ksXZq9gQfkfDsX4LHaBwTe1SY0Imx//t2iZA03DFnyYKPinxSRw==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "license": "MIT", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" } }, "node_modules/negotiator": { @@ -2539,6 +2703,32 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-fetch-native": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", @@ -2546,41 +2736,100 @@ "devOptional": true, "license": "MIT" }, + "node_modules/nodemailer": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.9.tgz", + "integrity": "sha512-9/Qm0qXIByEP8lEV2qOqcAW7bRpL8CR9jcTwk3NBnHJNmP9fIJ86g2fgmIXqHY+nj55ZEMwWqYAT2QTDpRUYiQ==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/nypm": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.6.tgz", - "integrity": "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", + "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", "devOptional": true, "license": "MIT", "dependencies": { - "citty": "^0.2.2", + "citty": "^0.1.6", + "consola": "^3.4.2", "pathe": "^2.0.3", - "tinyexec": "^1.1.1" + "pkg-types": "^2.3.0", + "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" }, "engines": { - "node": ">=18" + "node": "^14.16.0 || >=16.10.0" } }, - "node_modules/nypm/node_modules/citty": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", - "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", - "devOptional": true, - "license": "MIT" - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2621,20 +2870,10 @@ "node": ">= 0.8" } }, - "node_modules/on-headers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -2647,11 +2886,11 @@ "license": "MIT", "peer": true }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" }, "node_modules/parseurl": { "version": "1.3.3", @@ -2666,49 +2905,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, "node_modules/pathe": { @@ -2726,10 +2931,9 @@ "license": "MIT" }, "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -2739,28 +2943,27 @@ } }, "node_modules/pkg-types": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", - "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", "devOptional": true, "license": "MIT", "dependencies": { - "confbox": "^0.2.4", - "exsolve": "^1.0.8", + "confbox": "^0.2.2", + "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "node_modules/prisma": { - "version": "6.19.3", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.3.tgz", - "integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==", + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.2.tgz", + "integrity": "sha512-aRvldGE5UUJTtVmFiH3WfNFNiqFlAtePUxcI0UEGlnXCX7DqhiMT5TRYwncHFeA/Reca5W6ToXXyCMTeFPdSXA==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { - "@prisma/config": "6.19.3", - "@prisma/engines": "6.19.3" + "@prisma/config": "6.16.2", + "@prisma/engines": "6.16.2" }, "bin": { "prisma": "build/index.js" @@ -2777,6 +2980,12 @@ } } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2790,6 +2999,17 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "license": "MIT" + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -2808,9 +3028,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -2832,20 +3052,32 @@ } }, "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "license": "MIT", "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" }, "engines": { "node": ">= 0.8" } }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rc9": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", @@ -2858,75 +3090,79 @@ } }, "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "license": "MIT", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">= 6" + "node": ">=10" } }, "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", - "dev": true, + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" + "picomatch": "^2.2.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8.10.0" } }, "node_modules/rimraf": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", - "dev": true, + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "license": "ISC", "dependencies": { - "glob": "^10.3.7" + "glob": "^7.1.3" }, "bin": { - "rimraf": "dist/esm/bin.mjs" + "rimraf": "bin.js" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2958,10 +3194,22 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2971,77 +3219,91 @@ } }, "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", - "encodeurl": "~2.0.0", + "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", - "on-finished": "~2.4.1", + "on-finished": "2.4.1", "range-parser": "~1.2.1", - "statuses": "~2.0.2" + "statuses": "2.0.1" }, "engines": { "node": ">= 0.8.0" } }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "~0.19.1" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -3062,13 +3324,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" + "object-inspect": "^1.13.3" }, "engines": { "node": ">= 0.4" @@ -3115,54 +3377,36 @@ } }, "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", "license": "MIT", "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/ssf": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", - "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", - "license": "Apache-2.0", - "dependencies": { - "frac": "~1.1.2" + "semver": "^7.5.3" }, "engines": { - "node": ">=0.8" + "node": ">=10" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -3177,38 +3421,24 @@ } }, "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "license": "MIT", "dependencies": { - "safe-buffer": "~5.2.0" + "safe-buffer": "~5.1.0" } }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -3219,58 +3449,10 @@ "node": ">=8" } }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -3279,182 +3461,75 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, "engines": { "node": ">=4" } }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/swagger-jsdoc": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.3.0.tgz", - "integrity": "sha512-I+iQjVGV3t28pOkQUJv2MncthvOtkEactOn8R76SvSYhxgtIn7FoqfDHwQaN+GBnQdXQLrhgDXseKitmJcHMsA==", + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", "license": "MIT", "dependencies": { - "@apidevtools/swagger-parser": "^12.1.0", "commander": "6.2.0", "doctrine": "3.0.0", - "glob": "11.1.0", + "glob": "7.1.6", "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", "yaml": "2.0.0-1" }, "bin": { "swagger-jsdoc": "bin/swagger-jsdoc.js" }, "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/swagger-jsdoc/node_modules/@isaacs/cliui": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", - "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/swagger-jsdoc/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/swagger-jsdoc/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" + "node": ">=12.0.0" } }, "node_modules/swagger-jsdoc/node_modules/glob": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", - "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "BlueOak-1.0.0", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": "20 || >=22" + "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/swagger-jsdoc/node_modules/jackspeak": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", - "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", - "license": "BlueOak-1.0.0", + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "license": "MIT", "dependencies": { - "@isaacs/cliui": "^9.0.0" + "@apidevtools/swagger-parser": "10.0.3" }, "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/swagger-jsdoc/node_modules/lru-cache": { - "version": "11.5.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", - "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/swagger-jsdoc/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/swagger-jsdoc/node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=10" } }, "node_modules/swagger-ui-dist": { - "version": "5.32.6", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.6.tgz", - "integrity": "sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA==", + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.21.0.tgz", + "integrity": "sha512-E0K3AB6HvQd8yQNSMR7eE5bk+323AUxjtCz/4ZNKiahOlPhPJxqn3UPIGs00cyY/dhrTDJ61L7C/a8u6zhGrZg==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" @@ -3475,21 +3550,85 @@ "express": ">=4.0.0 || >=5.0.0-beta" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tinyexec": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.2.tgz", - "integrity": "sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", "devOptional": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", "license": "MIT", "engines": { - "node": ">=18" + "node": ">=14.14" } }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -3507,223 +3646,28 @@ "node": ">=0.6" } }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/ts-node-dev": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", - "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.1", - "dynamic-dedupe": "^0.3.0", - "minimist": "^1.2.6", - "mkdirp": "^1.0.4", - "resolve": "^1.0.0", - "rimraf": "^2.6.1", - "source-map-support": "^0.5.12", - "tree-kill": "^1.2.2", - "ts-node": "^10.4.0", - "tsconfig": "^7.0.0" - }, - "bin": { - "ts-node-dev": "lib/bin.js", - "tsnd": "lib/bin.js" - }, - "engines": { - "node": ">=0.8.0" - }, - "peerDependencies": { - "node-notifier": "*", - "typescript": "*" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/ts-node-dev/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/ts-node-dev/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/ts-node-dev/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", "engines": { "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/ts-node-dev/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ts-node-dev/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/ts-node-dev/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/tsconfig": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", - "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/strip-bom": "^3.0.0", - "@types/strip-json-comments": "0.0.30", - "strip-bom": "^3.0.0", - "strip-json-comments": "^2.0.0" - } - }, - "node_modules/tsx": { - "version": "4.22.3", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", - "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.28.0" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" } }, "node_modules/type-is": { @@ -3745,26 +3689,16 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "license": "MIT" }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -3776,6 +3710,24 @@ "node": ">= 0.8" } }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -3791,12 +3743,27 @@ "node": ">= 0.4.0" } }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/validator": { + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } }, "node_modules/vary": { "version": "1.1.2", @@ -3807,175 +3774,58 @@ "node": ">= 0.8" } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", "license": "ISC", "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wmf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", - "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/word": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", - "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" + "string-width": "^1.0.2 || 2 || 3 || 4" } }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, - "node_modules/xlsx": { - "version": "0.18.5", - "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", - "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", - "license": "Apache-2.0", - "dependencies": { - "adler-32": "~1.3.0", - "cfb": "~1.2.1", - "codepage": "~1.15.0", - "crc-32": "~1.2.1", - "ssf": "~0.11.2", - "wmf": "~1.0.1", - "word": "~0.3.0" - }, - "bin": { - "xlsx": "bin/xlsx.njs" - }, - "engines": { - "node": ">=0.8" - } + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4" } }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/yaml": { "version": "2.0.0-1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", @@ -3985,14 +3835,83 @@ "node": ">= 6" } }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, "engines": { - "node": ">=6" + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "license": "MIT", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } } } diff --git a/package.json b/package.json old mode 100644 new mode 100755 index a0d1041..26b097f --- a/package.json +++ b/package.json @@ -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" } -} \ No newline at end of file +} diff --git a/prisma.config.ts b/prisma.config.ts deleted file mode 100644 index ae97446..0000000 --- a/prisma.config.ts +++ /dev/null @@ -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', - }, -}); diff --git a/prisma/migrations/20260523212558_init/migration.sql b/prisma/migrations/20251116160821_init/migration.sql old mode 100644 new mode 100755 similarity index 51% rename from prisma/migrations/20260523212558_init/migration.sql rename to prisma/migrations/20251116160821_init/migration.sql index 8f1007a..6ef4c82 --- a/prisma/migrations/20260523212558_init/migration.sql +++ b/prisma/migrations/20251116160821_init/migration.sql @@ -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; diff --git a/prisma/migrations/20251116170005_add_batch_index_to_lowongan/migration.sql b/prisma/migrations/20251116170005_add_batch_index_to_lowongan/migration.sql new file mode 100755 index 0000000..5af5b64 --- /dev/null +++ b/prisma/migrations/20251116170005_add_batch_index_to_lowongan/migration.sql @@ -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; diff --git a/prisma/migrations/20260112094610_add_relation_batch_and_lamaran/migration.sql b/prisma/migrations/20260112094610_add_relation_batch_and_lamaran/migration.sql new file mode 100755 index 0000000..bd10293 --- /dev/null +++ b/prisma/migrations/20260112094610_add_relation_batch_and_lamaran/migration.sql @@ -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; diff --git a/prisma/migrations/20260531095731_import_remaining_schemas/migration.sql b/prisma/migrations/20260531095731_import_remaining_schemas/migration.sql deleted file mode 100644 index 964209c..0000000 --- a/prisma/migrations/20260531095731_import_remaining_schemas/migration.sql +++ /dev/null @@ -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; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml old mode 100644 new mode 100755 diff --git a/prisma/prisma.config.js b/prisma/prisma.config.js new file mode 100755 index 0000000..250881e --- /dev/null +++ b/prisma/prisma.config.js @@ -0,0 +1,9 @@ +import { defineConfig } from '@prisma/client'; + +export default defineConfig({ + datasources: { + db: { + url: process.env.DATABASE_URL, + }, + }, +}); diff --git a/prisma/schema.prisma b/prisma/schema.prisma old mode 100644 new mode 100755 index a4178fd..1ee42f3 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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,147 +14,125 @@ 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") + id Int @id @default(autoincrement()) + 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 { - 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) +model HasilResearch { + id Int @id @default(autoincrement()) + 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) - status StatusLamaran - batch Int? + id Int @id @default(autoincrement()) + 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 - negara String? @db.VarChar(255) - 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") + id Int @id @default(autoincrement()) + batch Int + negara String? @db.VarChar(255) + posisi String? @db.VarChar(255) + tahun Int + jumlah Int + 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]) @@ -159,44 +142,54 @@ model RiwayatPelamar { } model Feedback { - id Int @id @default(autoincrement()) - nama String @db.VarChar(255) - universitas String @db.VarChar(255) - pesan String @db.Text + id Int @id @default(autoincrement()) + nama String @db.VarChar(255) + universitas String @db.VarChar(255) + pesan String @db.Text batch Int - posisi String @db.VarChar(255) + 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 { - 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") +model Batch { + id Int @id @default(autoincrement()) + 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 } diff --git a/prisma/seed.ts b/prisma/seed.ts deleted file mode 100644 index 5ec787f..0000000 --- a/prisma/seed.ts +++ /dev/null @@ -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(); - }); diff --git a/prisma/seed_dummy.ts b/prisma/seed_dummy.ts deleted file mode 100644 index 8812187..0000000 --- a/prisma/seed_dummy.ts +++ /dev/null @@ -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: '

Mendesain antarmuka aplikasi mobile dan web, melakukan user testing, dan wireframing.

', - 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: '

Mengembangkan antarmuka responsif menggunakan React dan Tailwind CSS.

', - 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: '

Membangun REST API, mengoptimalkan query database, dan integrasi backend services.

', - 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(); - }); diff --git a/src/app.ts b/src/app.ts deleted file mode 100644 index d19efaf..0000000 --- a/src/app.ts +++ /dev/null @@ -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; diff --git a/src/docs/ERD.png b/src/docs/ERD.png new file mode 100755 index 0000000..e235d71 Binary files /dev/null and b/src/docs/ERD.png differ diff --git a/src/docs/internify_db.sql b/src/docs/internify_db.sql new file mode 100755 index 0000000..2d90ae7 --- /dev/null +++ b/src/docs/internify_db.sql @@ -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; + + diff --git a/src/docs/swagger.js b/src/docs/swagger.js new file mode 100755 index 0000000..e57c04a --- /dev/null +++ b/src/docs/swagger.js @@ -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; diff --git a/src/docs/swagger.ts b/src/docs/swagger.ts deleted file mode 100644 index 47aa5d8..0000000 --- a/src/docs/swagger.ts +++ /dev/null @@ -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; diff --git a/src/helpers/db/db_connection.js b/src/helpers/db/db_connection.js new file mode 100755 index 0000000..19a70a4 --- /dev/null +++ b/src/helpers/db/db_connection.js @@ -0,0 +1,7 @@ +const { PrismaClient } = require('../../generated/prisma'); + +const prisma = new PrismaClient({ + log: ['query', 'info', 'warn', 'error'], +}); + +module.exports = prisma \ No newline at end of file diff --git a/src/helpers/db/prisma.js b/src/helpers/db/prisma.js new file mode 100755 index 0000000..bad897e --- /dev/null +++ b/src/helpers/db/prisma.js @@ -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 }; \ No newline at end of file diff --git a/src/helpers/error/bad_request_error.js b/src/helpers/error/bad_request_error.js new file mode 100755 index 0000000..a4302ae --- /dev/null +++ b/src/helpers/error/bad_request_error.js @@ -0,0 +1,9 @@ +const CommonError = require('./common_error'); + +class BadRequestError extends CommonError { + constructor(message) { + super(message || 'Bad Request'); + } +} + +module.exports = BadRequestError; diff --git a/src/helpers/error/common_error.js b/src/helpers/error/common_error.js new file mode 100755 index 0000000..79211e2 --- /dev/null +++ b/src/helpers/error/common_error.js @@ -0,0 +1,7 @@ +class CommonError extends Error { + constructor(message) { + super(message); + } +} + +module.exports = CommonError; diff --git a/src/helpers/error/conflict_error.js b/src/helpers/error/conflict_error.js new file mode 100755 index 0000000..90eddd0 --- /dev/null +++ b/src/helpers/error/conflict_error.js @@ -0,0 +1,9 @@ +const CommonError = require('./common_error'); + +class ConflictError extends CommonError { + constructor(message) { + super(message || 'Conflict'); + } +} + +module.exports = ConflictError; diff --git a/src/helpers/error/expectation_failed_error.js b/src/helpers/error/expectation_failed_error.js new file mode 100755 index 0000000..44d6f6d --- /dev/null +++ b/src/helpers/error/expectation_failed_error.js @@ -0,0 +1,9 @@ +const CommonError = require('./common_error'); + +class ExpectationFailedError extends CommonError { + constructor(message) { + super(message || 'Expectation Failed'); + } +} + +module.exports = ExpectationFailedError; diff --git a/src/helpers/error/forbidden_error.js b/src/helpers/error/forbidden_error.js new file mode 100755 index 0000000..af680a7 --- /dev/null +++ b/src/helpers/error/forbidden_error.js @@ -0,0 +1,9 @@ +const CommonError = require('./common_error'); + +class ForbiddenError extends CommonError { + constructor(message) { + super(message || 'Forbidden'); + } +} + +module.exports = ForbiddenError; diff --git a/src/helpers/error/gateway_timeout_error.js b/src/helpers/error/gateway_timeout_error.js new file mode 100755 index 0000000..c2884c9 --- /dev/null +++ b/src/helpers/error/gateway_timeout_error.js @@ -0,0 +1,9 @@ +const CommonError = require('./common_error'); + +class GatewayTimeoutError extends CommonError { + constructor(message) { + super(message || 'Gateway Timeout'); + } +} + +module.exports = GatewayTimeoutError; diff --git a/src/helpers/error/index.js b/src/helpers/error/index.js new file mode 100755 index 0000000..abf9c15 --- /dev/null +++ b/src/helpers/error/index.js @@ -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 +}; diff --git a/src/helpers/error/internal_server_error.js b/src/helpers/error/internal_server_error.js new file mode 100755 index 0000000..873e32f --- /dev/null +++ b/src/helpers/error/internal_server_error.js @@ -0,0 +1,9 @@ +const CommonError = require('./common_error'); + +class InternalServerError extends CommonError { + constructor(message) { + super(message || 'Internal Server Error'); + } +} + +module.exports = InternalServerError; diff --git a/src/helpers/error/not_found_error.js b/src/helpers/error/not_found_error.js new file mode 100755 index 0000000..13a5e0d --- /dev/null +++ b/src/helpers/error/not_found_error.js @@ -0,0 +1,9 @@ +const CommonError = require('./common_error'); + +class NotFoundError extends CommonError { + constructor(message) { + super(message || 'Not Found'); + } +} + +module.exports = NotFoundError; diff --git a/src/helpers/error/service_unavailable_error.js b/src/helpers/error/service_unavailable_error.js new file mode 100755 index 0000000..7f4dad5 --- /dev/null +++ b/src/helpers/error/service_unavailable_error.js @@ -0,0 +1,9 @@ +const CommonError = require('./common_error'); + +class ServiceUnavailableError extends CommonError { + constructor(message) { + super(message || 'Service Unavailable'); + } +} + +module.exports = ServiceUnavailableError; diff --git a/src/helpers/error/unauthorized_error.js b/src/helpers/error/unauthorized_error.js new file mode 100755 index 0000000..4db104b --- /dev/null +++ b/src/helpers/error/unauthorized_error.js @@ -0,0 +1,9 @@ +const CommonError = require('./common_error'); + +class Unauthorized extends CommonError { + constructor(message) { + super(message || 'Unauthorized'); + } +} + +module.exports = Unauthorized; diff --git a/src/helpers/error/validation_error.js b/src/helpers/error/validation_error.js new file mode 100755 index 0000000..26e2b43 --- /dev/null +++ b/src/helpers/error/validation_error.js @@ -0,0 +1,8 @@ +class ValidationError extends Error { + constructor(message, errors) { + super(message || 'Validation Error'); + this.errors = errors + } +} + +module.exports = ValidationError; \ No newline at end of file diff --git a/src/helpers/http-status/status_code.js b/src/helpers/http-status/status_code.js new file mode 100755 index 0000000..d1f7584 --- /dev/null +++ b/src/helpers/http-status/status_code.js @@ -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 +}; diff --git a/src/helpers/infra/global_config.js b/src/helpers/infra/global_config.js new file mode 100755 index 0000000..40006f7 --- /dev/null +++ b/src/helpers/infra/global_config.js @@ -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 }; diff --git a/src/helpers/utils/fileHelper.js b/src/helpers/utils/fileHelper.js new file mode 100755 index 0000000..eb6d94c --- /dev/null +++ b/src/helpers/utils/fileHelper.js @@ -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 +}; diff --git a/src/helpers/utils/imageUpload.helper.js b/src/helpers/utils/imageUpload.helper.js new file mode 100755 index 0000000..10f17e0 --- /dev/null +++ b/src/helpers/utils/imageUpload.helper.js @@ -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(); diff --git a/src/helpers/utils/mail.js b/src/helpers/utils/mail.js new file mode 100755 index 0000000..b2a1f9a --- /dev/null +++ b/src/helpers/utils/mail.js @@ -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; diff --git a/src/helpers/utils/validator.js b/src/helpers/utils/validator.js new file mode 100755 index 0000000..0b22543 --- /dev/null +++ b/src/helpers/utils/validator.js @@ -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, +}; diff --git a/src/helpers/utils/wrapper.js b/src/helpers/utils/wrapper.js new file mode 100755 index 0000000..861427f --- /dev/null +++ b/src/helpers/utils/wrapper.js @@ -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 +}; \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100755 index 0000000..510536b --- /dev/null +++ b/src/index.js @@ -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 ๐Ÿ˜ +*/ diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 91d9cef..0000000 --- a/src/index.ts +++ /dev/null @@ -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); - }); -}); diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts deleted file mode 100644 index 0270799..0000000 --- a/src/lib/prisma.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { PrismaClient } from '../generated/prisma/client'; - -const prisma = new PrismaClient(); - -export default prisma; diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts deleted file mode 100644 index d38fad4..0000000 --- a/src/middleware/auth.ts +++ /dev/null @@ -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(); - }; -}; diff --git a/src/middleware/errorHandler.ts b/src/middleware/errorHandler.ts deleted file mode 100644 index f55ae3f..0000000 --- a/src/middleware/errorHandler.ts +++ /dev/null @@ -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 }), - }); -}; diff --git a/src/middleware/isAdmin.js b/src/middleware/isAdmin.js new file mode 100755 index 0000000..10239e5 --- /dev/null +++ b/src/middleware/isAdmin.js @@ -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); +} \ No newline at end of file diff --git a/src/middleware/multer.js b/src/middleware/multer.js new file mode 100755 index 0000000..a48f29f --- /dev/null +++ b/src/middleware/multer.js @@ -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; diff --git a/src/middleware/recaptcha.js b/src/middleware/recaptcha.js new file mode 100755 index 0000000..b826019 --- /dev/null +++ b/src/middleware/recaptcha.js @@ -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; diff --git a/src/middleware/upload.ts b/src/middleware/upload.ts deleted file mode 100644 index d82a84d..0000000 --- a/src/middleware/upload.ts +++ /dev/null @@ -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 - } -}); diff --git a/src/middleware/verifyJWT.js b/src/middleware/verifyJWT.js new file mode 100755 index 0000000..c0218d1 --- /dev/null +++ b/src/middleware/verifyJWT.js @@ -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 }; diff --git a/src/modules/admin/controllers/admin.controller.js b/src/modules/admin/controllers/admin.controller.js new file mode 100755 index 0000000..fc776e4 --- /dev/null +++ b/src/modules/admin/controllers/admin.controller.js @@ -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(); diff --git a/src/modules/admin/index.js b/src/modules/admin/index.js new file mode 100755 index 0000000..61503d2 --- /dev/null +++ b/src/modules/admin/index.js @@ -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 +}; diff --git a/src/modules/admin/models/admin.model.js b/src/modules/admin/models/admin.model.js new file mode 100755 index 0000000..e48eb80 --- /dev/null +++ b/src/modules/admin/models/admin.model.js @@ -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, +}; diff --git a/src/modules/admin/repositories/admin.repository.js b/src/modules/admin/repositories/admin.repository.js new file mode 100755 index 0000000..4ddcd79 --- /dev/null +++ b/src/modules/admin/repositories/admin.repository.js @@ -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(); diff --git a/src/modules/admin/services/admin.service.js b/src/modules/admin/services/admin.service.js new file mode 100755 index 0000000..ea271b3 --- /dev/null +++ b/src/modules/admin/services/admin.service.js @@ -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(); diff --git a/src/modules/auth/controllers/auth.controller.js b/src/modules/auth/controllers/auth.controller.js new file mode 100755 index 0000000..f223bb2 --- /dev/null +++ b/src/modules/auth/controllers/auth.controller.js @@ -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(); diff --git a/src/modules/auth/index.js b/src/modules/auth/index.js new file mode 100755 index 0000000..bcbd946 --- /dev/null +++ b/src/modules/auth/index.js @@ -0,0 +1,7 @@ +const authController = require('./controllers/auth.controller'); +const authService = require('./services/auth.service'); + +module.exports = { + authController, + authService +}; diff --git a/src/modules/auth/models/auth.model.js b/src/modules/auth/models/auth.model.js new file mode 100755 index 0000000..18b0669 --- /dev/null +++ b/src/modules/auth/models/auth.model.js @@ -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, +}; diff --git a/src/modules/auth/services/auth.service.js b/src/modules/auth/services/auth.service.js new file mode 100755 index 0000000..4c7cf59 --- /dev/null +++ b/src/modules/auth/services/auth.service.js @@ -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(); diff --git a/src/modules/batch/controllers/batch.controller.js b/src/modules/batch/controllers/batch.controller.js new file mode 100755 index 0000000..1562ee3 --- /dev/null +++ b/src/modules/batch/controllers/batch.controller.js @@ -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(); diff --git a/src/modules/batch/index.js b/src/modules/batch/index.js new file mode 100755 index 0000000..b20fe60 --- /dev/null +++ b/src/modules/batch/index.js @@ -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, +}; diff --git a/src/modules/batch/models/batch.model.js b/src/modules/batch/models/batch.model.js new file mode 100755 index 0000000..69020ae --- /dev/null +++ b/src/modules/batch/models/batch.model.js @@ -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, +}; diff --git a/src/modules/batch/repositories/batch.repository.js b/src/modules/batch/repositories/batch.repository.js new file mode 100755 index 0000000..10629ee --- /dev/null +++ b/src/modules/batch/repositories/batch.repository.js @@ -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(); diff --git a/src/modules/batch/services/batch.service.js b/src/modules/batch/services/batch.service.js new file mode 100755 index 0000000..ed6d746 --- /dev/null +++ b/src/modules/batch/services/batch.service.js @@ -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(); diff --git a/src/modules/faq/controllers/faq.controllers.js b/src/modules/faq/controllers/faq.controllers.js new file mode 100755 index 0000000..7deafa8 --- /dev/null +++ b/src/modules/faq/controllers/faq.controllers.js @@ -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(); \ No newline at end of file diff --git a/src/modules/faq/index.js b/src/modules/faq/index.js new file mode 100755 index 0000000..6127d9a --- /dev/null +++ b/src/modules/faq/index.js @@ -0,0 +1,5 @@ +const faqController = require('./controllers/faq.controllers'); + +module.exports = { + faqController, +}; diff --git a/src/modules/faq/models/faq.models.js b/src/modules/faq/models/faq.models.js new file mode 100755 index 0000000..5ca4418 --- /dev/null +++ b/src/modules/faq/models/faq.models.js @@ -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, +}; diff --git a/src/modules/faq/repositories/faq.repositories.js b/src/modules/faq/repositories/faq.repositories.js new file mode 100755 index 0000000..d12b602 --- /dev/null +++ b/src/modules/faq/repositories/faq.repositories.js @@ -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(); diff --git a/src/modules/faq/services/faq.services.js b/src/modules/faq/services/faq.services.js new file mode 100755 index 0000000..c13052e --- /dev/null +++ b/src/modules/faq/services/faq.services.js @@ -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(); diff --git a/src/modules/feedback/controllers/feedback.controller.js b/src/modules/feedback/controllers/feedback.controller.js new file mode 100755 index 0000000..3e79141 --- /dev/null +++ b/src/modules/feedback/controllers/feedback.controller.js @@ -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(); diff --git a/src/modules/feedback/index.js b/src/modules/feedback/index.js new file mode 100755 index 0000000..7a772d7 --- /dev/null +++ b/src/modules/feedback/index.js @@ -0,0 +1,5 @@ +const feedbackController = require('./controllers/feedback.controller'); + +module.exports = { + feedbackController, +}; diff --git a/src/modules/feedback/models/feedback.model.js b/src/modules/feedback/models/feedback.model.js new file mode 100755 index 0000000..f2d8733 --- /dev/null +++ b/src/modules/feedback/models/feedback.model.js @@ -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, +}; diff --git a/src/modules/feedback/repositories/feedback.repository.js b/src/modules/feedback/repositories/feedback.repository.js new file mode 100755 index 0000000..d83ae43 --- /dev/null +++ b/src/modules/feedback/repositories/feedback.repository.js @@ -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(); diff --git a/src/modules/feedback/services/feedback.service.js b/src/modules/feedback/services/feedback.service.js new file mode 100755 index 0000000..0c3fab7 --- /dev/null +++ b/src/modules/feedback/services/feedback.service.js @@ -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(); diff --git a/src/modules/hasilResearch/controllers/hasilResearch.controller.js b/src/modules/hasilResearch/controllers/hasilResearch.controller.js new file mode 100755 index 0000000..e3eeff6 --- /dev/null +++ b/src/modules/hasilResearch/controllers/hasilResearch.controller.js @@ -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(); diff --git a/src/modules/hasilResearch/index.js b/src/modules/hasilResearch/index.js new file mode 100755 index 0000000..d95e7d0 --- /dev/null +++ b/src/modules/hasilResearch/index.js @@ -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 +}; diff --git a/src/modules/hasilResearch/models/hasilResearch.model.js b/src/modules/hasilResearch/models/hasilResearch.model.js new file mode 100755 index 0000000..0c2a3b6 --- /dev/null +++ b/src/modules/hasilResearch/models/hasilResearch.model.js @@ -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, +}; diff --git a/src/modules/hasilResearch/repositories/hasilResearch.repository.js b/src/modules/hasilResearch/repositories/hasilResearch.repository.js new file mode 100755 index 0000000..db37b86 --- /dev/null +++ b/src/modules/hasilResearch/repositories/hasilResearch.repository.js @@ -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(); diff --git a/src/modules/hasilResearch/services/hasilResearch.service.js b/src/modules/hasilResearch/services/hasilResearch.service.js new file mode 100755 index 0000000..229f973 --- /dev/null +++ b/src/modules/hasilResearch/services/hasilResearch.service.js @@ -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(); diff --git a/src/modules/lamaranMagang/controllers/lamaranMagang.controller.js b/src/modules/lamaranMagang/controllers/lamaranMagang.controller.js new file mode 100755 index 0000000..9ab9265 --- /dev/null +++ b/src/modules/lamaranMagang/controllers/lamaranMagang.controller.js @@ -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(); diff --git a/src/modules/lamaranMagang/helpers/cleanUpFiles.js b/src/modules/lamaranMagang/helpers/cleanUpFiles.js new file mode 100755 index 0000000..e3c4506 --- /dev/null +++ b/src/modules/lamaranMagang/helpers/cleanUpFiles.js @@ -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, +}; \ No newline at end of file diff --git a/src/modules/lamaranMagang/helpers/email.helper.js b/src/modules/lamaranMagang/helpers/email.helper.js new file mode 100755 index 0000000..19ca822 --- /dev/null +++ b/src/modules/lamaranMagang/helpers/email.helper.js @@ -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: ` +
+

Lamaran Anda Telah Kami Terima

+

Halo ${fullName},

+ +

Terima kasih telah mengirimkan lamaran untuk posisi ${posisi} di Humic Engineering.

+ +

Kami telah menerima dokumen dan informasi yang Anda kirimkan, dan saat ini tim rekrutmen kami sedang meninjaunya dengan seksama.

+ +

Jika profil Anda sesuai dengan kebutuhan kami, tim kami akan menghubungi Anda untuk tahapan seleksi berikutnya. Proses ini dapat memakan waktu hingga beberapa minggu.

+ +

Kami sangat menghargai minat Anda untuk bergabung bersama tim kami.

+ +

Salam hangat,

+

Tim Rekrutmen Humic Engineering

+ +
+

Email ini dikirim secara otomatis. Jika Anda memiliki pertanyaan lebih lanjut, silakan hubungi kami melalui email resmi yang tersedia di website kami.

+
+ `, + }; + + 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 + ? ` +

Halo ${fullName},

+

Selamat! Lamaran Anda untuk posisi ${posisi} di Humic Engineering telah diterima.

+

Tim kami sangat terkesan dengan profil dan kualifikasi Anda. Kami akan segera menghubungi Anda terkait tahapan selanjutnya.

+

Terima kasih telah melamar dan kami menantikan kerja sama yang luar biasa bersama Anda.

+ ` + : ` +

Halo ${fullName},

+

Terima kasih atas lamaran Anda untuk posisi ${posisi} di Humic Engineering.

+

Setelah mempertimbangkan secara seksama, kami memutuskan untuk tidak melanjutkan proses lamaran Anda ke tahap berikutnya.

+

Jangan berkecil hatiโ€”kami sangat menghargai waktu dan usaha Anda. Kami mendorong Anda untuk terus mencoba dan semoga sukses dalam perjalanan karier Anda.

+ `; + + const mailOptions = { + from: process.env.MAIL_USER, + to: email, + subject, + headers: { + 'X-Priority': '1', + 'X-MSMail-Priority': 'High', + Importance: 'high', + }, + html: ` +
+ ${message} +

Salam hangat,

+

Tim Rekrutmen Humic Engineering

+
+

Email ini dikirim secara otomatis. Untuk informasi lebih lanjut, silakan hubungi kami melalui kontak resmi.

+
+ `, + }; + + await transporter.sendMail(mailOptions); + } catch (error) { + console.error('Gagal mengirim email status lamaran:', error); + } +}; + +module.exports = { + sendConfirmationEmail, + sendStatusEmail, +}; diff --git a/src/modules/lamaranMagang/index.js b/src/modules/lamaranMagang/index.js new file mode 100755 index 0000000..f8e488a --- /dev/null +++ b/src/modules/lamaranMagang/index.js @@ -0,0 +1,5 @@ +const lamaranMagangController = require('./controllers/lamaranMagang.controller'); + +module.exports = { + lamaranMagangController, +}; diff --git a/src/modules/lamaranMagang/models/lamaranMagang.model.js b/src/modules/lamaranMagang/models/lamaranMagang.model.js new file mode 100755 index 0000000..b616857 --- /dev/null +++ b/src/modules/lamaranMagang/models/lamaranMagang.model.js @@ -0,0 +1,206 @@ +const joi = require('joi'); + +const phoneRegex = /^(\+62|62|0)8[1-9][0-9]{6,11}$/; + +const createLamaranMagangModel = joi.object().keys({ + id_lowongan_magang: joi.string().uuid().required().messages({ + 'string.base': 'ID lowongan magang harus berupa teks.', + 'string.empty': 'ID lowongan magang tidak boleh kosong.', + 'string.guid': 'Format ID lowongan magang tidak valid.', + 'any.required': 'ID lowongan magang wajib diisi.', + }), + + 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).optional().allow('', null).messages({ + 'string.base': 'Nama belakang harus berupa teks.', + 'string.max': 'Nama belakang maksimal {#limit} karakter.', + }), + + 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.', + }), + + kontak: joi.string().pattern(phoneRegex).max(255).required().messages({ + 'string.base': 'Kontak harus berupa teks.', + 'string.empty': 'Kontak tidak boleh kosong.', + 'string.pattern.base': 'Format nomor kontak tidak valid. Gunakan format Indonesia (08xx atau +628xx).', + 'string.max': 'Kontak maksimal {#limit} karakter.', + 'any.required': 'Kontak wajib diisi.', + }), + + jurusan: joi.string().max(255).required().messages({ + 'string.base': 'Jurusan harus berupa teks.', + 'string.empty': 'Jurusan tidak boleh kosong.', + 'string.max': 'Jurusan maksimal {#limit} karakter.', + 'any.required': 'Jurusan 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.', + }), + + negara: joi.string().max(255).required().messages({ + 'string.base': 'Negara harus berupa teks.', + 'string.empty': 'Negara tidak boleh kosong.', + 'string.max': 'Negara maksimal {#limit} karakter.', + 'any.required': 'Negara wajib diisi.', + }), + + motivasi: joi.string().required().messages({ + 'string.base': 'Motivasi harus berupa teks.', + 'string.empty': 'Motivasi tidak boleh kosong.', + 'any.required': 'Motivasi wajib diisi.', + }), + + relevant_skills: joi.string().max(255).required().messages({ + 'string.base': 'Keahlian yang relevan harus berupa teks.', + 'string.empty': 'Keahlian yang relevan tidak boleh kosong.', + 'string.max': 'Keahlian yang relevan maksimal {#limit} karakter.', + 'any.required': 'Keahlian yang relevan wajib diisi.', + }), + + cv: joi.string().required().messages({ + 'string.base': 'Path CV harus berupa teks.', + 'string.empty': 'Path CV tidak boleh kosong.', + 'any.required': 'CV wajib diupload.', + }), + + portofolio: joi.string().required().messages({ + 'string.base': 'Path portofolio harus berupa teks.', + 'string.empty': 'Path portofolio tidak boleh kosong.', + 'any.required': 'Portofolio wajib diupload.', + }), + + status: joi.string().valid('diterima', 'diproses', 'ditolak').default('pending').messages({ + 'string.base': 'Status harus berupa teks.', + 'any.only': 'Status hanya boleh "diterima", "diproses", atau "ditolak".', + }), +}); + +const updateLamaranStatusModel = joi.object().keys({ + status: joi.string().valid('diterima', 'diproses', 'ditolak').required().messages({ + 'string.base': 'Status harus berupa teks.', + 'string.empty': 'Status tidak boleh kosong.', + 'any.only': 'Status hanya boleh "diterima", "diproses", atau "ditolak".', + 'any.required': 'Status wajib diisi.', + }), +}); + +const updateLamaranMagangModel = joi.object().keys({ + id_lowongan_magang: joi.string().uuid().optional().messages({ + 'string.base': 'ID lowongan magang harus berupa teks.', + 'string.guid': 'Format ID lowongan magang tidak valid.', + }), + + nama_depan: joi.string().max(255).optional().messages({ + 'string.base': 'Nama depan harus berupa teks.', + 'string.max': 'Nama depan maksimal {#limit} karakter.', + }), + + nama_belakang: joi.string().max(255).optional().allow('', null).messages({ + 'string.base': 'Nama belakang harus berupa teks.', + '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.', + }), + + kontak: joi.string().pattern(phoneRegex).max(255).optional().messages({ + 'string.base': 'Kontak harus berupa teks.', + 'string.pattern.base': 'Format nomor kontak tidak valid. Gunakan format Indonesia (08xx atau +628xx).', + 'string.max': 'Kontak maksimal {#limit} karakter.', + }), + + jurusan: joi.string().max(255).optional().messages({ + 'string.base': 'Jurusan harus berupa teks.', + 'string.max': 'Jurusan maksimal {#limit} karakter.', + }), + + universitas: joi.string().max(255).optional().messages({ + 'string.base': 'Universitas harus berupa teks.', + 'string.max': 'Universitas maksimal {#limit} karakter.', + }), + + negara: joi.string().max(255).optional().messages({ + 'string.base': 'Negara harus berupa teks.', + 'string.max': 'Negara maksimal {#limit} karakter.', + }), + + motivasi: joi.string().optional().messages({ + 'string.base': 'Motivasi harus berupa teks.', + }), + + relevant_skills: joi.string().max(255).optional().messages({ + 'string.base': 'Keahlian yang relevan harus berupa teks.', + 'string.max': 'Keahlian yang relevan maksimal {#limit} karakter.', + }), + + cv_path: joi.string().optional().messages({ + 'string.base': 'Path CV harus berupa teks.', + }), + + portofolio_path: joi.string().optional().messages({ + 'string.base': 'Path portofolio harus berupa teks.', + }), + + status: joi.string().valid('diterima', 'diproses', 'ditolak').optional().messages({ + 'string.base': 'Status harus berupa teks.', + 'any.only': 'Status hanya boleh "diterima", "diproses", atau "ditolak".', + }), +}); + +const getAllLamaranMagangModel = joi.object().keys({ + page: joi.number().integer().min(0).optional().messages({ + 'number.base': 'page harus berupa angka.', + }), + limit: joi.number().integer().min(1).optional().messages({ + 'number.base': 'Limit harus berupa angka.', + }), + posisi: joi.string().max(255).optional().messages({ + 'string.base': 'Posisi harus berupa teks.', + 'string.max': 'Posisi maksimal {#limit} karakter.', + }), + status: joi.string().valid('diterima', 'diproses', 'ditolak').optional().messages({ + 'string.base': 'Status harus berupa teks.', + 'any.only': 'Status hanya boleh "diterima", "diproses", atau "ditolak".', + }), + universitas: joi.string().max(255).optional().messages({ + 'string.base': 'Universitas harus berupa teks.', + 'string.max': 'Universitas maksimal {#limit} karakter.', + }), + id_lowongan: joi.string().uuid().optional().messages({ + 'string.base': 'ID lowongan magang harus berupa teks.', + 'string.guid': 'Format ID lowongan magang tidak valid.', + }), +}); + +const statisticModel = joi.object().keys({ + batch: joi.number().integer().min(1).optional().messages({ + 'number.base': 'Batch harus berupa angka.', + 'number.min': 'Batch minimal adalah {#limit}.', + }), +}); + +module.exports = { + createLamaranMagangModel, + updateLamaranStatusModel, + updateLamaranMagangModel, + getAllLamaranMagangModel, + statisticModel, +}; diff --git a/src/modules/lamaranMagang/repositories/lamaranMagang.repository.js b/src/modules/lamaranMagang/repositories/lamaranMagang.repository.js new file mode 100755 index 0000000..69af92b --- /dev/null +++ b/src/modules/lamaranMagang/repositories/lamaranMagang.repository.js @@ -0,0 +1,292 @@ +const prisma = require('../../../helpers/db/db_connection'); +const { Prisma } = require('../../../generated/prisma') + +class LamaranMagangRepository { + async create(lamaranData) { + return prisma.lamaranMagang.create({ + data: { + ...lamaranData, + created_at: new Date(), + update_at: new Date(), + }, + }); + } + + async findAll({ offset, limit, status, posisi, universitas, id_lowongan }) { + const where = {}; + + if (status) { + where.status = status; + } + if (posisi) { + where.lowongan_magang = { posisi: posisi }; + } + if (universitas) { + where.mahasiswa = { universitas: universitas }; + } + if (id_lowongan) { + where.id_lowongan_magang = id_lowongan; + } + + const lamaranList = await prisma.lamaranMagang.findMany({ + skip: offset, + take: limit, + where, + include: { + mahasiswa: { + select: { nama_depan: true, nama_belakang: true }, + }, + lowongan_magang: { + select: { posisi: true, kelompok_peminatan: true }, + }, + batch_data: { + select: { + id: true, + batch_number: true, + is_active: true, + } + } + }, + }); + + return lamaranList; + } + + async countAllLamaran({ status, posisi, universitas }) { + + const where = {}; + + if (status) { + where.status = status; + } + if (posisi) { + where.lowongan_magang = { posisi: posisi }; + } + if (universitas) { + where.mahasiswa = { universitas: universitas }; + } + + return prisma.lamaranMagang.count( + { where } + ); + } + + async findById(id_lamaran_magang) { + return prisma.lamaranMagang.findUnique({ + where: { id: parseInt(id_lamaran_magang) }, + include: { + mahasiswa: { + select: { + nama_depan: true, + nama_belakang: true, + email: true, + kontak: true, + jurusan: true, + cv_path: true, + portofolio_path: true, + motivasi: true, + relevant_skills: true, + universitas: true, + negara: true, + }, + }, + lowongan_magang: { + select: { + posisi: true, + }, + }, + batch_data: { + select: { + id: true, + batch_number: true, + is_active: true, + } + } + }, + }); + } + + async findByMahasiswaAndLowongan(id_mahasiswa, id_lowongan_magang) { + return prisma.lamaranMagang.findFirst({ + where: { + id_mahasiswa: parseInt(id_mahasiswa), + id_lowongan_magang: id_lowongan_magang, + }, + }); + } + + async findAnyLamaranByMahasiswa(id_mahasiswa) { + return prisma.lamaranMagang.findFirst({ + where: { + id_mahasiswa: parseInt(id_mahasiswa), + }, + include: { + lowongan_magang: { + select: { + posisi: true, + }, + }, + }, + }); + } + + async findDetailById(idLamaran) { + return prisma.lamaranMagang.findUnique({ + where: { id: parseInt(idLamaran) }, + include: { + mahasiswa: true, + lowongan_magang: true, + batch_data: { + select: { + id: true, + batch_number: true, + is_active: true, + } + } + }, + }); + } + + async updateStatus(id_lamaran_magang, status) { + return prisma.lamaranMagang.update({ + where: { id: parseInt(id_lamaran_magang) }, + data: { + status, + update_at: new Date(), + }, + }); + } + + async deleteById(id) { + return prisma.lamaranMagang.delete({ + where: { id: parseInt(id) }, + }); + } + + async getStatistics({ query }) { + let where = {}; + + if (query && query.batch) { + where.batch = parseInt(query.batch); + } + + const [total, diterima, ditolak, diproses] = await Promise.all([ + prisma.lamaranMagang.count({ where }), + prisma.lamaranMagang.count({ where: { ...where, status: 'diterima' } }), + prisma.lamaranMagang.count({ where: { ...where, status: 'ditolak' } }), + prisma.lamaranMagang.count({ where: { ...where, status: 'diproses' } }), + ]); + + return { total, diterima, ditolak, diproses }; + } + + async getStatisticsByPosition({ query }) { + let where = {}; + + if (query && query.batch) { + where.batch = parseInt(query.batch); + } + + const data = await prisma.lamaranMagang.groupBy({ + by: ['id_lowongan_magang'], + where, + _count: { + id: true, + }, + }); + + const positions = await Promise.all( + data.map(async (item) => { + const lowongan = await prisma.lowonganMagang.findUnique({ + where: { id: item.id_lowongan_magang }, + select: { posisi: true }, + }); + + const [total, diterima, ditolak, diproses] = await Promise.all([ + prisma.lamaranMagang.count({ + where: { id_lowongan_magang: item.id_lowongan_magang }, + }), + prisma.lamaranMagang.count({ + where: { id_lowongan_magang: item.id_lowongan_magang, status: 'diterima' }, + }), + prisma.lamaranMagang.count({ + where: { id_lowongan_magang: item.id_lowongan_magang, status: 'ditolak' }, + }), + prisma.lamaranMagang.count({ + where: { id_lowongan_magang: item.id_lowongan_magang, status: 'diproses' }, + }), + ]); + + return { + posisi: lowongan?.posisi || 'Tidak Diketahui', + total, + diterima, + ditolak, + diproses, + }; + }) + ); + + return positions; + } + + async getStatisticsByCountry({ query }) { + + // Note : Menggunakan Raw Query karena Prisma ngga dukung join yang kompleks (lebih mudah juga :b) + const whereClause = query.batch + ? Prisma.sql`WHERE lm.batch = ${parseInt(query.batch)}` + : Prisma.sql``; + + const data = await prisma.$queryRaw` + SELECT + m.negara, + COUNT(*) as total, + SUM(lm.status = 'diterima') as diterima, + SUM(lm.status = 'ditolak') as ditolak, + SUM(lm.status = 'diproses') as diproses + FROM lamaran_magang lm + JOIN mahasiswa m ON lm.id_mahasiswa = m.id + ${whereClause} + GROUP BY m.negara + ORDER BY total DESC +`; + + return data.map((item) => ({ + negara: item.negara, + total: Number(item.total), + diterima: Number(item.diterima), + ditolak: Number(item.ditolak), + diproses: Number(item.diproses), + })); + } + + async getStatisticsByUniversity({ query }) { + const whereClause = query.batch + ? Prisma.sql`WHERE lm.batch = ${parseInt(query.batch)}` + : Prisma.sql``; + + const data = await prisma.$queryRaw` + SELECT + m.universitas, + COUNT(*) as total, + SUM(lm.status = 'diterima') as diterima, + SUM(lm.status = 'ditolak') as ditolak, + SUM(lm.status = 'diproses') as diproses + FROM lamaran_magang lm + JOIN mahasiswa m ON lm.id_mahasiswa = m.id + ${whereClause} + GROUP BY m.universitas + ORDER BY total DESC +`; + + return data.map((item) => ({ + universitas: item.universitas, + total: Number(item.total), + diterima: Number(item.diterima), + ditolak: Number(item.ditolak), + diproses: Number(item.diproses), + })); + } +} + +module.exports = new LamaranMagangRepository(); diff --git a/src/modules/lamaranMagang/services/lamaranMagang.service.js b/src/modules/lamaranMagang/services/lamaranMagang.service.js new file mode 100755 index 0000000..67197a0 --- /dev/null +++ b/src/modules/lamaranMagang/services/lamaranMagang.service.js @@ -0,0 +1,311 @@ +const lamaranMagangRepository = require('../repositories/lamaranMagang.repository'); +const mahasiswaRepository = require('../../mahasiswa/repositories/mahasiswa.repository'); +const lowonganMagangRepository = require('../../lowonganMagang/repositories/lowonganMagang.repository'); +const batchRepository = require('../../batch/repositories/batch.repository'); +const imageUploadHelper = require('../../../helpers/utils/imageUpload.helper'); +const { sendConfirmationEmail, sendStatusEmail } = require('../helpers/email.helper'); +const ExcelJS = require('exceljs'); +const path = require('path'); +const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper'); +const { data, error, paginationData } = require('../../../helpers/utils/wrapper'); +const { NotFoundError, ConflictError } = require('../../../helpers/error'); + +class LamaranMagangService { + async createLamaranMagang(id_lowongan_magang, mahasiswaFormData, uploadedFiles) { + try { + const lowonganMagang = await lowonganMagangRepository.findById(id_lowongan_magang); + + if (!lowonganMagang) { + return error(new NotFoundError('Lowongan Magang Tidak Ditemukan')); + } + + const batchId = lowonganMagang.batch?.id || null; + + const existingMahasiswa = await mahasiswaRepository.findByEmail(mahasiswaFormData.email); + + if (existingMahasiswa) { + + const existingLamaran = await lamaranMagangRepository.findAnyLamaranByMahasiswa( + existingMahasiswa.id + ); + + if (existingLamaran) { + const posisiYangSudahDilamar = existingLamaran.lowongan_magang?.posisi || 'posisi tertentu'; + return error(new ConflictError( + `Anda sudah pernah melamar untuk posisi ${posisiYangSudahDilamar}. Setiap pengguna hanya dapat melamar satu posisi.` + )); + } + + const lamaranData = { + id_mahasiswa: existingMahasiswa.id, + id_lowongan_magang, + batch: batchId, + status: 'diproses', + }; + + await lamaranMagangRepository.create(lamaranData); + + return data({ + id_mahasiswa: existingMahasiswa.id, + id_lowongan_magang, + batch: batchId, + status: 'diproses', + }); + } + + const cv_path = imageUploadHelper.uploadCV(uploadedFiles.cv); + const portofolio_path = imageUploadHelper.uploadPortofolio(uploadedFiles.portofolio); + + const namaBelakangFinal = mahasiswaFormData.nama_belakang?.trim() || null; + + const mahasiswaData = { + nama_depan: mahasiswaFormData.nama_depan, + nama_belakang: namaBelakangFinal, + email: mahasiswaFormData.email, + kontak: mahasiswaFormData.kontak, + jurusan: mahasiswaFormData.jurusan, + universitas: mahasiswaFormData.universitas, + negara: mahasiswaFormData.negara, + motivasi: mahasiswaFormData.motivasi, + relevant_skills: mahasiswaFormData.relevant_skills, + role: 'student', + cv_path, + portofolio_path, + }; + + const createdMahasiswa = await mahasiswaRepository.create(mahasiswaData); + const id_mahasiswa = createdMahasiswa.id; + + const lamaranData = { + id_mahasiswa, + id_lowongan_magang, + batch: batchId, + status: 'diproses', + }; + + await lamaranMagangRepository.create(lamaranData); + + await sendConfirmationEmail(mahasiswaData, lowonganMagang); + + return data({ + id_mahasiswa, + id_lowongan_magang, + batch: batchId, + status: 'diproses', + }); + } catch (err) { + return error(err); + } + } + + async getAllLamaranMagang(query = {}) { + try { + const { page = 1, limit = 10, posisi, status, universitas, id_lowongan } = query; + const offset = (page - 1) * limit; + const lamaranList = await lamaranMagangRepository.findAll({ offset, limit, status, posisi, universitas, id_lowongan }); + const total = await lamaranMagangRepository.countAllLamaran({ status, posisi, universitas, id_lowongan }); + + const totalPages = Math.ceil(total / limit); + const meta = { page: Number(page), limit: Number(limit), total, totalPages }; + return paginationData(lamaranList, meta); + } catch (err) { + return error(err); + } + } + + async getLamaranById(id_lamaran_magang) { + try { + const lamaran = await lamaranMagangRepository.findById(id_lamaran_magang); + + if (!lamaran) { + return error(new NotFoundError('Lamaran Magang Tidak Ditemukan')); + } + + return data(lamaran); + } catch (err) { + return error(err); + } + } + + async getDetailLamaranById(id_lamaran) { + try { + const lamaran = await lamaranMagangRepository.findDetailById(id_lamaran); + + if (!lamaran) { + return error(new NotFoundError('Lamaran Magang Tidak Ditemukan')); + } + + return data(lamaran); + } catch (err) { + return error(err); + } + } + + async updateLamaranStatus(id_lamaran_magang, status) { + try { + const checkResult = await this.getDetailLamaranById(id_lamaran_magang); + + if (checkResult.err) { + return checkResult; + } + + const result = await lamaranMagangRepository.updateStatus(id_lamaran_magang, status); + + await sendStatusEmail(checkResult.data, status); + + return data(result); + } catch (err) { + return error(err); + } + } + + async deleteLamaran(id) { + try { + const checkResult = await this.getDetailLamaranById(id); + if (checkResult.err) { + return checkResult; + } + + const lamaran = checkResult.data; + + if (lamaran.cv_path) { + const cvFileName = path.basename(lamaran.cv_path); + await deleteFileIfExists(cvFileName); + } + + if (lamaran.portofolio_path) { + const portofolioFileName = path.basename(lamaran.portofolio_path); + await deleteFileIfExists(portofolioFileName); + } + + const result = await lamaranMagangRepository.deleteById(id); + return data(result); + } catch (err) { + return error(err); + } + } + + async exportToExcel() { + try { + const lamaranList = await lamaranMagangRepository.findAll({}); + + if (!lamaranList || lamaranList.length === 0) { + return error(new NotFoundError("Tidak ada data lamaran untuk diekspor")); + } + + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet('Lamaran Magang'); + + worksheet.columns = [ + { header: 'ID', key: 'id', width: 10 }, + { header: 'Nama Depan', key: 'nama_depan', width: 20 }, + { header: 'Nama Belakang', key: 'nama_belakang', width: 20 }, + { header: 'Posisi', key: 'posisi', width: 25 }, + { header: 'Kelompok Peminatan', key: 'kelompok_peminatan', width: 25 }, + { header: 'Status', key: 'status', width: 15 }, + { header: 'Tanggal Dibuat', key: 'created_at', width: 20 }, + ]; + + lamaranList.forEach((lamaran) => { + worksheet.addRow({ + id: lamaran.id, + nama_depan: lamaran.mahasiswa?.nama_depan || '', + nama_belakang: lamaran.mahasiswa?.nama_belakang || '', + posisi: lamaran.lowongan_magang?.posisi || '', + kelompok_peminatan: lamaran.lowongan_magang?.kelompok_peminatan || '', + status: lamaran.status, + created_at: lamaran.created_at, + }); + }); + + worksheet.getRow(1).font = { bold: true }; + worksheet.getRow(1).fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFD3D3D3' }, + }; + + return data(workbook); + } catch (err) { + return error(err); + } + } + + async getDashboardStatistics(query) { + try { + const batch = await batchRepository.getBatchById(query.batch); + const batchDisplay = batch ? batch.batch_number : 'Data Seluruh Batch'; + const stats = await lamaranMagangRepository.getStatistics({ query }); + + const acceptanceRate = stats.total > 0 ? ((stats.diterima / stats.total) * 100).toFixed(1) : 0; + const rejectionRate = stats.total > 0 ? ((stats.ditolak / stats.total) * 100).toFixed(1) : 0; + const pendingRate = stats.total > 0 ? ((stats.diproses / stats.total) * 100).toFixed(1) : 0; + + if (stats.total === 0) { + return error(new NotFoundError(`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`)); + } + + return data({ + total_pendaftar: stats.total, + total_diterima: stats.diterima, + total_ditolak: stats.ditolak, + sedang_diproses: stats.diproses, + presentase_diterima: `${acceptanceRate}%`, + presentase_ditolak: `${rejectionRate}%`, + presentase_diproses: `${pendingRate}%`, + batch: batchDisplay, + }); + } catch (err) { + return error(err); + } + } + + async getStatisticsByPosition(query) { + try { + const batch = await batchRepository.getBatchById(query.batch); + const batchDisplay = batch ? batch.batch_number : 'semua batch'; + const positions = await lamaranMagangRepository.getStatisticsByPosition({ query }); + + if (positions.length === 0) { + return error(new NotFoundError(`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`)); + } + + return data(positions); + } catch (err) { + return error(err); + } + } + + async getStatisticsByCountry(query) { + try { + const batch = await batchRepository.getBatchById(query.batch); + const batchDisplay = batch ? batch.batch_number : 'semua batch'; + const countries = await lamaranMagangRepository.getStatisticsByCountry({ query }); + if (countries.length === 0) { + return error(new NotFoundError(`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`)); + } + + return data(countries); + } catch (err) { + return error(err); + } + } + + async getStatisticsByUniversity(query) { + try { + const batch = await batchRepository.getBatchById(query.batch); + const batchDisplay = batch ? batch.batch_number : 'semua batch'; + const universities = await lamaranMagangRepository.getStatisticsByUniversity({ query }); + if (universities.length === 0) { + return error(new NotFoundError(`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`)); + } + return data(universities); + } catch (err) { + return error(err); + } + } +} + + + +module.exports = new LamaranMagangService(); diff --git a/src/modules/lowonganMagang/controllers/lowonganMagang.controller.js b/src/modules/lowonganMagang/controllers/lowonganMagang.controller.js new file mode 100755 index 0000000..9a6fe04 --- /dev/null +++ b/src/modules/lowonganMagang/controllers/lowonganMagang.controller.js @@ -0,0 +1,157 @@ +const lowonganMagangService = require('../services/lowonganMagang.service'); +const { response, error } = require('../../../helpers/utils/wrapper'); +const { BadRequestError, ForbiddenError, InternalServerError } = require('../../../helpers/error'); +const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code'); +const { isValidPayload } = require('../../../helpers/utils/validator'); +const { createLowonganMagangModel, updateLowonganMagangModel } = require('../models/lowonganMagang.model'); +const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper'); + +class LowonganMagangController { + async addLowonganMagang(req, res) { + try { + const image_path = req.file; + + const validatePayload = isValidPayload(req.body, createLowonganMagangModel); + + 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 lowonganMagangService.createLowonganMagang(validatePayload.data, image_path); + + if (result.err) { + return response(res, 'fail', result); + } + + return response(res, 'success', result, 'Lowongan magang 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 getAllLowonganMagang(req, res) { + try { + const result = await lowonganMagangService.getAllLowonganMagang(); + + if (result.err) { + return response(res, 'fail', result); + } + + return response(res, 'success', result, 'Berhasil mengambil semua lowongan magang', SUCCESS.OK); + } catch (err) { + return response(res, 'fail', error(new InternalServerError(err.message)), + 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR); + } + } + + async getLowonganMagangById(req, res) { + try { + const { id } = req.params; + + const result = await lowonganMagangService.getLowonganMagangById(id); + + if (result.err) { + return response(res, 'fail', result); + } + + return response(res, 'success', result, 'Berhasil mengambil lowongan magang berdasarkan ID', SUCCESS.OK); + } catch (err) { + return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR); + } + } + + async getLowonganByKelompokPeminatan(req, res) { + try { + const { kelompok_peminatan } = req.params; + + const result = await lowonganMagangService.getLowonganByKelompokPeminatan(kelompok_peminatan); + + if (result.err) { + return response(res, 'fail', result); + } + + return response(res, 'success', result, 'Berhasil mengambil lowongan magang berdasarkan kelompok peminatan', SUCCESS.OK); + } catch (err) { + return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR); + } + } + + async getAllKelompokPeminatan(req, res) { + try { + const result = await lowonganMagangService.getAllKelompokPeminatan(); + + if (result.err) { + return response(res, 'fail', result); + } + + return response(res, 'success', result, 'Berhasil mengambil semua kelompok peminatan', SUCCESS.OK); + } catch (err) { + return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR); + } + } + + async updateLowonganMagang(req, res) { + try { + const { id } = req.params; + const image_path = req.file; + + const validatePayload = isValidPayload(req.body, updateLowonganMagangModel); + + 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 lowonganMagangService.updateLowonganMagang(id, validatePayload.data, image_path); + + if (result.err) { + return response(res, 'fail', result); + } + + return response(res, 'success', result, 'Lowongan magang 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 deleteLowonganMagangById(req, res) { + try { + const { id } = req.params; + + const result = await lowonganMagangService.deleteLowonganMagang(id); + + if (result.err) { + return response(res, 'fail', result); + } + + return response(res, 'success', result, 'Lowongan magang 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 LowonganMagangController(); diff --git a/src/modules/lowonganMagang/helpers/fileUpload.helper.js b/src/modules/lowonganMagang/helpers/fileUpload.helper.js new file mode 100755 index 0000000..98a8cd1 --- /dev/null +++ b/src/modules/lowonganMagang/helpers/fileUpload.helper.js @@ -0,0 +1,21 @@ +class FileUploadHelper { + uploadImage(file) { + if (!file) { + throw new Error('Please upload an image!'); + } + + const filePath = `/uploads/${file.filename}`; + return filePath; + } + + validateImage(file) { + if (!file) { + return false; + } + + const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg', 'image/webp']; + return allowedTypes.includes(file.mimetype); + } +} + +module.exports = new FileUploadHelper(); diff --git a/src/modules/lowonganMagang/helpers/status.helper.js b/src/modules/lowonganMagang/helpers/status.helper.js new file mode 100755 index 0000000..1cf93d7 --- /dev/null +++ b/src/modules/lowonganMagang/helpers/status.helper.js @@ -0,0 +1,26 @@ +class StatusHelper { + calculateStatus(durasi_awal, durasi_akhir) { + const now = new Date(); + const startDate = new Date(durasi_awal); + const endDate = new Date(durasi_akhir); + + if (now >= startDate && now <= endDate) { + return 'dibuka'; + } + return 'ditutup'; + } + + updateLowonganStatus(lowongan) { + const status = this.calculateStatus(lowongan.durasi_awal, lowongan.durasi_akhir); + return { + ...lowongan, + status_lowongan: status, + }; + } + + updateMultipleLowonganStatus(lowonganList) { + return lowonganList.map((lowongan) => this.updateLowonganStatus(lowongan)); + } +} + +module.exports = new StatusHelper(); diff --git a/src/modules/lowonganMagang/index.js b/src/modules/lowonganMagang/index.js new file mode 100755 index 0000000..22d9fc6 --- /dev/null +++ b/src/modules/lowonganMagang/index.js @@ -0,0 +1,11 @@ +const lowonganMagangController = require('./controllers/lowonganMagang.controller'); +const lowonganMagangService = require('./services/lowonganMagang.service'); +const lowonganMagangRepository = require('./repositories/lowonganMagang.repository'); +const statusHelper = require('./helpers/status.helper'); + +module.exports = { + lowonganMagangController, + lowonganMagangService, + lowonganMagangRepository, + statusHelper +}; diff --git a/src/modules/lowonganMagang/models/lowonganMagang.model.js b/src/modules/lowonganMagang/models/lowonganMagang.model.js new file mode 100755 index 0000000..ff70e54 --- /dev/null +++ b/src/modules/lowonganMagang/models/lowonganMagang.model.js @@ -0,0 +1,128 @@ +const joi = require('joi'); + +const createLowonganMagangModel = joi.object().keys({ + 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.', + }), + + kelompok_peminatan: joi.string().max(255).required().messages({ + 'string.base': 'Kelompok peminatan harus berupa teks.', + 'string.empty': 'Kelompok peminatan tidak boleh kosong.', + 'string.max': 'Kelompok peminatan maksimal {#limit} karakter.', + 'any.required': 'Kelompok peminatan wajib diisi.', + }), + + jobdesk: joi.string().required().messages({ + 'string.base': 'Jobdesk harus berupa teks.', + 'string.empty': 'Jobdesk tidak boleh kosong.', + 'any.required': 'Jobdesk wajib diisi.', + }), + + lokasi: joi.string().max(255).required().messages({ + 'string.base': 'Lokasi harus berupa teks.', + 'string.empty': 'Lokasi tidak boleh kosong.', + 'string.max': 'Lokasi maksimal {#limit} karakter.', + 'any.required': 'Lokasi wajib diisi.', + }), + + kualifikasi: joi.string().required().messages({ + 'string.base': 'Kualifikasi harus berupa teks.', + 'string.empty': 'Kualifikasi tidak boleh kosong.', + 'any.required': 'Kualifikasi wajib diisi.', + }), + + benefit: joi.string().required().messages({ + 'string.base': 'Benefit harus berupa teks.', + 'string.empty': 'Benefit tidak boleh kosong.', + 'any.required': 'Benefit wajib diisi.', + }), + + durasi_awal: joi.date().required().messages({ + 'date.base': 'Durasi awal harus berupa tanggal yang valid.', + 'any.required': 'Durasi awal wajib diisi.', + }), + + durasi_akhir: joi.date().min(joi.ref('durasi_awal')).required().messages({ + 'date.base': 'Durasi akhir harus berupa tanggal yang valid.', + 'date.min': 'Durasi akhir harus setelah atau sama dengan durasi awal.', + 'any.required': 'Durasi akhir wajib diisi.', + }), + + paid: joi.string().valid('paid', 'unpaid').required().messages({ + 'string.empty': 'Status paid tidak boleh kosong.', + 'string.base': 'Status paid harus berupa teks.', + 'any.only': 'Status paid hanya boleh "paid" atau "unpaid".', + 'any.required': 'Status paid wajib diisi.', + }), + + 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}.', + }), + + image_path: joi.string().optional().allow('', null).messages({ + 'string.base': 'Path gambar harus berupa teks.', + }), +}); + +const updateLowonganMagangModel = joi.object().keys({ + posisi: joi.string().max(255).optional().messages({ + 'string.base': 'Posisi harus berupa teks.', + 'string.max': 'Posisi maksimal {#limit} karakter.', + }), + + kelompok_peminatan: joi.string().max(255).optional().messages({ + 'string.base': 'Kelompok peminatan harus berupa teks.', + 'string.max': 'Kelompok peminatan maksimal {#limit} karakter.', + }), + + jobdesk: joi.string().optional().messages({ + 'string.base': 'Jobdesk harus berupa teks.', + }), + + lokasi: joi.string().max(255).optional().messages({ + 'string.base': 'Lokasi harus berupa teks.', + 'string.max': 'Lokasi maksimal {#limit} karakter.', + }), + + kualifikasi: joi.string().optional().messages({ + 'string.base': 'Kualifikasi harus berupa teks.', + }), + + benefit: joi.string().optional().messages({ + 'string.base': 'Benefit harus berupa teks.', + }), + + durasi_awal: joi.date().optional().messages({ + 'date.base': 'Durasi awal harus berupa tanggal yang valid.', + }), + + durasi_akhir: joi.date().min(joi.ref('durasi_awal')).optional().messages({ + 'date.base': 'Durasi akhir harus berupa tanggal yang valid.', + 'date.min': 'Durasi akhir harus setelah atau sama dengan durasi awal.', + }), + + paid: joi.string().valid('paid', 'unpaid').optional().messages({ + 'string.base': 'Status paid harus berupa teks.', + 'any.only': 'Status paid hanya boleh "paid" atau "unpaid".', + }), + + 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}.', + }), + + image_path: joi.string().optional().allow('', null).messages({ + 'string.base': 'Path gambar harus berupa teks.', + }), +}); + +module.exports = { + createLowonganMagangModel, + updateLowonganMagangModel, +}; diff --git a/src/modules/lowonganMagang/repositories/lowonganMagang.repository.js b/src/modules/lowonganMagang/repositories/lowonganMagang.repository.js new file mode 100755 index 0000000..1d30777 --- /dev/null +++ b/src/modules/lowonganMagang/repositories/lowonganMagang.repository.js @@ -0,0 +1,78 @@ +const prisma = require('../../../helpers/db/db_connection'); + + +class LowonganMagangRepository { + async create(lowonganData) { + return prisma.lowonganMagang.create({ + data: lowonganData, + }); + } + + async findAll() { + return prisma.lowonganMagang.findMany({ + include: { + batch: { + select: { + id: true, + batch_number: true, + is_active: true, + }, + }, + }, + }); + } + + async findById(id) { + return prisma.lowonganMagang.findUnique({ + where: { id: id }, + include: { + batch: { + select: { + id: true, + batch_number: true, + is_active: true, + }, + }, + }, + }); + } + + async findByKelompokPeminatan(kelompok_peminatan) { + return prisma.lowonganMagang.findMany({ + where: { kelompok_peminatan }, + include: { + batch: { + select: { + id: true, + batch_number: true, + is_active: true, + }, + }, + }, + }); + } + + async findAllKelompokPeminatan() { + return prisma.lowonganMagang.findMany({ + select: { + kelompok_peminatan: true, + }, + distinct: ['kelompok_peminatan'], + }); + } + + async updateById(id, lowonganData) { + return prisma.lowonganMagang.update({ + where: { id: id }, + data: lowonganData, + }); + } + + async deleteById(id) { + return prisma.lowonganMagang.delete({ + where: { id: id }, + }); + } +} + +module.exports = new LowonganMagangRepository(); diff --git a/src/modules/lowonganMagang/services/lowonganMagang.service.js b/src/modules/lowonganMagang/services/lowonganMagang.service.js new file mode 100755 index 0000000..cce8d6d --- /dev/null +++ b/src/modules/lowonganMagang/services/lowonganMagang.service.js @@ -0,0 +1,191 @@ +const { v4: uuidv4 } = require('uuid'); +const path = require('path'); +const lowonganMagangRepository = require('../repositories/lowonganMagang.repository'); +const batchRepository = require('../../batch/repositories/batch.repository'); +const imageUploadHelper = require('../../../helpers/utils/imageUpload.helper'); +const statusHelper = require('../helpers/status.helper'); +const { deleteFileIfExists, validateImagePath } = require('../../../helpers/utils/fileHelper'); +const { data, error } = require('../../../helpers/utils/wrapper'); +const { NotFoundError } = require('../../../helpers/error'); + +class LowonganMagangService { + async createLowonganMagang(lowonganData, imageFile) { + try { + const activeBatch = await batchRepository.findActiveBatch(); + + if (!activeBatch) { + return error(new NotFoundError('Tidak ada batch yang aktif')); + } + + const id = uuidv4(); + const imagePath = imageUploadHelper.uploadImage(imageFile); + const status = statusHelper.calculateStatus( + lowonganData.durasi_awal, + lowonganData.durasi_akhir + ); + + const newLowonganData = { + id, + ...lowonganData, + id_batch: activeBatch.id, + status_lowongan: status, + image_path: imagePath, + }; + + const result = await lowonganMagangRepository.create(newLowonganData); + 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 getAllLowonganMagang() { + try { + const lowonganList = await lowonganMagangRepository.findAll(); + + if (lowonganList.length === 0) { + return data([]); + } + + const updatedList = statusHelper.updateMultipleLowonganStatus(lowonganList); + + const validatedList = updatedList.map(lowongan => ({ + ...lowongan, + image_path: validateImagePath(lowongan.image_path) + })); + + return data(validatedList); + } catch (err) { + return error(err); + } + } + + async getLowonganMagangById(id) { + try { + const lowongan = await lowonganMagangRepository.findById(id); + + if (!lowongan) { + return error(new NotFoundError('Lowongan Magang Tidak Ditemukan')); + } + + const updatedLowongan = statusHelper.updateLowonganStatus(lowongan); + + updatedLowongan.image_path = validateImagePath(updatedLowongan.image_path); + + return data(updatedLowongan); + } catch (err) { + return error(err); + } + } + + async getLowonganByKelompokPeminatan(kelompok_peminatan) { + try { + const lowonganList = await lowonganMagangRepository.findByKelompokPeminatan( + kelompok_peminatan + ); + + if (lowonganList.length === 0) { + return data([]); + } + + const updatedList = statusHelper.updateMultipleLowonganStatus(lowonganList); + + const validatedList = updatedList.map(lowongan => ({ + ...lowongan, + image_path: validateImagePath(lowongan.image_path) + })); + + return data(validatedList); + } catch (err) { + return error(err); + } + } + + async getAllKelompokPeminatan() { + try { + const kelompokList = await lowonganMagangRepository.findAllKelompokPeminatan(); + const kelompokNames = kelompokList.map((row) => row.kelompok_peminatan); + return data(kelompokNames); + } catch (err) { + return error(err); + } + } + + async updateLowonganMagang(id, lowonganData, imageFile) { + let oldImagePath = null; + let newImageUploaded = false; + + try { + const checkResult = await this.getLowonganMagangById(id); + if (checkResult.err) { + return checkResult; + } + + const existingLowongan = checkResult.data; + + if (imageFile) { + oldImagePath = existingLowongan.image_path; + + lowonganData.image_path = imageUploadHelper.uploadImage(imageFile); + newImageUploaded = true; + + if (oldImagePath) { + const oldFileName = path.basename(oldImagePath); + await deleteFileIfExists(oldFileName); + } + } + + if (lowonganData.durasi_awal || lowonganData.durasi_akhir) { + const durasi_awal = lowonganData.durasi_awal || existingLowongan.durasi_awal; + const durasi_akhir = lowonganData.durasi_akhir || existingLowongan.durasi_akhir; + + lowonganData.status_lowongan = statusHelper.calculateStatus( + durasi_awal, + durasi_akhir + ); + } + + const result = await lowonganMagangRepository.updateById(id, lowonganData); + 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 deleteLowonganMagang(id) { + try { + const checkResult = await this.getLowonganMagangById(id); + if (checkResult.err) { + return checkResult; + } + + const lowongan = checkResult.data; + + if (lowongan.image_path) { + const fileName = path.basename(lowongan.image_path); + await deleteFileIfExists(fileName); + } + + const result = await lowonganMagangRepository.deleteById(id); + return data(result); + } catch (err) { + return error(err); + } + } +} + +module.exports = new LowonganMagangService(); diff --git a/src/modules/mahasiswa/controllers/mahasiswa.controller.js b/src/modules/mahasiswa/controllers/mahasiswa.controller.js new file mode 100755 index 0000000..168bebd --- /dev/null +++ b/src/modules/mahasiswa/controllers/mahasiswa.controller.js @@ -0,0 +1,137 @@ +const mahasiswaService = require('../services/mahasiswa.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 { createMahasiswaModel, updateMahasiswaModel, getAllMahasiswaModel } = require('../models/mahasiswa.model'); + +class MahasiswaController { + async createMahasiswa(req, res) { + try { + const validatePayload = isValidPayload(req.body, createMahasiswaModel); + + if (validatePayload.err) { + return response( + res, + 'fail', + { err: validatePayload.err, data: null }, + 'Data tidak valid', + ERROR.EXPECTATION_FAILED + ); + } + + const result = await mahasiswaService.createMahasiswa(validatePayload.data); + + if (result.err) { + return response(res, 'fail', result); + } + + return response(res, 'success', result, 'Mahasiswa berhasil dibuat', SUCCESS.CREATED); + } catch (err) { + return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR); + } + } + + async getAllMahasiswa(req, res) { + try { + const validatePayload = isValidPayload(req.query, getAllMahasiswaModel); + + 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 mahasiswaService.getAllMahasiswa({ query }); + + if (result.err) { + return response(res, 'fail', result); + } + + return paginationResponse(res, 'success', result, 'Data mahasiswa berhasil diambil', SUCCESS.OK); + } catch (err) { + return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR); + } + } + + async getMahasiswaById(req, res) { + try { + const { id } = req.params; + + const result = await mahasiswaService.getMahasiswaById(id); + + if (result.err) { + return response(res, 'fail', result); + } + + return response(res, 'success', result, 'Mahasiswa berhasil diambil', SUCCESS.OK); + } catch (err) { + return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR); + } + } + + async updateMahasiswa(req, res) { + try { + const { id } = req.params; + + const validatePayload = isValidPayload(req.body, updateMahasiswaModel); + + if (validatePayload.err) { + return response( + res, + 'fail', + { err: validatePayload.err, data: null }, + 'Data tidak valid', + ERROR.EXPECTATION_FAILED + ); + } + + const result = await mahasiswaService.updateMahasiswa(id, validatePayload.data); + + if (result.err) { + return response(res, 'fail', result); + } + + return response(res, 'success', result, 'Mahasiswa berhasil diperbarui', SUCCESS.OK); + } catch (err) { + return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR); + } + } + + async deleteMahasiswa(req, res) { + try { + const { id } = req.params; + + const result = await mahasiswaService.deleteMahasiswa(id); + + if (result.err) { + return response(res, 'fail', result); + } + + return response(res, 'success', result, 'Mahasiswa berhasil dihapus', SUCCESS.OK); + } catch (err) { + return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR); + } + } + + async deleteAllMahasiswa(req, res) { + try { + const result = await mahasiswaService.deleteAllMahasiswa(); + + if (result.err) { + return response(res, 'fail', result); + } + + return response(res, 'success', result, 'Semua mahasiswa 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 MahasiswaController(); diff --git a/src/modules/mahasiswa/index.js b/src/modules/mahasiswa/index.js new file mode 100755 index 0000000..5481cc0 --- /dev/null +++ b/src/modules/mahasiswa/index.js @@ -0,0 +1,9 @@ +const mahasiswaController = require('./controllers/mahasiswa.controller'); +const mahasiswaService = require('./services/mahasiswa.service'); +const mahasiswaRepository = require('./repositories/mahasiswa.repository'); + +module.exports = { + mahasiswaController, + mahasiswaService, + mahasiswaRepository +}; diff --git a/src/modules/mahasiswa/models/mahasiswa.model.js b/src/modules/mahasiswa/models/mahasiswa.model.js new file mode 100755 index 0000000..b974347 --- /dev/null +++ b/src/modules/mahasiswa/models/mahasiswa.model.js @@ -0,0 +1,159 @@ +const joi = require('joi'); + +const phoneRegex = /^(\+62|62|0)8[1-9][0-9]{6,11}$/; + +const createMahasiswaModel = 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).optional().allow('', null).messages({ + 'string.base': 'Nama belakang harus berupa teks.', + 'string.max': 'Nama belakang maksimal {#limit} karakter.', + }), + + 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.', + }), + + kontak: joi.string().pattern(phoneRegex).max(255).required().messages({ + 'string.base': 'Kontak harus berupa teks.', + 'string.empty': 'Kontak tidak boleh kosong.', + 'string.pattern.base': 'Format nomor kontak tidak valid. Gunakan format Indonesia (08xx atau +628xx).', + 'string.max': 'Kontak maksimal {#limit} karakter.', + 'any.required': 'Kontak wajib diisi.', + }), + + jurusan: joi.string().max(255).required().messages({ + 'string.base': 'Jurusan harus berupa teks.', + 'string.empty': 'Jurusan tidak boleh kosong.', + 'string.max': 'Jurusan maksimal {#limit} karakter.', + 'any.required': 'Jurusan 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.', + }), + + negara: joi.string().max(255).required().messages({ + 'string.base': 'Negara harus berupa teks.', + 'string.empty': 'Negara tidak boleh kosong.', + 'string.max': 'Negara maksimal {#limit} karakter.', + 'any.required': 'Negara wajib diisi.', + }), + + role: joi.string().valid('student').default('student').messages({ + 'string.base': 'Role harus berupa teks.', + 'any.only': 'Role hanya boleh "student".', + }), + + cv_path: joi.string().required().messages({ + 'string.base': 'Path CV harus berupa teks.', + 'string.empty': 'Path CV tidak boleh kosong.', + 'any.required': 'CV wajib diupload.', + }), + + portofolio_path: joi.string().required().messages({ + 'string.base': 'Path portofolio harus berupa teks.', + 'string.empty': 'Path portofolio tidak boleh kosong.', + 'any.required': 'Portofolio wajib diupload.', + }), + + motivasi: joi.string().required().messages({ + 'string.base': 'Motivasi harus berupa teks.', + 'string.empty': 'Motivasi tidak boleh kosong.', + 'any.required': 'Motivasi wajib diisi.', + }), + + relevant_skills: joi.string().max(255).required().messages({ + 'string.base': 'Keahlian yang relevan harus berupa teks.', + 'string.empty': 'Keahlian yang relevan tidak boleh kosong.', + 'string.max': 'Keahlian yang relevan maksimal {#limit} karakter.', + 'any.required': 'Keahlian yang relevan wajib diisi.', + }), +}); + +const updateMahasiswaModel = joi.object().keys({ + nama_depan: joi.string().max(255).optional().messages({ + 'string.base': 'Nama depan harus berupa teks.', + 'string.max': 'Nama depan maksimal {#limit} karakter.', + }), + + nama_belakang: joi.string().max(255).optional().allow('', null).messages({ + 'string.base': 'Nama belakang harus berupa teks.', + '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.', + }), + + kontak: joi.string().pattern(phoneRegex).max(255).optional().messages({ + 'string.base': 'Kontak harus berupa teks.', + 'string.pattern.base': 'Format nomor kontak tidak valid. Gunakan format Indonesia (08xx atau +628xx).', + 'string.max': 'Kontak maksimal {#limit} karakter.', + }), + + jurusan: joi.string().max(255).optional().messages({ + 'string.base': 'Jurusan harus berupa teks.', + 'string.max': 'Jurusan maksimal {#limit} karakter.', + }), + + universitas: joi.string().max(255).optional().messages({ + 'string.base': 'Universitas harus berupa teks.', + 'string.max': 'Universitas maksimal {#limit} karakter.', + }), + + negara: joi.string().max(255).optional().messages({ + 'string.base': 'Negara harus berupa teks.', + 'string.max': 'Negara maksimal {#limit} karakter.', + }), + + cv_path: joi.string().optional().messages({ + 'string.base': 'Path CV harus berupa teks.', + }), + + portofolio_path: joi.string().optional().messages({ + 'string.base': 'Path portofolio harus berupa teks.', + }), + + motivasi: joi.string().optional().messages({ + 'string.base': 'Motivasi harus berupa teks.', + }), + + relevant_skills: joi.string().max(255).optional().messages({ + 'string.base': 'Keahlian yang relevan harus berupa teks.', + 'string.max': 'Keahlian yang relevan maksimal {#limit} karakter.', + }), +}); + +const getAllMahasiswaModel = joi.object().keys({ + page: joi.number().integer().min(1).default(1).messages({ + 'number.base': 'Halaman harus berupa angka.', + 'number.integer': 'Halaman harus berupa angka bulat.', + 'number.min': 'Halaman minimal adalah {#limit}.', + }), + limit: joi.number().integer().min(1).default(10).messages({ + 'number.base': 'Limit harus berupa angka.', + 'number.integer': 'Limit harus berupa angka bulat.', + 'number.min': 'Limit minimal adalah {#limit}.', + }), +}); + +module.exports = { + createMahasiswaModel, + updateMahasiswaModel, + getAllMahasiswaModel +}; diff --git a/src/modules/mahasiswa/repositories/mahasiswa.repository.js b/src/modules/mahasiswa/repositories/mahasiswa.repository.js new file mode 100755 index 0000000..251a64a --- /dev/null +++ b/src/modules/mahasiswa/repositories/mahasiswa.repository.js @@ -0,0 +1,65 @@ +const prisma = require('../../../helpers/db/db_connection'); + +class MahasiswaRepository { + async create(mahasiswaData) { + return prisma.mahasiswa.create({ + data: mahasiswaData, + }); + } + + async findAll({ offset, limit }) { + return prisma.mahasiswa.findMany({ + skip: offset, + take: limit, + }); + } + + async findById(id) { + return prisma.mahasiswa.findUnique({ + where: { id: parseInt(id) }, + include: { + lamaran_magang: { + include: { + lowongan_magang: { + select: { + posisi: true, + kelompok_peminatan: true, + durasi_awal: true, + durasi_akhir: true, + }, + }, + }, + }, + }, + }); + } + + async findByEmail(email) { + return prisma.mahasiswa.findUnique({ + where: { email }, + }); + } + + async updateById(id, mahasiswaData) { + return prisma.mahasiswa.update({ + where: { id: parseInt(id) }, + data: mahasiswaData, + }); + } + + async deleteById(id) { + return prisma.mahasiswa.delete({ + where: { id: parseInt(id) }, + }); + } + + async deleteAll() { + return prisma.mahasiswa.deleteMany(); + } + + async countAllMahasiswa() { + return prisma.mahasiswa.count(); + } +} + +module.exports = new MahasiswaRepository(); diff --git a/src/modules/mahasiswa/services/mahasiswa.service.js b/src/modules/mahasiswa/services/mahasiswa.service.js new file mode 100755 index 0000000..8af6998 --- /dev/null +++ b/src/modules/mahasiswa/services/mahasiswa.service.js @@ -0,0 +1,119 @@ +const mahasiswaRepository = require('../repositories/mahasiswa.repository'); +const { data, error } = require('../../../helpers/utils/wrapper'); +const { BadRequestError, NotFoundError } = require('../../../helpers/error'); +const { paginationData } = require("../../../helpers/utils/wrapper"); + +class MahasiswaService { + async createMahasiswa(mahasiswaData) { + try { + const existingMahasiswa = await mahasiswaRepository.findByEmail(mahasiswaData.email); + + if (existingMahasiswa) { + return error(new BadRequestError('Email Sudah Terdaftar')); + } + + if (!mahasiswaData.role) { + mahasiswaData.role = 'student'; + } + + const result = await mahasiswaRepository.create(mahasiswaData); + return data(result); + } catch (err) { + return error(err); + } + } + + async getAllMahasiswa({ page = 1, limit = 10 }) { + try { + const offset = (page - 1) * limit; + + const mahasiswa = await mahasiswaRepository.findAll({ offset, limit }); + + if (!mahasiswa || mahasiswa.length === 0) { + return error(new NotFoundError('Tidak ada mahasiswa ditemukan')); + } + + const total = await mahasiswaRepository.countAllMahasiswa(); + const totalPages = Math.ceil(total / limit); + const meta = { page: Number(page), limit: Number(limit), total, totalPages }; + return paginationData(mahasiswa, meta); + } catch (err) { + return error(err); + } + } + + async getMahasiswaById(id) { + try { + const mahasiswa = await mahasiswaRepository.findById(id); + + if (!mahasiswa) { + return error(new NotFoundError('Mahasiswa Tidak Ditemukan')); + } + + return data(mahasiswa); + } catch (err) { + return error(err); + } + } + + async getMahasiswaByEmail(email) { + try { + const mahasiswa = await mahasiswaRepository.findByEmail(email); + + if (!mahasiswa) { + return error(new NotFoundError('Mahasiswa Tidak Ditemukan')); + } + + return data(mahasiswa); + } catch (err) { + return error(err); + } + } + + async updateMahasiswa(id, mahasiswaData) { + try { + const checkResult = await this.getMahasiswaById(id); + if (checkResult.err) { + return checkResult; + } + + if (mahasiswaData.email) { + const existingMahasiswa = await mahasiswaRepository.findByEmail(mahasiswaData.email); + + if (existingMahasiswa && existingMahasiswa.id !== id) { + return error(new BadRequestError('Email Sudah Terdaftar')); + } + } + + const result = await mahasiswaRepository.updateById(id, mahasiswaData); + return data(result); + } catch (err) { + return error(err); + } + } + + async deleteMahasiswa(id) { + try { + const checkResult = await this.getMahasiswaById(id); + if (checkResult.err) { + return checkResult; + } + + const result = await mahasiswaRepository.deleteById(id); + return data(result); + } catch (err) { + return error(err); + } + } + + async deleteAllMahasiswa() { + try { + const result = await mahasiswaRepository.deleteAll(); + return data(result); + } catch (err) { + return error(err); + } + } +} + +module.exports = new MahasiswaService(); diff --git a/src/modules/partnership/controllers/partnership.controller.js b/src/modules/partnership/controllers/partnership.controller.js new file mode 100755 index 0000000..148d4ac --- /dev/null +++ b/src/modules/partnership/controllers/partnership.controller.js @@ -0,0 +1,128 @@ +const partnershipService = require('../services/partnership.service'); +const { response, error } = 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 { createPartnershipModel, updatePartnershipModel } = require('../models/partnership.model'); +const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper'); + +class PartnershipController { + async addPartnership(req, res) { + try { + const image = req.file; + + const validatePayload = isValidPayload(req.body, createPartnershipModel); + + 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 { nama_partner } = validatePayload.data; + const result = await partnershipService.createPartnership(nama_partner, image); + + if (result.err) { + return response(res, 'fail', result); + } + + return response(res, 'success', result, 'Data partnership 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 getPartnership(req, res) { + try { + const result = await partnershipService.getAllPartnerships(); + + if (result.err) { + return response(res, 'fail', result); + } + + return response(res, 'success', result, 'Data partnership berhasil diambil', SUCCESS.OK); + } catch (err) { + return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR); + } + } + + async getPartnershipById(req, res) { + try { + const { id } = req.params; + + const result = await partnershipService.getPartnershipById(id); + + if (result.err) { + return response(res, 'fail', result); + } + + return response(res, 'success', result, 'Data partnership berhasil diambil', SUCCESS.OK); + } catch (err) { + return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR); + } + } + + async updatePartnership(req, res) { + try { + const { id } = req.params; + const image = req.file; + + const validatePayload = isValidPayload(req.body, updatePartnershipModel); + + 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 { nama_partner } = validatePayload.data; + const result = await partnershipService.updatePartnership(id, nama_partner, image); + + if (result.err) { + return response(res, 'fail', result); + } + + return response(res, 'success', result, 'Data partnership 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 deletePartnership(req, res) { + try { + const { id } = req.params; + + const result = await partnershipService.deletePartnership(id); + + if (result.err) { + return response(res, 'fail', result); + } + + return response(res, 'success', result, 'Data partnership 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 PartnershipController(); diff --git a/src/modules/partnership/index.js b/src/modules/partnership/index.js new file mode 100755 index 0000000..98018cb --- /dev/null +++ b/src/modules/partnership/index.js @@ -0,0 +1,9 @@ +const partnershipController = require('./controllers/partnership.controller'); +const partnershipService = require('./services/partnership.service'); +const partnershipRepository = require('./repositories/partnership.repository'); + +module.exports = { + partnershipController, + partnershipService, + partnershipRepository +}; diff --git a/src/modules/partnership/models/partnership.model.js b/src/modules/partnership/models/partnership.model.js new file mode 100755 index 0000000..3c97075 --- /dev/null +++ b/src/modules/partnership/models/partnership.model.js @@ -0,0 +1,30 @@ +const joi = require('joi'); + +const createPartnershipModel = joi.object().keys({ + nama_partner: joi.string().max(255).required().messages({ + 'string.base': 'Nama partner harus berupa teks.', + 'string.empty': 'Nama partner tidak boleh kosong.', + 'string.max': 'Nama partner maksimal {#limit} karakter.', + 'any.required': 'Nama partner wajib diisi.', + }), + + image_path: joi.string().optional().allow('', null).messages({ + 'string.base': 'Path gambar harus berupa teks.', + }), +}); + +const updatePartnershipModel = joi.object().keys({ + nama_partner: joi.string().max(255).optional().messages({ + 'string.base': 'Nama partner harus berupa teks.', + 'string.max': 'Nama partner maksimal {#limit} karakter.', + }), + + image_path: joi.string().optional().allow('', null).messages({ + 'string.base': 'Path gambar harus berupa teks.', + }), +}); + +module.exports = { + createPartnershipModel, + updatePartnershipModel, +}; diff --git a/src/modules/partnership/repositories/partnership.repository.js b/src/modules/partnership/repositories/partnership.repository.js new file mode 100755 index 0000000..4bccdf1 --- /dev/null +++ b/src/modules/partnership/repositories/partnership.repository.js @@ -0,0 +1,34 @@ +const prisma = require('../../../helpers/db/db_connection'); + +class PartnershipRepository { + async create(partnershipData) { + return prisma.partnership.create({ + data: partnershipData, + }); + } + + async findAll() { + return prisma.partnership.findMany(); + } + + async findById(id) { + return prisma.partnership.findUnique({ + where: { id: parseInt(id) }, + }); + } + + async updateById(id, partnershipData) { + return prisma.partnership.update({ + where: { id: parseInt(id) }, + data: partnershipData, + }); + } + + async deleteById(id) { + return prisma.partnership.delete({ + where: { id: parseInt(id) }, + }); + } +} + +module.exports = new PartnershipRepository(); diff --git a/src/modules/partnership/services/partnership.service.js b/src/modules/partnership/services/partnership.service.js new file mode 100755 index 0000000..563c11a --- /dev/null +++ b/src/modules/partnership/services/partnership.service.js @@ -0,0 +1,121 @@ +const partnershipRepository = require('../repositories/partnership.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 PartnershipService { + async createPartnership(nama_partner, imageFile) { + try { + const imagePath = imageUploadHelper.uploadImage(imageFile); + + const partnershipData = { + nama_partner, + image_path: imagePath, + }; + + const result = await partnershipRepository.create(partnershipData); + 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 getAllPartnerships() { + try { + const partnerships = await partnershipRepository.findAll(); + return data(partnerships); + } catch (err) { + return error(err); + } + } + + async getPartnershipById(id) { + try { + const partnership = await partnershipRepository.findById(id); + + if (!partnership) { + return error(new NotFoundError('Partnership Tidak Ditemukan')); + } + + return data(partnership); + } catch (err) { + return error(err); + } + } + + async updatePartnership(id, nama_partner, imageFile) { + let oldImagePath = null; + let newImageUploaded = false; + + try { + const checkResult = await this.getPartnershipById(id); + if (checkResult.err) { + return checkResult; + } + + const existingPartnership = checkResult.data; + let imagePath = existingPartnership.image_path; + + if (imageFile) { + oldImagePath = existingPartnership.image_path; + + imagePath = imageUploadHelper.uploadImage(imageFile); + newImageUploaded = true; + + if (oldImagePath) { + const oldFileName = path.basename(oldImagePath); + await deleteFileIfExists(oldFileName); + } + } + + const partnershipData = { + nama_partner, + image_path: imagePath, + }; + + const result = await partnershipRepository.updateById(id, partnershipData); + 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 deletePartnership(id) { + try { + const checkResult = await this.getPartnershipById(id); + if (checkResult.err) { + return checkResult; + } + + const partnership = checkResult.data; + + if (partnership.image_path) { + const fileName = path.basename(partnership.image_path); + await deleteFileIfExists(fileName); + } + + const result = await partnershipRepository.deleteById(id); + return data(result); + } catch (err) { + return error(err); + } + } +} + +module.exports = new PartnershipService(); diff --git a/src/routes/admin.routes.js b/src/routes/admin.routes.js new file mode 100755 index 0000000..9f5b324 --- /dev/null +++ b/src/routes/admin.routes.js @@ -0,0 +1,91 @@ +const express = require('express'); +const { adminController } = require('../modules/admin'); +const { verifyJWT } = require('../middleware/verifyJWT'); +const isAdmin = require('../middleware/isAdmin'); + +const router = express.Router(); + +/** + * @swagger + * components: + * schemas: + * Admin: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * nama_depan: + * type: string + * example: "John" + * nama_belakang: + * type: string + * example: "Doe" + * email: + * type: string + * example: "admin@example.com" + * role: + * type: string + * example: "admin" + */ + +/** + * @swagger + * /admin-api/add: + * post: + * summary: Create a new admin account + * tags: [Admin] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * nama_depan: + * type: string + * example: "John" + * nama_belakang: + * type: string + * example: "Doe" + * email: + * type: string + * example: "admin@example.com" + * password: + * type: string + * example: "password123" + * responses: + * 200: + * description: Admin created successfully + * 400: + * description: Email already exists or unauthorized access + * 500: + * description: Internal server error + */ +router.post("/add", verifyJWT, isAdmin, adminController.addAdmin); + +/** + * @swagger + * /admin-api/get: + * get: + * summary: Retrieve all admin account + * tags: [Admin] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Successfully retrieved all admins + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/Admin' + * 500: + * description: Internal server error + */ +router.get("/get", verifyJWT, adminController.getAdmins); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/auth.routes.js b/src/routes/auth.routes.js new file mode 100755 index 0000000..6b39d98 --- /dev/null +++ b/src/routes/auth.routes.js @@ -0,0 +1,95 @@ +const express = require('express'); +const { authController } = require('../modules/auth'); +const { verifyJWT } = require('../middleware/verifyJWT'); + +const router = express.Router(); + +/** + * @swagger + * /auth-api/login: + * post: + * summary: Admin login + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * email: + * type: string + * example: "admin@example.com" + * password: + * type: string + * example: "password123" + * responses: + * 200: + * description: Login successful + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Login successful" + * token: + * type: string + * example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + * 400: + * description: Invalid email or password + * 500: + * description: Internal server error + */ +router.post("/login", authController.login); + +/** + * @swagger + * /auth-api/me: + * get: + * summary: Get authenticated admin details + * tags: [Auth] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Get admin details successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Get admin successfully" + * data: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * nama_depan: + * type: string + * example: "John" + * nama_belakang: + * type: string + * example: "Doe" + * email: + * type: string + * example: "admin@example.com" + * role: + * type: string + * example: "admin" + * 401: + * description: Authorization header missing or invalid + * 403: + * description: Invalid or expired token + * 404: + * description: Admin not found + * 500: + * description: Internal server error + */ +router.get("/me", verifyJWT, authController.me); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/auth.ts b/src/routes/auth.ts deleted file mode 100644 index 6b8d6cb..0000000 --- a/src/routes/auth.ts +++ /dev/null @@ -1,432 +0,0 @@ -import { Router, Request, Response, NextFunction } from 'express'; -import bcrypt from 'bcryptjs'; -import jwt from 'jsonwebtoken'; -import prisma from '../lib/prisma'; -import { protect, AuthenticatedRequest } from '../middleware/auth'; -import { CustomError } from '../middleware/errorHandler'; - -const router = Router(); - -const generateToken = (payload: { id: string; email: string; role: string }) => { - return jwt.sign( - payload, - process.env.JWT_SECRET || 'internify_secret_key_2026_dev', - { expiresIn: (process.env.JWT_EXPIRES_IN || '7d') as any } - ); -}; - -/** - * @swagger - * /api/auth/register: - * post: - * summary: Registrasi akun mahasiswa baru - * description: Membuat akun pengguna baru dengan role `STUDENT` beserta profil mahasiswa. - * tags: [Auth] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - email - * - password - * - namaDepan - * - kontak - * - jurusan - * - universitas - * - negara - * properties: - * email: - * type: string - * example: student@example.com - * password: - * type: string - * example: Student123! - * namaDepan: - * type: string - * example: Alya - * namaBelakang: - * type: string - * example: Putri - * kontak: - * type: string - * example: 08123456789 - * jurusan: - * type: string - * example: Informatika - * universitas: - * type: string - * example: Telkom University - * negara: - * type: string - * example: Indonesia - * cvPath: - * type: string - * example: /uploads/docs/cv.pdf - * portofolioPath: - * type: string - * example: /uploads/docs/portofolio.pdf - * motivasi: - * type: string - * example: Saya tertarik mengikuti program ini. - * relevantSkills: - * type: string - * example: React, Node.js - * responses: - * 201: - * description: Registrasi mahasiswa berhasil - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * example: true - * message: - * type: string - * example: Registrasi mahasiswa berhasil - * data: - * type: object - * properties: - * id: - * type: string - * example: cku3xq2v0000xk8w7a1b2c3d4 - * email: - * type: string - * example: student@example.com - * role: - * type: string - * example: STUDENT - * createdAt: - * type: string - * format: date-time - * example: 2026-06-02T10:00:00.000Z - * 400: - * description: Validasi registrasi gagal - * content: - * application/json: - * examples: - * field_wajib_kosong: - * value: - * status: error - * statusCode: 400 - * message: Mohon lengkapi semua field wajib yang diperlukan - * email_sudah_terdaftar: - * value: - * status: error - * statusCode: 400 - * message: Email sudah terdaftar - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.post('/register', async (req: Request, res: Response, next: NextFunction) => { - try { - const { - email, - password, - namaDepan, - namaBelakang, - kontak, - jurusan, - universitas, - negara, - cvPath, - portofolioPath, - motivasi, - relevantSkills, - } = req.body; - - if (!email || !password || !namaDepan || !kontak || !jurusan || !universitas || !negara) { - const error: CustomError = new Error('Mohon lengkapi semua field wajib yang diperlukan'); - error.statusCode = 400; - return next(error); - } - - const existingUser = await prisma.user.findUnique({ - where: { email }, - }); - - if (existingUser) { - const error: CustomError = new Error('Email sudah terdaftar'); - error.statusCode = 400; - return next(error); - } - - const salt = await bcrypt.genSalt(10); - const passwordHash = await bcrypt.hash(password, salt); - - const newUser = await prisma.$transaction(async (tx) => { - const user = await tx.user.create({ - data: { - email, - passwordHash, - role: 'STUDENT', - }, - }); - - await tx.mahasiswa.create({ - data: { - idUser: user.id, - namaDepan, - namaBelakang: namaBelakang || null, - kontak, - jurusan, - universitas, - negara, - cvPath: cvPath || '', - portofolioPath: portofolioPath || '', - motivasi: motivasi || '', - relevantSkills: relevantSkills || '', - }, - }); - - return user; - }); - - res.status(201).json({ - success: true, - message: 'Registrasi mahasiswa berhasil', - data: { - id: newUser.id, - email: newUser.email, - role: newUser.role, - createdAt: newUser.createdAt, - }, - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/auth/login: - * post: - * summary: Login pengguna dan mendapatkan token JWT - * description: Melakukan autentikasi pengguna (admin/mahasiswa) dan mengembalikan token akses. - * tags: [Auth] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - email - * - password - * properties: - * email: - * type: string - * example: admin@internify.com - * password: - * type: string - * example: AdminInternify123! - * responses: - * 200: - * description: Login berhasil - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * example: true - * data: - * type: object - * properties: - * token: - * type: string - * example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... - * user: - * type: object - * properties: - * id: - * type: string - * example: cku3xq2v0000xk8w7a1b2c3d4 - * email: - * type: string - * example: admin@internify.com - * role: - * type: string - * example: ADMIN - * profile: - * type: object - * description: Data profil admin atau mahasiswa - * 401: - * description: Autentikasi gagal - * content: - * application/json: - * example: - * status: error - * statusCode: 401 - * message: Email atau password salah - * 400: - * description: Validasi login gagal - * content: - * application/json: - * example: - * status: error - * statusCode: 400 - * message: Mohon masukkan email dan password - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.post('/login', async (req: Request, res: Response, next: NextFunction) => { - try { - const { email, password } = req.body; - - if (!email || !password) { - const error: CustomError = new Error('Mohon masukkan email dan password'); - error.statusCode = 400; - return next(error); - } - - const user = await prisma.user.findUnique({ - where: { email }, - include: { - admin: true, - mahasiswa: true, - }, - }); - - if (!user) { - const error: CustomError = new Error('Email atau password salah'); - error.statusCode = 401; - return next(error); - } - - const isMatch = await bcrypt.compare(password, user.passwordHash); - if (!isMatch) { - const error: CustomError = new Error('Email atau password salah'); - error.statusCode = 401; - return next(error); - } - - const profile = user.role === 'ADMIN' ? user.admin : user.mahasiswa; - - const token = generateToken({ - id: user.id, - email: user.email, - role: user.role, - }); - - res.status(200).json({ - success: true, - data: { - token, - user: { - id: user.id, - email: user.email, - role: user.role, - profile, - }, - }, - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/auth/me: - * get: - * summary: Ambil profil pengguna yang sedang login - * description: Mengembalikan informasi ringkas user berdasarkan token JWT aktif. - * tags: [Auth] - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: Profil pengguna berhasil diambil - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * data: - * type: object - * properties: - * id: - * type: string - * example: cku3xq2v0000xk8w7a1b2c3d4 - * email: - * type: string - * example: admin@internify.com - * role: - * type: string - * example: ADMIN - * nama_depan: - * type: string - * example: Super - * 401: - * description: Token tidak valid atau tidak tersedia - * content: - * application/json: - * examples: - * token_missing: - * value: - * status: error - * statusCode: 401 - * message: Not authorized to access this route, token is missing - * token_invalid: - * value: - * status: error - * statusCode: 401 - * message: Not authorized to access this route, token is invalid or expired - * user_token_tidak_ada: - * value: - * status: error - * statusCode: 401 - * message: User belonging to this token no longer exists - * 404: - * description: Pengguna tidak ditemukan - * content: - * application/json: - * example: - * status: error - * statusCode: 404 - * message: User not found - */ -router.get('/me', protect, async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - try { - const userId = req.user?.id; - - const user = await prisma.user.findUnique({ - where: { id: userId }, - include: { - admin: true, - mahasiswa: true, - }, - }); - - if (!user) { - const error: CustomError = new Error('User not found'); - error.statusCode = 404; - return next(error); - } - - const profile = user.role === 'ADMIN' ? user.admin : user.mahasiswa; - const namaDepan = profile?.namaDepan || ''; - - res.status(200).json({ - status: true, - data: { - id: user.id, - email: user.email, - role: user.role, - nama_depan: namaDepan - }, - }); - } catch (error) { - next(error); - } -}); - -export default router; diff --git a/src/routes/batch.routes.js b/src/routes/batch.routes.js new file mode 100755 index 0000000..e8b97f6 --- /dev/null +++ b/src/routes/batch.routes.js @@ -0,0 +1,336 @@ +const express = require('express'); +const { batchController } = require('../modules/batch'); +const { verifyJWT } = require('../middleware/verifyJWT'); +const isAdmin = require('../middleware/isAdmin'); + +const router = express.Router(); + +/** + * @swagger + * components: + * schemas: + * Batch: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * batch_number: + * type: integer + * example: 1 + * is_active: + * type: boolean + * example: true + * created_at: + * type: string + * format: date-time + * example: "2025-01-01T00:00:00.000Z" + * updated_at: + * type: string + * format: date-time + * example: "2025-01-01T00:00:00.000Z" + * BatchCreate: + * type: object + * properties: + * batch_number: + * type: integer + * description: Opsional - Nomor batch akan di-generate otomatis (batch terakhir + 1) jika tidak disediakan + * example: 1 + * is_active: + * type: boolean + * description: Opsional - Batch baru akan otomatis aktif dan menonaktifkan batch sebelumnya + * example: true + * BatchUpdate: + * type: object + * properties: + * batch_number: + * type: integer + * example: 2 + * is_active: + * type: boolean + * example: true + */ + +/** + * @swagger + * tags: + * name: Batch + * description: API untuk mengelola batch magang + */ + +/** + * @swagger + * /batch-api/add: + * post: + * summary: Membuat batch baru (auto-increment + auto-deactivate batch sebelumnya) + * description: Membuat batch baru dengan nomor batch otomatis (batch terakhir + 1), mengaktifkan batch baru, dan menonaktifkan semua batch sebelumnya + * tags: [Batch] + * security: + * - bearerAuth: [] + * requestBody: + * required: false + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/BatchCreate' + * examples: + * auto: + * summary: Auto-generate batch (recommended) + * value: {} + * manual: + * summary: Manual batch number (optional) + * value: + * batch_number: 5 + * responses: + * 201: + * description: Batch berhasil dibuat + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "Batch berhasil dibuat" + * data: + * $ref: '#/components/schemas/Batch' + * examples: + * success: + * value: + * success: true + * message: "Batch berhasil dibuat" + * data: + * id: 1 + * batch_number: 6 + * is_active: true + * created_at: "2025-11-17T00:00:00.000Z" + * updated_at: "2025-11-17T00:00:00.000Z" + * 400: + * description: Data tidak valid + * 401: + * description: Tidak terautentikasi + * 403: + * description: Tidak memiliki akses (bukan admin) + * 409: + * description: Nomor batch sudah digunakan (jika manual) + */ +router.post('/add', verifyJWT, isAdmin, batchController.createBatch); + +/** + * @swagger + * /batch-api/: + * get: + * summary: Mengambil semua batch + * description: Mendapatkan daftar semua batch yang tersedia, diurutkan berdasarkan nomor batch (descending) + * tags: [Batch] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Berhasil mengambil semua batch + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "Berhasil mengambil semua batch" + * data: + * type: array + * items: + * $ref: '#/components/schemas/Batch' + * examples: + * success: + * value: + * success: true + * message: "Berhasil mengambil semua batch" + * data: + * - id: 3 + * batch_number: 3 + * is_active: true + * created_at: "2025-11-17T00:00:00.000Z" + * updated_at: "2025-11-17T00:00:00.000Z" + * - id: 2 + * batch_number: 2 + * is_active: false + * created_at: "2025-11-16T00:00:00.000Z" + * updated_at: "2025-11-17T00:00:00.000Z" + * - id: 1 + * batch_number: 1 + * is_active: false + * created_at: "2025-11-15T00:00:00.000Z" + * updated_at: "2025-11-16T00:00:00.000Z" + * 401: + * description: Tidak terautentikasi + * 403: + * description: Tidak memiliki akses (bukan admin) + */ +router.get('/', verifyJWT, isAdmin, batchController.getAllBatches); + +/** + * @swagger + * /batch-api/{id}: + * put: + * summary: Memperbarui batch + * description: Memperbarui data batch. Jika is_active diset true, semua batch lain akan otomatis dinonaktifkan + * tags: [Batch] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * schema: + * type: integer + * required: true + * description: ID batch + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/BatchUpdate' + * examples: + * activate: + * summary: Aktifkan batch + * value: + * is_active: true + * deactivate: + * summary: Nonaktifkan batch + * value: + * is_active: false + * update_number: + * summary: Update nomor batch + * value: + * batch_number: 3 + * responses: + * 200: + * description: Batch berhasil diperbarui + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "Batch berhasil diperbarui" + * data: + * $ref: '#/components/schemas/Batch' + * 400: + * description: Data tidak valid + * 401: + * description: Tidak terautentikasi + * 403: + * description: Tidak memiliki akses (bukan admin) + * 404: + * description: Batch tidak ditemukan + * 409: + * description: Nomor batch sudah digunakan + */ +router.put('/:id', verifyJWT, isAdmin, batchController.updateBatch); + +/** + * @swagger + * /batch-api/{id}: + * delete: + * summary: Menghapus batch + * tags: [Batch] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * schema: + * type: integer + * required: true + * description: ID batch + * responses: + * 200: + * description: Batch berhasil dihapus + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "Batch berhasil dihapus" + * data: + * $ref: '#/components/schemas/Batch' + * 401: + * description: Tidak terautentikasi + * 403: + * description: Tidak memiliki akses (bukan admin) + * 404: + * description: Batch tidak ditemukan + */ +router.delete('/:id', verifyJWT, isAdmin, batchController.deleteBatch); + +/** + * @swagger + * /batch-api/switch/{batch_number}: + * patch: + * summary: Mengaktifkan batch yang sudah ada (switch batch) + * description: Menonaktifkan semua batch dan mengaktifkan batch dengan nomor yang ditentukan + * tags: [Batch] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: batch_number + * schema: + * type: integer + * required: true + * description: Nomor batch yang akan diaktifkan + * example: 3 + * responses: + * 200: + * description: Batch berhasil diaktifkan + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "Batch berhasil diaktifkan" + * data: + * $ref: '#/components/schemas/Batch' + * examples: + * success: + * value: + * success: true + * message: "Batch berhasil diaktifkan" + * data: + * id: 3 + * batch_number: 3 + * is_active: true + * created_at: "2025-11-17T00:00:00.000Z" + * updated_at: "2025-11-17T00:00:00.000Z" + * 400: + * description: Nomor batch tidak valid + * 401: + * description: Tidak terautentikasi + * 403: + * description: Tidak memiliki akses (bukan admin) + * 404: + * description: Batch tidak ditemukan + */ +router.patch('/switch/:batch_number', verifyJWT, isAdmin, batchController.switchBatch); + +module.exports = router; diff --git a/src/routes/faq.routes.js b/src/routes/faq.routes.js new file mode 100755 index 0000000..1181a27 --- /dev/null +++ b/src/routes/faq.routes.js @@ -0,0 +1,254 @@ +const express = require('express'); +const { faqController } = require('../modules/faq'); +const { verifyJWT } = require('../middleware/verifyJWT'); +const isAdmin = require('../middleware/isAdmin'); + +const router = express.Router(); + +/** + * @swagger + * components: + * schemas: + * FAQ: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * pertanyaan: + * type: string + * example: "Bagaimana cara mendaftar magang?" + * jawaban: + * type: string + * example: "Anda dapat mendaftar melalui halaman lowongan magang yang tersedia." + * created_at: + * type: string + * format: date-time + * example: "2025-11-10T10:00:00Z" + */ + +/** + * @swagger + * /faq-api/get: + * get: + * summary: Get all FAQ + * tags: [FAQ] + * responses: + * 200: + * description: Successfully retrieved all FAQ + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * type: array + * items: + * $ref: '#/components/schemas/FAQ' + * message: + * type: string + * example: "Success" + * code: + * type: integer + * example: 200 + * 404: + * description: No FAQ found + * 500: + * description: Internal server error + */ +router.get('/get', faqController.getAllFaq); + +/** + * @swagger + * /faq-api/get/{id}: + * get: + * summary: Get FAQ by ID + * tags: [FAQ] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The FAQ ID + * responses: + * 200: + * description: Successfully retrieved FAQ + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * $ref: '#/components/schemas/FAQ' + * message: + * type: string + * example: "Success" + * code: + * type: integer + * example: 200 + * 404: + * description: FAQ not found + * 500: + * description: Internal server error + */ +router.get('/get/:id', faqController.getFaqById); + +/** + * @swagger + * /faq-api/add: + * post: + * summary: Create a new FAQ + * tags: [FAQ] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - pertanyaan + * - jawaban + * properties: + * pertanyaan: + * type: string + * example: "Bagaimana cara mendaftar magang?" + * jawaban: + * type: string + * example: "Anda dapat mendaftar melalui halaman lowongan magang yang tersedia." + * responses: + * 200: + * description: FAQ created successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * $ref: '#/components/schemas/FAQ' + * message: + * type: string + * example: "Success" + * code: + * type: integer + * example: 200 + * 417: + * description: Validation error + * 500: + * description: Internal server error + */ +router.post('/add', verifyJWT, isAdmin, faqController.addFaq); + +/** + * @swagger + * /faq-api/update/{id}: + * patch: + * summary: Update an existing FAQ + * tags: [FAQ] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The FAQ ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * pertanyaan: + * type: string + * example: "Bagaimana cara mendaftar magang?" + * jawaban: + * type: string + * example: "Anda dapat mendaftar melalui halaman lowongan magang yang tersedia." + * responses: + * 200: + * description: FAQ updated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * $ref: '#/components/schemas/FAQ' + * message: + * type: string + * example: "Success" + * code: + * type: integer + * example: 200 + * 404: + * description: FAQ not found + * 417: + * description: Validation error + * 500: + * description: Internal server error + */ +router.patch('/update/:id', verifyJWT, isAdmin, faqController.updateFaq); + +/** + * @swagger + * /faq-api/delete/{id}: + * delete: + * summary: Delete an FAQ + * tags: [FAQ] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The FAQ ID + * responses: + * 200: + * description: FAQ deleted successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * type: object + * properties: + * message: + * type: string + * example: "FAQ berhasil dihapus" + * message: + * type: string + * example: "Success" + * code: + * type: integer + * example: 200 + * 404: + * description: FAQ not found + * 500: + * description: Internal server error + */ +router.delete('/delete/:id', verifyJWT, isAdmin, faqController.deleteFaq); + +module.exports = router; diff --git a/src/routes/faq.ts b/src/routes/faq.ts deleted file mode 100644 index 43d3390..0000000 --- a/src/routes/faq.ts +++ /dev/null @@ -1,352 +0,0 @@ -import { Router, Request, Response, NextFunction } from 'express'; -import { protect, restrictTo, AuthenticatedRequest } from '../middleware/auth'; -import prisma from '../lib/prisma'; -import { CustomError } from '../middleware/errorHandler'; - -const router = Router(); - -// Map data faq dari DB ke format frontend -function mapToFrontend(faq: any) { - return { - id: faq.id, - pertanyaan: faq.pertanyaan, - jawaban: faq.jawaban, - created_at: faq.createdAt - }; -} - -/** - * @swagger - * /api/faq-api/get: - * get: - * summary: Ambil seluruh FAQ - * description: Mengembalikan daftar semua pertanyaan dan jawaban FAQ. - * tags: [FAQ] - * responses: - * 200: - * description: Data FAQ berhasil diambil - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * data: - * type: array - * items: - * type: object - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.get('/get', async (_req: Request, res: Response, next: NextFunction) => { - try { - const faqs = await prisma.faq.findMany({ - orderBy: { createdAt: 'desc' } - }); - res.status(200).json({ - status: true, - data: faqs.map(mapToFrontend) - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/faq-api/get/{id}: - * get: - * summary: Ambil detail FAQ berdasarkan ID - * description: Mengembalikan detail satu item FAQ. - * tags: [FAQ] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * description: ID FAQ - * responses: - * 200: - * description: Detail FAQ berhasil diambil - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * data: - * type: object - * 404: - * description: FAQ tidak ditemukan - * content: - * application/json: - * example: - * status: error - * statusCode: 404 - * message: FAQ tidak ditemukan - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.get('/get/:id', async (req: Request, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const faq = await prisma.faq.findUnique({ - where: { id: parseInt(id, 10) } - }); - - if (!faq) { - const error: CustomError = new Error('FAQ tidak ditemukan'); - error.statusCode = 404; - return next(error); - } - - res.status(200).json({ - status: true, - data: mapToFrontend(faq) - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/faq-api/add: - * post: - * summary: Tambah FAQ baru - * description: Menambahkan item FAQ baru. Hanya dapat diakses oleh admin. - * tags: [FAQ] - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - pertanyaan - * - jawaban - * properties: - * pertanyaan: - * type: string - * example: Apakah magang ini bisa remote? - * jawaban: - * type: string - * example: Ya, beberapa posisi mendukung skema remote. - * responses: - * 201: - * description: FAQ berhasil ditambahkan - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * message: - * type: string - * example: FAQ berhasil ditambahkan - * 400: - * description: Validasi FAQ gagal - * content: - * application/json: - * example: - * status: error - * statusCode: 400 - * message: Pertanyaan dan jawaban harus diisi - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 403: - * $ref: '#/components/responses/ForbiddenError' - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.post('/add', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - try { - const { pertanyaan, jawaban } = req.body; - - if (!pertanyaan || !jawaban) { - const error: CustomError = new Error('Pertanyaan dan jawaban harus diisi'); - error.statusCode = 400; - return next(error); - } - - const faq = await prisma.faq.create({ - data: { - pertanyaan, - jawaban - } - }); - - res.status(201).json({ - status: true, - message: 'FAQ berhasil ditambahkan', - data: mapToFrontend(faq) - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/faq-api/update/{id}: - * patch: - * summary: Perbarui FAQ - * description: Memperbarui item FAQ berdasarkan ID. Hanya dapat diakses oleh admin. - * tags: [FAQ] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * description: ID FAQ - * requestBody: - * required: false - * content: - * application/json: - * schema: - * type: object - * properties: - * pertanyaan: - * type: string - * jawaban: - * type: string - * responses: - * 200: - * description: FAQ berhasil diperbarui - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * message: - * type: string - * example: FAQ berhasil diupdate - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 403: - * $ref: '#/components/responses/ForbiddenError' - * 404: - * description: FAQ tidak ditemukan - * content: - * application/json: - * example: - * status: error - * statusCode: 404 - * message: FAQ tidak ditemukan - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.patch('/update/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const { pertanyaan, jawaban } = req.body; - - const existing = await prisma.faq.findUnique({ - where: { id: parseInt(id, 10) } - }); - - if (!existing) { - const error: CustomError = new Error('FAQ tidak ditemukan'); - error.statusCode = 404; - return next(error); - } - - const updated = await prisma.faq.update({ - where: { id: parseInt(id, 10) }, - data: { - pertanyaan: pertanyaan ?? existing.pertanyaan, - jawaban: jawaban ?? existing.jawaban - } - }); - - res.status(200).json({ - status: true, - message: 'FAQ berhasil diupdate', - data: mapToFrontend(updated) - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/faq-api/delete/{id}: - * delete: - * summary: Hapus FAQ - * description: Menghapus item FAQ berdasarkan ID. Hanya dapat diakses oleh admin. - * tags: [FAQ] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * description: ID FAQ - * responses: - * 200: - * description: FAQ berhasil dihapus - * content: - * application/json: - * example: - * status: true - * message: FAQ berhasil dihapus - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 403: - * $ref: '#/components/responses/ForbiddenError' - * 404: - * description: FAQ tidak ditemukan - * content: - * application/json: - * example: - * status: error - * statusCode: 404 - * message: FAQ tidak ditemukan - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const existing = await prisma.faq.findUnique({ - where: { id: parseInt(id, 10) } - }); - - if (!existing) { - const error: CustomError = new Error('FAQ tidak ditemukan'); - error.statusCode = 404; - return next(error); - } - - await prisma.faq.delete({ - where: { id: parseInt(id, 10) } - }); - - res.status(200).json({ - status: true, - message: 'FAQ berhasil dihapus' - }); - } catch (error) { - next(error); - } -}); - -export default router; diff --git a/src/routes/feedback.routes.js b/src/routes/feedback.routes.js new file mode 100755 index 0000000..2153178 --- /dev/null +++ b/src/routes/feedback.routes.js @@ -0,0 +1,254 @@ +const express = require('express'); +const { feedbackController } = require('../modules/feedback'); +const { verifyJWT } = require('../middleware/verifyJWT'); +const isAdmin = require('../middleware/isAdmin'); +const multer = require('../middleware/multer'); +const { multerErrorHandler } = require('../middleware/multer'); + +const router = express.Router(); + +/** + * @swagger + * components: + * schemas: + * Feedback: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * nama: + * type: string + * example: "John Doe" + * universitas: + * type: string + * example: "Universitas Indonesia" + * pesan: + * type: string + * example: "Great internship experience!" + * batch: + * type: integer + * example: 3 + * posisi: + * type: string + * example: "Back End Developer" + * tahun: + * type: integer + * example: 2025 + * image_path: + * type: string + * example: "/uploads/feedback_image.png" + * created_at: + * type: string + * format: date-time + * example: "2025-01-15T10:00:00Z" + */ + +/** + * @swagger + * /feedback-api/add: + * post: + * summary: Add a new feedback + * tags: [Feedback] + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * nama: + * type: string + * description: Name of the person giving feedback + * example: "John Doe" + * universitas: + * type: string + * description: University name + * example: "Universitas Indonesia" + * pesan: + * type: string + * description: Feedback message + * example: "Great experience during the internship" + * batch: + * type: integer + * description: Batch number + * example: 3 + * posisi: + * type: string + * description: Internship position + * example: "Back End Developer" + * tahun: + * type: integer + * description: Year + * example: 2025 + * image: + * type: string + * format: binary + * description: Optional feedback image + * responses: + * 201: + * description: Feedback created successfully + * 413: + * description: File size too large (max 5MB) + * 417: + * description: Validation error + * 500: + * description: Internal server error + */ +router.post('/add', multer.single('image'), multerErrorHandler, feedbackController.addFeedback); + +/** + * @swagger + * /feedback-api/get: + * get: + * summary: Get all feedback with optional filters + * tags: [Feedback] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: batch + * schema: + * type: integer + * minimum: 1 + * description: Filter feedback by batch number + * example: 3 + * - in: query + * name: tahun + * schema: + * type: integer + * minimum: 2000 + * maximum: 2100 + * description: Filter feedback by year + * example: 2025 + * responses: + * 200: + * description: Successfully retrieved feedback + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Berhasil mengambil semua feedback" + * data: + * type: array + * items: + * $ref: '#/components/schemas/Feedback' + * 417: + * description: Query parameter validation error + * 500: + * description: Internal server error + */ +router.get('/get', feedbackController.getAllFeedback); + +/** + * @swagger + * /feedback-api/get/{id}: + * get: + * summary: Get feedback by ID + * tags: [Feedback] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Feedback ID + * responses: + * 200: + * description: Successfully retrieved feedback + * 404: + * description: Feedback not found + * 500: + * description: Internal server error + */ +router.get('/get/:id', verifyJWT, isAdmin, feedbackController.getFeedbackById); + +/** + * @swagger + * /feedback-api/update/{id}: + * patch: + * summary: Update feedback by ID + * tags: [Feedback] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Feedback ID + * requestBody: + * required: false + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * nama: + * type: string + * example: "John Doe" + * universitas: + * type: string + * example: "Universitas Indonesia" + * pesan: + * type: string + * example: "Updated feedback message" + * batch: + * type: integer + * example: 3 + * posisi: + * type: string + * example: "Back End Developer" + * tahun: + * type: integer + * example: 2025 + * image: + * type: string + * format: binary + * description: Optional new image + * responses: + * 200: + * description: Feedback updated successfully + * 404: + * description: Feedback not found + * 413: + * description: File size too large (max 5MB) + * 417: + * description: Validation error + * 500: + * description: Internal server error + */ +router.patch('/update/:id', verifyJWT, isAdmin, multer.single('image'), multerErrorHandler, feedbackController.updateFeedback); + +/** + * @swagger + * /feedback-api/delete/{id}: + * delete: + * summary: Delete feedback by ID + * tags: [Feedback] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Feedback ID + * responses: + * 200: + * description: Feedback deleted successfully + * 404: + * description: Feedback not found + * 500: + * description: Internal server error + */ +router.delete('/delete/:id', verifyJWT, isAdmin, feedbackController.deleteFeedback); + +module.exports = router; diff --git a/src/routes/feedback.ts b/src/routes/feedback.ts deleted file mode 100644 index 1000e93..0000000 --- a/src/routes/feedback.ts +++ /dev/null @@ -1,460 +0,0 @@ -import { Router, Request, Response, NextFunction } from 'express'; -import { protect, restrictTo, AuthenticatedRequest } from '../middleware/auth'; -import prisma from '../lib/prisma'; -import { upload } from '../middleware/upload'; -import { CustomError } from '../middleware/errorHandler'; - -const router = Router(); - -// Map data feedback dari DB ke format frontend -function mapToFrontend(feedback: any) { - return { - id: feedback.id, - nama: feedback.nama, - universitas: feedback.universitas, - posisi: feedback.posisi, - batch: feedback.batch, - tahun: feedback.tahun, - pesan: feedback.pesan, - image_path: feedback.imagePath, - createdAt: feedback.createdAt - }; -} - -/** - * @swagger - * /api/feedback-api/get: - * get: - * summary: Ambil semua feedback - * description: Mengembalikan daftar seluruh feedback/testimoni peserta. - * tags: [Feedback] - * responses: - * 200: - * description: Data feedback berhasil diambil - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * data: - * type: array - * items: - * type: object - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.get('/get', async (_req: Request, res: Response, next: NextFunction) => { - try { - const feedbacks = await prisma.feedback.findMany({ - orderBy: { createdAt: 'desc' } - }); - res.status(200).json({ - status: true, - data: feedbacks.map(mapToFrontend) - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/feedback-api/get/{id}: - * get: - * summary: Ambil detail feedback berdasarkan ID - * description: Mengembalikan detail satu feedback/testimoni. - * tags: [Feedback] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * description: ID feedback - * responses: - * 200: - * description: Detail feedback berhasil diambil - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * data: - * type: object - * 404: - * description: Feedback tidak ditemukan - * content: - * application/json: - * example: - * status: error - * statusCode: 404 - * message: Feedback tidak ditemukan - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.get('/get/:id', async (req: Request, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const feedback = await prisma.feedback.findUnique({ - where: { id: parseInt(id, 10) } - }); - - if (!feedback) { - const error: CustomError = new Error('Feedback tidak ditemukan'); - error.statusCode = 404; - return next(error); - } - - res.status(200).json({ - status: true, - data: mapToFrontend(feedback) - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/feedback-api/add: - * post: - * summary: Tambah feedback baru - * description: Menambahkan feedback/testimoni baru. Dapat diakses user yang sudah login. - * tags: [Feedback] - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * multipart/form-data: - * schema: - * type: object - * required: - * - nama - * - universitas - * - posisi - * - batch - * - tahun - * - pesan - * properties: - * nama: - * type: string - * example: Alya Putri - * universitas: - * type: string - * example: Telkom University - * posisi: - * type: string - * example: Frontend Developer Intern - * batch: - * type: integer - * example: 3 - * tahun: - * type: integer - * example: 2026 - * pesan: - * type: string - * example: Pengalaman magang sangat membantu perkembangan skill saya. - * image: - * type: string - * format: binary - * responses: - * 201: - * description: Feedback berhasil ditambahkan - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * message: - * type: string - * example: Feedback berhasil ditambahkan - * 400: - * description: Validasi input feedback gagal - * content: - * application/json: - * examples: - * field_wajib: - * value: - * status: error - * statusCode: 400 - * message: Mohon lengkapi semua field yang wajib diisi - * angka_tidak_valid: - * value: - * status: error - * statusCode: 400 - * message: Batch dan tahun harus berupa angka - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 500: - * description: Terjadi kesalahan server (termasuk validasi file upload) - * content: - * application/json: - * examples: - * file_bukan_gambar: - * value: - * status: error - * statusCode: 500 - * message: File upload harus berupa gambar! - * internal: - * value: - * status: error - * statusCode: 500 - * message: Internal Server Error - */ -router.post('/add', protect, upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - try { - const { nama, universitas, posisi, batch, tahun, pesan } = req.body; - - if (!nama || !universitas || !posisi || !batch || !tahun || !pesan) { - const error: CustomError = new Error('Mohon lengkapi semua field yang wajib diisi'); - error.statusCode = 400; - return next(error); - } - - const parsedBatch = parseInt(batch, 10); - const parsedTahun = parseInt(tahun, 10); - - if (isNaN(parsedBatch) || isNaN(parsedTahun)) { - const error: CustomError = new Error('Batch dan tahun harus berupa angka'); - error.statusCode = 400; - return next(error); - } - - const imagePath = req.file ? `/uploads/images/${req.file.filename}` : ''; - - const feedback = await prisma.feedback.create({ - data: { - nama, - universitas, - posisi, - batch: parsedBatch, - tahun: parsedTahun, - pesan, - imagePath - } - }); - - res.status(201).json({ - status: true, - message: 'Feedback berhasil ditambahkan', - data: mapToFrontend(feedback) - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/feedback-api/update/{id}: - * patch: - * summary: Perbarui feedback - * description: Memperbarui data feedback berdasarkan ID. Dapat diakses user yang sudah login. - * tags: [Feedback] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * description: ID feedback - * requestBody: - * required: false - * content: - * multipart/form-data: - * schema: - * type: object - * properties: - * nama: - * type: string - * universitas: - * type: string - * posisi: - * type: string - * batch: - * type: integer - * tahun: - * type: integer - * pesan: - * type: string - * image: - * type: string - * format: binary - * responses: - * 200: - * description: Feedback berhasil diperbarui - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * message: - * type: string - * example: Feedback berhasil diupdate - * 400: - * description: Validasi data feedback gagal - * content: - * application/json: - * example: - * status: error - * statusCode: 400 - * message: Batch dan tahun harus berupa angka - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 404: - * description: Feedback tidak ditemukan - * content: - * application/json: - * example: - * status: error - * statusCode: 404 - * message: Feedback tidak ditemukan - * 500: - * description: Terjadi kesalahan server (termasuk validasi file upload) - * content: - * application/json: - * examples: - * file_bukan_gambar: - * value: - * status: error - * statusCode: 500 - * message: File upload harus berupa gambar! - * internal: - * value: - * status: error - * statusCode: 500 - * message: Internal Server Error - */ -router.patch('/update/:id', protect, upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const { nama, universitas, posisi, batch, tahun, pesan } = req.body; - - const existing = await prisma.feedback.findUnique({ - where: { id: parseInt(id, 10) } - }); - - if (!existing) { - const error: CustomError = new Error('Feedback tidak ditemukan'); - error.statusCode = 404; - return next(error); - } - - let imagePath = existing.imagePath; - if (req.file) { - imagePath = `/uploads/images/${req.file.filename}`; - } - - const parsedBatch = batch ? parseInt(batch, 10) : undefined; - const parsedTahun = tahun ? parseInt(tahun, 10) : undefined; - - if ((batch && isNaN(parsedBatch as number)) || (tahun && isNaN(parsedTahun as number))) { - const error: CustomError = new Error('Batch dan tahun harus berupa angka'); - error.statusCode = 400; - return next(error); - } - - const updated = await prisma.feedback.update({ - where: { id: parseInt(id, 10) }, - data: { - nama: nama ?? existing.nama, - universitas: universitas ?? existing.universitas, - posisi: posisi ?? existing.posisi, - batch: parsedBatch ?? existing.batch, - tahun: parsedTahun ?? existing.tahun, - pesan: pesan ?? existing.pesan, - imagePath - } - }); - - res.status(200).json({ - status: true, - message: 'Feedback berhasil diupdate', - data: mapToFrontend(updated) - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/feedback-api/delete/{id}: - * delete: - * summary: Hapus feedback - * description: Menghapus data feedback berdasarkan ID. Hanya dapat diakses oleh admin. - * tags: [Feedback] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * description: ID feedback - * responses: - * 200: - * description: Feedback berhasil dihapus - * content: - * application/json: - * example: - * status: true - * message: Feedback berhasil dihapus - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 403: - * $ref: '#/components/responses/ForbiddenError' - * 404: - * description: Feedback tidak ditemukan - * content: - * application/json: - * example: - * status: error - * statusCode: 404 - * message: Feedback tidak ditemukan - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const existing = await prisma.feedback.findUnique({ - where: { id: parseInt(id, 10) } - }); - - if (!existing) { - const error: CustomError = new Error('Feedback tidak ditemukan'); - error.statusCode = 404; - return next(error); - } - - await prisma.feedback.delete({ - where: { id: parseInt(id, 10) } - }); - - res.status(200).json({ - status: true, - message: 'Feedback berhasil dihapus' - }); - } catch (error) { - next(error); - } -}); - -export default router; diff --git a/src/routes/hasilResearch.routes.js b/src/routes/hasilResearch.routes.js new file mode 100755 index 0000000..7ff3ade --- /dev/null +++ b/src/routes/hasilResearch.routes.js @@ -0,0 +1,286 @@ +const express = require('express'); +const { hasilResearchController } = require('../modules/hasilResearch'); +const { verifyJWT } = require('../middleware/verifyJWT'); +const isAdmin = require('../middleware/isAdmin'); +const multer = require('../middleware/multer'); +const { multerErrorHandler } = require('../middleware/multer'); + +const router = express.Router(); + +/** + * @swagger + * components: + * schemas: + * HasilResearch: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * image_path: + * type: string + * example: "/uploads/research_image.png" + * nama_project: + * type: string + * example: "Machine Learning for Healthcare" + * deskripsi: + * type: string + * example: "A project exploring machine learning techniques in healthcare." + * link_project: + * type: string + * example: "https://youtube.com" + */ + +/** + * @swagger + * /hasil-research-api/add: + * post: + * summary: Add a HUMIC Internship research (product) + * tags: [Hasil Research] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * nama_project: + * type: string + * description: Name of the research project + * example: "Machine Learning for Healthcare" + * deskripsi: + * type: string + * description: Description of the research project + * example: "A project exploring machine learning techniques in healthcare." + * link_project: + * type: string + * description: link to the project + * example: "https://youtube.com" + * image: + * type: string + * format: binary + * description: Image of the research project + * responses: + * 200: + * description: Research result added successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Data hasil research berhasil ditambahkan" + * data: + * type: object + * properties: + * nama_project: + * type: string + * example: "Machine Learning for Healthcare" + * deskripsi: + * type: string + * example: "A project exploring machine learning techniques in healthcare." + * image_path: + * type: string + * example: "/uploads/research_image.png" + * 413: + * description: File size too large (max 5MB) + * 500: + * description: Internal server error + */ +router.post('/add', verifyJWT, isAdmin, multer.single('image'), multerErrorHandler, hasilResearchController.addHasilResearch); + +/** + * @swagger + * /hasil-research-api/get: + * get: + * summary: Retrieve all research (product) results + * tags: [Hasil Research] + * responses: + * 200: + * description: Successfully retrieved all research results + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Research data was successfully collected" + * data: + * type: array + * items: + * $ref: '#/components/schemas/HasilResearch' + * 404: + * description: No research data found + * 500: + * description: Internal server error + */ +router.get('/get', hasilResearchController.getHasilResearch); + +/** + * @swagger + * /hasil-research-api/get/{id}: + * get: + * summary: Get research result by ID + * tags: [Hasil Research] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The ID of the research data to retrieve + * responses: + * 200: + * description: Research data was successfully collected + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Research data was successfully collected + * data: + * type: array + * items: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * image_path: + * type: string + * example: "/uploads/1629882719283.png" + * nama_project: + * type: string + * example: "Smart Farming IoT" + * deskripsi: + * type: string + * example: "A smart farming system using IoT sensors" + * link_project: + * type: string + * example: "https://github.com/user/smart-farming" + * 404: + * description: Hasil research not found + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Hasil research not found + * 500: + * description: Internal server error + */ +router.get('/get/:id', hasilResearchController.getHasilResearchById); + +/** + * @swagger + * /hasil-research-api/update/{id}: + * patch: + * summary: Update an existing hasil research entry + * tags: [Hasil Research] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The ID of the hasil research to update + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * nama_project: + * type: string + * example: "Sistem Deteksi Kualitas Udara" + * deskripsi: + * type: string + * example: "Aplikasi yang memantau dan menilai kualitas udara secara real-time" + * link_project: + * type: string + * example: "https://github.com/username/project" + * image: + * type: string + * format: binary + * description: Optional. Upload a new image to replace the old one. + * responses: + * 200: + * description: Hasil research successfully updated + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Hasil research successfully updated" + * data: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * nama_project: + * type: string + * deskripsi: + * type: string + * link_project: + * type: string + * image_path: + * type: string + * example: "/uploads/1721019403915.png" + * 404: + * description: Hasil research not found + * 413: + * description: File size too large (max 5MB) + * 500: + * description: Internal server error + */ +router.patch("/update/:id", verifyJWT, isAdmin, multer.single('image'), multerErrorHandler, hasilResearchController.updateHasilResearch); + +/** + * @swagger + * /hasil-research-api/delete/{id}: + * delete: + * summary: Delete a research (product) result by ID + * tags: [Hasil Research] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: ID of the research (product) result to delete + * responses: + * 200: + * description: Research result deleted successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Research result data successfully deleted" + * 404: + * description: Research result not found + * 500: + * description: Internal server error + */ +router.delete('/delete/:id', verifyJWT, isAdmin, hasilResearchController.deleteHasilResearch); + + +module.exports = router; \ No newline at end of file diff --git a/src/routes/index.ts b/src/routes/index.ts deleted file mode 100644 index 2a6f3ef..0000000 --- a/src/routes/index.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { Router, Request, Response, NextFunction } from 'express'; -import prisma from '../lib/prisma'; -import authRouter from './auth'; -import lowonganRouter from './lowongan'; -import lamaranRouter from './lamaran'; -import partnershipRouter from './partnership'; -import researchRouter from './research'; -import feedbackRouter from './feedback'; -import faqRouter from './faq'; - -const router = Router(); - -router.use('/auth', authRouter); -router.use('/auth-api', authRouter); -router.use('/lowongan-magang-api', lowonganRouter); -router.use('/lamaran-magang-api', lamaranRouter); -router.use('/partnership-api', partnershipRouter); -router.use('/hasil-research-api', researchRouter); -router.use('/feedback-api', feedbackRouter); -router.use('/faq-api', faqRouter); - -/** - * @swagger - * /api/health: - * get: - * summary: Cek status kesehatan server - * description: Digunakan untuk memastikan API berjalan normal. - * tags: [Admin] - * responses: - * 200: - * description: Server berjalan dengan baik - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: OK - * message: - * type: string - * example: Server is running smoothly - * timestamp: - * type: string - * format: date-time - * example: 2026-06-02T10:00:00.000Z - */ -router.get('/health', (_req: Request, res: Response) => { - res.status(200).json({ - status: 'OK', - message: 'Server is running smoothly', - timestamp: new Date().toISOString(), - }); -}); - -/** - * @swagger - * /api/users: - * get: - * summary: Ambil daftar seluruh pengguna - * description: Mengembalikan seluruh data user dasar dari database. - * tags: [Admin] - * responses: - * 200: - * description: Daftar pengguna berhasil diambil - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * example: true - * count: - * type: integer - * example: 2 - * data: - * type: array - * items: - * type: object - * properties: - * id: - * type: string - * example: cku3xq2v0000xk8w7a1b2c3d4 - * email: - * type: string - * example: admin@internify.com - * role: - * type: string - * example: ADMIN - * createdAt: - * type: string - * format: date-time - * example: 2026-06-02T10:00:00.000Z - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.get('/users', async (_req: Request, res: Response, next: NextFunction) => { - try { - const users = await prisma.user.findMany({ - select: { - id: true, - email: true, - role: true, - createdAt: true, - }, - }); - res.status(200).json({ - success: true, - count: users.length, - data: users, - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/error-test: - * get: - * summary: Simulasi error server untuk pengujian - * description: Endpoint utilitas untuk menguji middleware penanganan error global. - * tags: [Admin] - * responses: - * 500: - * description: Error simulasi berhasil dipicu - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: error - * statusCode: - * type: integer - * example: 500 - * message: - * type: string - * example: This is a test error to verify our custom global error handler! - */ -router.get('/error-test', (_req: Request, _res: Response, next: NextFunction) => { - try { - throw new Error('This is a test error to verify our custom global error handler!'); - } catch (error) { - next(error); - } -}); - -export default router; diff --git a/src/routes/lamaran.ts b/src/routes/lamaran.ts deleted file mode 100644 index 56a3bae..0000000 --- a/src/routes/lamaran.ts +++ /dev/null @@ -1,622 +0,0 @@ -import { Router, Request, Response, NextFunction } from 'express'; -import bcrypt from 'bcryptjs'; -import * as XLSX from 'xlsx'; -import { protect, restrictTo, AuthenticatedRequest } from '../middleware/auth'; -import prisma from '../lib/prisma'; -import { upload } from '../middleware/upload'; -import { CustomError } from '../middleware/errorHandler'; - -const router = Router(); - -// Map data lamaran dari DB ke format frontend -function mapToFrontend(lamaran: any) { - return { - id: lamaran.id, - status: (lamaran.status || 'diproses').toLowerCase(), - batch: lamaran.batch, - createdAt: lamaran.createdAt, - updatedAt: lamaran.updatedAt, - mahasiswa: lamaran.mahasiswa ? { - id: lamaran.mahasiswa.id, - nama_depan: lamaran.mahasiswa.namaDepan, - nama_belakang: lamaran.mahasiswa.namaBelakang || '', - email: lamaran.mahasiswa.user?.email || '', - kontak: lamaran.mahasiswa.kontak, - jurusan: lamaran.mahasiswa.jurusan, - universitas: lamaran.mahasiswa.universitas, - negara: lamaran.mahasiswa.negara, - cv_path: lamaran.mahasiswa.cvPath, - portofolio_path: lamaran.mahasiswa.portofolioPath, - motivasi: lamaran.mahasiswa.motivasi, - relevant_skills: lamaran.mahasiswa.relevantSkills - } : null, - lowongan_magang: lamaran.lowonganMagang ? { - id: lamaran.lowonganMagang.id, - posisi: lamaran.lowonganMagang.posisi, - kelompok_peminatan: lamaran.lowonganMagang.kelompokPeminatan - } : null - }; -} - -/** - * @swagger - * /api/lamaran-magang-api/add/{lowonganId}: - * post: - * summary: Kirim lamaran magang baru - * description: Mengirim pendaftaran magang ke lowongan tertentu beserta CV dan portofolio (PDF). - * tags: [Lamaran Magang] - * parameters: - * - in: path - * name: lowonganId - * required: true - * schema: - * type: string - * description: ID lowongan magang - * requestBody: - * required: true - * content: - * multipart/form-data: - * schema: - * type: object - * required: - * - nama_depan - * - email - * - kontak - * - universitas - * - negara - * - jurusan - * - motivasi - * - cv - * - portofolio - * properties: - * nama_depan: - * type: string - * example: Alya - * nama_belakang: - * type: string - * example: Putri - * email: - * type: string - * example: alya@example.com - * kontak: - * type: string - * example: 081234567890 - * universitas: - * type: string - * example: Telkom University - * negara: - * type: string - * example: Indonesia - * jurusan: - * type: string - * example: Informatika - * batch: - * type: integer - * example: 3 - * motivasi: - * type: string - * example: Saya ingin belajar langsung dari tim engineering. - * relevant_skills: - * type: string - * example: React, TypeScript, Node.js - * cv: - * type: string - * format: binary - * portofolio: - * type: string - * format: binary - * responses: - * 201: - * description: Lamaran berhasil dikirim - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * message: - * type: string - * example: Pendaftaran magang berhasil dikirim - * 400: - * description: Validasi pengajuan lamaran gagal - * content: - * application/json: - * examples: - * field_wajib: - * value: - * status: error - * statusCode: 400 - * message: Mohon lengkapi semua field wajib yang diperlukan - * dokumen_wajib: - * value: - * status: error - * statusCode: 400 - * message: CV dan Portofolio wajib diunggah! - * 404: - * description: Lowongan magang tidak ditemukan - * content: - * application/json: - * example: - * status: error - * statusCode: 404 - * message: Lowongan magang tidak ditemukan - * 500: - * description: Terjadi kesalahan server (termasuk validasi tipe file upload) - * content: - * application/json: - * examples: - * tipe_file_pdf: - * value: - * status: error - * statusCode: 500 - * message: CV dan Portofolio harus dalam format PDF! - * internal: - * value: - * status: error - * statusCode: 500 - * message: Internal Server Error - */ -router.post( - '/add/:lowonganId', - upload.fields([ - { name: 'cv', maxCount: 1 }, - { name: 'portofolio', maxCount: 1 } - ]), - async (req: Request, res: Response, next: NextFunction) => { - try { - const { lowonganId } = req.params; - const { - nama_depan, - nama_belakang, - email, - kontak, - universitas, - negara, - jurusan, - batch, - motivasi, - relevant_skills - } = req.body; - - if (!nama_depan || !email || !kontak || !universitas || !negara || !jurusan || !motivasi) { - const error: CustomError = new Error('Mohon lengkapi semua field wajib yang diperlukan'); - error.statusCode = 400; - return next(error); - } - - const files = req.files as { [fieldname: string]: Express.Multer.File[] }; - const cvFile = files?.['cv']?.[0]; - const portofolioFile = files?.['portofolio']?.[0]; - - if (!cvFile || !portofolioFile) { - const error: CustomError = new Error('CV dan Portofolio wajib diunggah!'); - error.statusCode = 400; - return next(error); - } - - const lowongan = await prisma.lowonganMagang.findUnique({ - where: { id: lowonganId } - }); - - if (!lowongan) { - const error: CustomError = new Error('Lowongan magang tidak ditemukan'); - error.statusCode = 404; - return next(error); - } - - let user = await prisma.user.findUnique({ - where: { email }, - include: { mahasiswa: true } - }); - - const cvPath = `/uploads/docs/${cvFile.filename}`; - const portofolioPath = `/uploads/docs/${portofolioFile.filename}`; - - if (!user) { - // Kalo user belom terdaftar, kita buatin akun STUDENT baru - const randomPassword = Math.random().toString(36).substring(2, 15); - const salt = await bcrypt.genSalt(10); - const passwordHash = await bcrypt.hash(randomPassword, salt); - - user = await prisma.user.create({ - data: { - email, - passwordHash, - role: 'STUDENT', - mahasiswa: { - create: { - namaDepan: nama_depan, - namaBelakang: nama_belakang || null, - kontak, - jurusan, - universitas, - negara, - cvPath, - portofolioPath, - motivasi, - relevantSkills: relevant_skills || '' - } - } - }, - include: { mahasiswa: true } - }); - } else { - // Kalo user udah ada, kita update data profil mahasiswanya aja - if (user.mahasiswa) { - await prisma.mahasiswa.update({ - where: { id: user.mahasiswa.id }, - data: { - namaDepan: nama_depan, - namaBelakang: nama_belakang || user.mahasiswa.namaBelakang, - kontak: kontak || user.mahasiswa.kontak, - jurusan: jurusan || user.mahasiswa.jurusan, - universitas: universitas || user.mahasiswa.universitas, - negara: negara || user.mahasiswa.negara, - cvPath, - portofolioPath, - motivasi, - relevantSkills: relevant_skills || user.mahasiswa.relevantSkills - } - }); - } - } - - const parsedBatch = batch ? parseInt(batch, 10) : lowongan.idBatch; - - const lamaran = await prisma.lamaranMagang.create({ - data: { - idMahasiswa: user.mahasiswa!.id, - idLowonganMagang: lowongan.id, - status: 'DIPROSES', - batch: parsedBatch - }, - include: { - mahasiswa: { - include: { user: true } - }, - lowonganMagang: true - } - }); - - res.status(201).json({ - status: true, - message: 'Pendaftaran magang berhasil dikirim', - data: mapToFrontend(lamaran) - }); - } catch (error) { - next(error); - } - } -); - -/** - * @swagger - * /api/lamaran-magang-api/get: - * get: - * summary: Ambil seluruh data lamaran - * description: Mengambil daftar seluruh lamaran magang. Hanya dapat diakses oleh admin. - * tags: [Lamaran Magang] - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: Data lamaran berhasil diambil - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * data: - * type: array - * items: - * type: object - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 403: - * $ref: '#/components/responses/ForbiddenError' - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.get('/get', protect, restrictTo('ADMIN'), async (_req: AuthenticatedRequest, res: Response, next: NextFunction) => { - try { - const applications = await prisma.lamaranMagang.findMany({ - include: { - mahasiswa: { - include: { user: true } - }, - lowonganMagang: true - }, - orderBy: { createdAt: 'desc' } - }); - res.status(200).json({ - status: true, - data: applications.map(mapToFrontend) - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/lamaran-magang-api/get/{id}: - * get: - * summary: Ambil detail lamaran berdasarkan ID - * description: Mengambil detail satu lamaran magang. Hanya dapat diakses oleh admin. - * tags: [Lamaran Magang] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * description: ID lamaran magang - * responses: - * 200: - * description: Detail lamaran berhasil diambil - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * data: - * type: object - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 403: - * $ref: '#/components/responses/ForbiddenError' - * 404: - * description: Lamaran tidak ditemukan - * content: - * application/json: - * example: - * status: error - * statusCode: 404 - * message: Lamaran tidak ditemukan - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.get('/get/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const application = await prisma.lamaranMagang.findUnique({ - where: { id: parseInt(id, 10) }, - include: { - mahasiswa: { - include: { user: true } - }, - lowonganMagang: true - } - }); - - if (!application) { - const error: CustomError = new Error('Lamaran tidak ditemukan'); - error.statusCode = 404; - return next(error); - } - - res.status(200).json({ - status: true, - data: mapToFrontend(application) - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/lamaran-magang-api/update/{id}: - * patch: - * summary: Perbarui status lamaran - * description: Mengubah status lamaran menjadi `DIPROSES`, `DITERIMA`, atau `DITOLAK`. Hanya admin. - * tags: [Lamaran Magang] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * description: ID lamaran magang - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - status - * properties: - * status: - * type: string - * enum: [DIPROSES, DITERIMA, DITOLAK] - * example: DITERIMA - * responses: - * 200: - * description: Status lamaran berhasil diperbarui - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * message: - * type: string - * example: Status lamaran berhasil diupdate - * 400: - * description: Validasi status lamaran gagal - * content: - * application/json: - * examples: - * status_kosong: - * value: - * status: error - * statusCode: 400 - * message: Mohon masukkan status baru - * status_tidak_valid: - * value: - * status: error - * statusCode: 400 - * message: Status tidak valid - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 403: - * $ref: '#/components/responses/ForbiddenError' - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.patch('/update/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const { status } = req.body; - - if (!status) { - const error: CustomError = new Error('Mohon masukkan status baru'); - error.statusCode = 400; - return next(error); - } - - const formattedStatus = status.toUpperCase(); - if (formattedStatus !== 'DITERIMA' && formattedStatus !== 'DITOLAK' && formattedStatus !== 'DIPROSES') { - const error: CustomError = new Error('Status tidak valid'); - error.statusCode = 400; - return next(error); - } - - const updated = await prisma.lamaranMagang.update({ - where: { id: parseInt(id, 10) }, - data: { - status: formattedStatus as any - }, - include: { - mahasiswa: { - include: { user: true } - }, - lowonganMagang: true - } - }); - - res.status(200).json({ - status: true, - message: 'Status lamaran berhasil diupdate', - data: mapToFrontend(updated) - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/lamaran-magang-api/delete: - * delete: - * summary: Hapus seluruh data lamaran - * description: Menghapus seluruh data lamaran magang. Hanya dapat diakses oleh admin. - * tags: [Lamaran Magang] - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: Seluruh data pelamar berhasil dihapus - * content: - * application/json: - * example: - * status: true - * message: Seluruh data pelamar berhasil dihapus - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 403: - * $ref: '#/components/responses/ForbiddenError' - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.delete('/delete', protect, restrictTo('ADMIN'), async (_req: AuthenticatedRequest, res: Response, next: NextFunction) => { - try { - await prisma.lamaranMagang.deleteMany({}); - res.status(200).json({ - status: true, - message: 'Seluruh data pelamar berhasil dihapus' - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/lamaran-magang-api/export: - * get: - * summary: Ekspor data lamaran ke file Excel - * description: Mengunduh data lamaran dalam format `.xlsx`. Hanya dapat diakses oleh admin. - * tags: [Lamaran Magang] - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: File Excel berhasil dibuat - * content: - * application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: - * schema: - * type: string - * format: binary - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 403: - * $ref: '#/components/responses/ForbiddenError' - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.get('/export', protect, restrictTo('ADMIN'), async (_req: AuthenticatedRequest, res: Response, next: NextFunction) => { - try { - const applications = await prisma.lamaranMagang.findMany({ - include: { - mahasiswa: { - include: { user: true } - }, - lowonganMagang: true - } - }); - - const rows = applications.map((app) => ({ - ID: app.id, - Nama: `${app.mahasiswa?.namaDepan} ${app.mahasiswa?.namaBelakang || ''}`.trim(), - Email: app.mahasiswa?.user?.email || '', - Kontak: app.mahasiswa?.kontak || '', - Universitas: app.mahasiswa?.universitas || '', - Jurusan: app.mahasiswa?.jurusan || '', - Negara: app.mahasiswa?.negara || '', - Posisi: app.lowonganMagang?.posisi || '', - Status: app.status, - Motivasi: app.mahasiswa?.motivasi || '', - Skills: app.mahasiswa?.relevantSkills || '' - })); - - const worksheet = XLSX.utils.json_to_sheet(rows); - const workbook = XLSX.utils.book_new(); - XLSX.utils.book_append_sheet(workbook, worksheet, 'Applications'); - - const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' }); - - res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); - res.setHeader('Content-Disposition', 'attachment; filename="data.xlsx"'); - res.send(buffer); - } catch (error) { - next(error); - } -}); - -export default router; diff --git a/src/routes/lamaranMagang.routes.js b/src/routes/lamaranMagang.routes.js new file mode 100755 index 0000000..8fa981b --- /dev/null +++ b/src/routes/lamaranMagang.routes.js @@ -0,0 +1,686 @@ +const express = require('express'); +const { lamaranMagangController } = require('../modules/lamaranMagang'); +const multer = require('../middleware/multer'); +const { multerErrorHandler, checkFileSizes } = require('../middleware/multer'); +const { verifyJWT } = require('../middleware/verifyJWT'); +const isAdmin = require('../middleware/isAdmin'); +const verifyRecaptcha = require('../middleware/recaptcha'); + +const router = express.Router(); + +/** + * @swagger + * components: + * schemas: + * LamaranMagang: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * id_lamaran_magang: + * type: integer + * example: 7 + * nama_depan: + * type: string + * example: "Wowo" + * nama_belakang: + * type: string + * example: "Wewe" + * email: + * type: string + * example: "yntkts@example.com" + * kontak: + * type: string + * example: "08123456789" + * jurusan: + * type: string + * example: "Informatika" + * motivasi: + * type: string + * example: "I want to gain experience in back end development." + * relevant_skills: + * type: string + * example: "JavaScript, Node.js" + * cv_path: + * type: string + * example: "/uploads/cv.pdf" + * portofolio_path: + * type: string + * example: "/uploads/portofolio.pdf" + * posisi: + * type: string + * example: "Backend Developer" + * kelompok_peminatan: + * type: string + * example: "Software Engineering" + * status: + * type: string + * enum: [diproses, diterima, ditolak] + * example: "diproses" + * created_at: + * type: string + * format: date-time + * example: "2025-05-12T10:00:00Z" + */ + +/** + * @swagger + * /lamaran-magang-api/add/{id_lowongan_magang}: + * post: + * summary: Create a new lamaran magang (internship application) + * tags: [Lamaran Magang] + * parameters: + * - in: path + * name: id_lowongan_magang + * required: true + * schema: + * type: string + * description: The ID (string) of the lowongan magang (internship) + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * g-recaptcha-response: + * type: string + * description: "This field requests the TOKEN GENERATED when the user completes the recaptcha, NOT THE SITE KEY. Refer to the reCAPTCHA v2 docs" + * nama_depan: + * type: string + * nama_belakang: + * type: string + * nullable: true + * description: Optional, can be left empty + * email: + * type: string + * kontak: + * type: string + * jurusan: + * type: string + * motivasi: + * type: string + * relevant_skills: + * type: string + * cv: + * type: string + * format: binary + * portofolio: + * type: string + * format: binary + * responses: + * 200: + * description: Lamaran magang created successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Internship application submitted successfully." + * data: + * $ref: '#/components/schemas/LamaranMagang' + * 400: + * description: Invalid reCAPTCHA or unauthorized access + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "reCAPTCHA validation failed" + * error: + * type: array + * example: ["invalid-input-response"] + * 413: + * description: File size too large (max 5MB) + * 500: + * description: Internal server error + */ +router.post( + '/add/:id_lowongan_magang', + multer.fields([ + { name: 'cv', maxCount: 1 }, + { name: 'portofolio', maxCount: 1 }, + ]), + checkFileSizes, + multerErrorHandler, + // verifyRecaptcha, // multer dulu baru verify gegara datanya mesti diparse sama multer + lamaranMagangController.addLamaranMagang +); + +/** + * @swagger + * /lamaran-magang-api/add-mobile/{id_lowongan_magang}: + * post: + * summary: Create a new lamaran magang (internship application) without recaptcha + * tags: [Lamaran Magang] + * parameters: + * - in: path + * name: id_lowongan_magang + * required: true + * schema: + * type: string + * description: The ID (string) of the lowongan magang (internship) + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * nama_depan: + * type: string + * nama_belakang: + * type: string + * nullable: true + * description: Optional, can be left empty + * email: + * type: string + * kontak: + * type: string + * jurusan: + * type: string + * motivasi: + * type: string + * relevant_skills: + * type: string + * cv: + * type: string + * format: binary + * portofolio: + * type: string + * format: binary + * responses: + * 200: + * description: Lamaran magang created successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Internship application submitted successfully." + * data: + * $ref: '#/components/schemas/LamaranMagang' + * 404: + * description: lowongan_magang id not found + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Internship vacancy not found" + * error: + * type: array + * example: ["invalid-input-response"] + * 413: + * description: File size too large (max 5MB) + * 500: + * description: Internal server error + */ +router.post( + '/add-mobile/:id_lowongan_magang', + multer.fields([ + { name: 'cv', maxCount: 1 }, + { name: 'portofolio', maxCount: 1 }, + ]), + checkFileSizes, + multerErrorHandler, + lamaranMagangController.addLamaranMagang +); + +/** + * @swagger + * /lamaran-magang-api/get: + * get: + * summary: Get all lamaran magang + * tags: [Lamaran Magang] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * minimum: 1 + * description: Page number (default 1) + * example: 1 + * - in: query + * name: limit + * schema: + * type: integer + * minimum: 1 + * description: Number of items per page (default 10) + * example: 10 + * - in: query + * name: status + * schema: + * type: string + * enum: [diproses, diterima, ditolak] + * description: Filter by application status + * example: diterima + * - in: query + * name: posisi + * schema: + * type: string + * description: Filter by internship position + * example: Backend Developer + * - in: query + * name: universitas + * schema: + * type: string + * description: Filter by university name + * example: Universitas Indonesia + * responses: + * 200: + * description: Successfully retrieved all lamaran magang + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/LamaranMagang' + * 404: + * description: No lamaran magang found + * 417: + * description: Validation error in query parameters + * 500: + * description: Internal server error + */ +router.get('/get', verifyJWT, isAdmin, lamaranMagangController.getAllLamaranMagang); + +/** + * @swagger + * /lamaran-magang-api/get/{id_lamaran_magang}: + * get: + * summary: Get lamaran magang by ID + * tags: [Lamaran Magang] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id_lamaran_magang + * required: true + * schema: + * type: string + * description: The ID of the lamaran magang (internship) + * responses: + * 200: + * description: Successfully retrieved lamaran magang + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/LamaranMagang' + * 404: + * description: No lamaran magang found + * 500: + * description: Internal server error + */ +router.get( + '/get/:id_lamaran_magang', + verifyJWT, + lamaranMagangController.getLamaranByID +); + +/** + * @swagger + * /lamaran-magang-api/update/{id_lamaran_magang}: + * patch: + * summary: Update the status of a lamaran magang + * tags: [Lamaran Magang] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id_lamaran_magang + * required: true + * schema: + * type: string + * description: The ID of the lamaran magang + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: "diterima" + * responses: + * 200: + * description: Status lamaran magang updated successfully + * 404: + * description: Unauthorized or lamaran magang not found + * 500: + * description: Internal server error + */ +router.patch( + '/update/:id_lamaran_magang', + verifyJWT, + isAdmin, + lamaranMagangController.updateStatusLamaran +); + +/** + * @swagger + * /lamaran-magang-api/export: + * get: + * summary: Export all internship applications to Excel + * tags: [Lamaran Magang] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Excel file generated successfully + * content: + * application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: + * schema: + * type: string + * format: binary + * 403: + * description: Unauthorized access + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Unauthorized access." + * 500: + * description: Error generating Excel file + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Error generating Excel file" + */ +router.get('/export', verifyJWT, isAdmin, lamaranMagangController.exportLamaranToExcel); + +/** + * @swagger + * /lamaran-magang-api/delete/{id}: + * delete: + * summary: Delete a specific lamaran magang by ID (including CV and portfolio files) + * tags: [Lamaran Magang] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: The ID of the lamaran magang to delete + * responses: + * 200: + * description: Lamaran magang deleted successfully + * 403: + * description: "Access denied: Only admins can access this route" + * 404: + * description: Lamaran magang not found + * 500: + * description: Internal server error + */ +router.delete('/delete/:id', verifyJWT, isAdmin, lamaranMagangController.deleteLamaran); + +/** + * @swagger + * /lamaran-magang-api/delete: + * delete: + * summary: Delete all internship applications, students, and their files + * tags: [Lamaran Magang] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: All student data, applications, and files were successfully deleted. + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: All student data, applications, and files were successfully deleted. + * 500: + * description: Failed to delete all data + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Failed to delete all data. + * error: + * type: string + */ +// router.delete("/delete", verifyJWT, lamaranMagangController.deleteAllLamaranMagang); // Method doesn't exist + +/** + * @swagger + * /lamaran-magang-api/statistics/dashboard: + * get: + * summary: Get dashboard statistics (total applicants, accepted, rejected, pending) + * tags: [Lamaran Magang] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Dashboard statistics retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * type: object + * properties: + * total_pendaftar: + * type: integer + * example: 400 + * total_diterima: + * type: integer + * example: 150 + * total_ditolak: + * type: integer + * example: 250 + * sedang_diproses: + * type: integer + * example: 400 + * acceptance_rate: + * type: string + * example: "43.7%" + * rejection_rate: + * type: string + * example: "31.3%" + * pending_rate: + * type: string + * example: "25.0%" + * message: + * type: string + * example: "Statistik dashboard berhasil diambil" + * 500: + * description: Internal server error + */ +router.get('/statistics/dashboard', verifyJWT, isAdmin, lamaranMagangController.getDashboardStatistics); + +/** + * @swagger + * /lamaran-magang-api/statistics/position: + * get: + * summary: Get statistics grouped by position + * tags: [Lamaran Magang] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Position statistics retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * type: array + * items: + * type: object + * properties: + * posisi: + * type: string + * example: "Backend Developer" + * total: + * type: integer + * example: 150 + * diterima: + * type: integer + * example: 50 + * ditolak: + * type: integer + * example: 100 + * 500: + * description: Internal server error + */ +router.get('/statistics/position', verifyJWT, isAdmin, lamaranMagangController.getStatisticsByPosition); + +/** + * @swagger + * /lamaran-magang-api/statistics/country: + * get: + * summary: Get statistics grouped by country + * tags: [Lamaran Magang] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: batch + * schema: + * type: integer + * description: Filter by batch number + * example: 1 + * responses: + * 200: + * description: Country statistics retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * type: array + * items: + * type: object + * properties: + * negara: + * type: string + * example: "Indonesia" + * total: + * type: integer + * example: 200 + * diterima: + * type: integer + * example: 80 + * ditolak: + * type: integer + * example: 70 + * diproses: + * type: integer + * example: 50 + * 500: + * description: Internal server error + */ +router.get('/statistics/country', verifyJWT, isAdmin, lamaranMagangController.getStatisticsByCountry); + +/** + * @swagger + * /lamaran-magang-api/statistics/university: + * get: + * summary: Get statistics grouped by university + * tags: [Lamaran Magang] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: batch + * schema: + * type: integer + * description: Filter by batch number + * example: 1 + * responses: + * 200: + * description: University statistics retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * type: array + * items: + * type: object + * properties: + * universitas: + * type: string + * example: "Universitas Indonesia" + * total: + * type: integer + * example: 150 + * diterima: + * type: integer + * example: 60 + * ditolak: + * type: integer + * example: 50 + * diproses: + * type: integer + * example: 40 + * message: + * type: string + * example: "Statistik per universitas berhasil diambil" + * 404: + * description: No data found for the specified batch + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: false + * message: + * type: string + * example: "Tidak ada data lamaran ditemukan pada batch 1" + * 500: + * description: Internal server error + */ +router.get('/statistics/university', verifyJWT, isAdmin, lamaranMagangController.getStatisticsByUniversity); + + + +module.exports = router; \ No newline at end of file diff --git a/src/routes/lowongan.ts b/src/routes/lowongan.ts deleted file mode 100644 index 5728fe7..0000000 --- a/src/routes/lowongan.ts +++ /dev/null @@ -1,584 +0,0 @@ -import { Router, Request, Response, NextFunction } from 'express'; -import { protect, restrictTo, AuthenticatedRequest } from '../middleware/auth'; -import prisma from '../lib/prisma'; -import { upload } from '../middleware/upload'; -import { CustomError } from '../middleware/errorHandler'; - -const router = Router(); - -// Nyari batch yg aktif, kalo ga ada ya kita bikinin yg baru -async function getOrCreateActiveBatch() { - let batch = await prisma.batch.findFirst({ - where: { isActive: true } - }); - if (!batch) { - batch = await prisma.batch.create({ - data: { - batchNumber: 1, - isActive: true - } - }); - } - return batch; -} - -function mapToFrontend(lowongan: any) { - return { - id: lowongan.id, - id_batch: lowongan.idBatch, - posisi: lowongan.posisi, - kelompok_peminatan: lowongan.kelompokPeminatan, - image_path: lowongan.imagePath, - jobdesk: lowongan.jobdesk, - lokasi: lowongan.lokasi, - kualifikasi: lowongan.kualifikasi, - benefit: lowongan.benefit, - durasi_awal: lowongan.durasiAwal, - durasi_akhir: lowongan.durasiAkhir, - status_lowongan: lowongan.statusLowongan, - paid: lowongan.paid, - createdAt: lowongan.createdAt - }; -} - -/** - * @swagger - * /api/lowongan-magang-api/get: - * get: - * summary: Ambil seluruh lowongan magang - * description: Mengembalikan daftar semua lowongan magang yang tersedia. - * tags: [Lowongan Magang] - * responses: - * 200: - * description: Daftar lowongan berhasil diambil - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * data: - * type: array - * items: - * type: object - * properties: - * id: - * type: string - * example: lowongan-frontend - * posisi: - * type: string - * example: Frontend Developer Intern - * kelompok_peminatan: - * type: string - * example: Web Engineering - * status_lowongan: - * type: string - * example: DIBUKA - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.get('/get', async (_req: Request, res: Response, next: NextFunction) => { - try { - const lowongans = await prisma.lowonganMagang.findMany({ - orderBy: { createdAt: 'desc' } - }); - res.status(200).json({ - status: true, - data: lowongans.map(mapToFrontend) - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/lowongan-magang-api/get/kelompok-all: - * get: - * summary: Ambil semua kelompok peminatan unik - * description: Mengembalikan daftar kategori peminatan tanpa duplikasi. - * tags: [Lowongan Magang] - * responses: - * 200: - * description: Daftar kelompok peminatan berhasil diambil - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * data: - * type: array - * items: - * type: string - * example: ["Web Engineering", "Design & Creative"] - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.get('/get/kelompok-all', async (_req: Request, res: Response, next: NextFunction) => { - try { - const result = await prisma.lowonganMagang.findMany({ - select: { kelompokPeminatan: true }, - distinct: ['kelompokPeminatan'] - }); - const categories = result.map((r) => r.kelompokPeminatan); - res.status(200).json({ - status: true, - data: categories - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/lowongan-magang-api/get/id/{id}: - * get: - * summary: Ambil detail lowongan berdasarkan ID - * description: Mengembalikan detail satu lowongan magang berdasarkan parameter `id`. - * tags: [Lowongan Magang] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * description: ID lowongan - * responses: - * 200: - * description: Detail lowongan berhasil diambil - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * data: - * type: object - * properties: - * id: - * type: string - * example: lowongan-frontend - * posisi: - * type: string - * example: Frontend Developer Intern - * kelompok_peminatan: - * type: string - * example: Web Engineering - * 404: - * description: Lowongan tidak ditemukan - * content: - * application/json: - * example: - * status: error - * statusCode: 404 - * message: Lowongan tidak ditemukan - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.get('/get/id/:id', async (req: Request, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const lowongan = await prisma.lowonganMagang.findUnique({ - where: { id } - }); - if (!lowongan) { - const error: CustomError = new Error('Lowongan tidak ditemukan'); - error.statusCode = 404; - return next(error); - } - res.status(200).json({ - status: true, - data: mapToFrontend(lowongan) - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/lowongan-magang-api/add: - * post: - * summary: Tambah lowongan magang baru - * description: Membuat lowongan magang baru. Hanya dapat diakses oleh admin. - * tags: [Lowongan Magang] - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * multipart/form-data: - * schema: - * type: object - * required: - * - posisi - * - kelompok_peminatan - * - lokasi - * - jobdesk - * - durasi_awal - * - durasi_akhir - * - paid - * properties: - * posisi: - * type: string - * example: Frontend Developer Intern - * kelompok_peminatan: - * type: string - * example: Web Engineering - * lokasi: - * type: string - * example: Remote - * jobdesk: - * type: string - * example: Mengembangkan antarmuka responsif menggunakan React. - * kualifikasi: - * type: string - * example: Memahami React JS, ES6 JavaScript, HTML, CSS. - * benefit: - * type: string - * example: Sertifikat magang resmi, jam kerja fleksibel. - * durasi_awal: - * type: string - * format: date - * example: 2026-06-01 - * durasi_akhir: - * type: string - * format: date - * example: 2026-09-01 - * paid: - * type: string - * enum: [PAID, UNPAID] - * example: PAID - * image: - * type: string - * format: binary - * responses: - * 201: - * description: Lowongan berhasil ditambahkan - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * message: - * type: string - * example: Internship lowongan berhasil ditambahkan - * data: - * type: object - * 400: - * description: Validasi input lowongan gagal - * content: - * application/json: - * example: - * status: error - * statusCode: 400 - * message: Mohon lengkapi semua field yang wajib diisi - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 403: - * $ref: '#/components/responses/ForbiddenError' - * 500: - * description: Terjadi kesalahan server (termasuk validasi file upload) - * content: - * application/json: - * examples: - * file_bukan_gambar: - * value: - * status: error - * statusCode: 500 - * message: File upload harus berupa gambar! - * internal: - * value: - * status: error - * statusCode: 500 - * message: Internal Server Error - */ -router.post('/add', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - try { - const { - posisi, - kelompok_peminatan, - lokasi, - jobdesk, - kualifikasi, - benefit, - durasi_awal, - durasi_akhir, - paid - } = req.body; - - if (!posisi || !kelompok_peminatan || !lokasi || !jobdesk || !durasi_awal || !durasi_akhir || !paid) { - const error: CustomError = new Error('Mohon lengkapi semua field yang wajib diisi'); - error.statusCode = 400; - return next(error); - } - - const batch = await getOrCreateActiveBatch(); - - const imagePath = req.file ? `/uploads/images/${req.file.filename}` : ''; - - const id = `lowongan-${Date.now()}`; - - const lowongan = await prisma.lowonganMagang.create({ - data: { - id, - idBatch: batch.id, - posisi, - kelompokPeminatan: kelompok_peminatan, - imagePath, - jobdesk, - lokasi, - kualifikasi: kualifikasi || '', - benefit: benefit || '', - durasiAwal: new Date(durasi_awal), - durasiAkhir: new Date(durasi_akhir), - statusLowongan: 'DIBUKA', - paid: paid.toUpperCase() === 'PAID' ? 'PAID' : 'UNPAID' - } - }); - - res.status(201).json({ - status: true, - message: 'Internship lowongan berhasil ditambahkan', - data: mapToFrontend(lowongan) - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/lowongan-magang-api/update/{id}: - * patch: - * summary: Perbarui data lowongan magang - * description: Memperbarui data lowongan berdasarkan ID. Hanya dapat diakses oleh admin. - * tags: [Lowongan Magang] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * description: ID lowongan - * requestBody: - * required: false - * content: - * multipart/form-data: - * schema: - * type: object - * properties: - * posisi: - * type: string - * kelompok_peminatan: - * type: string - * lokasi: - * type: string - * jobdesk: - * type: string - * kualifikasi: - * type: string - * benefit: - * type: string - * durasi_awal: - * type: string - * format: date - * durasi_akhir: - * type: string - * format: date - * paid: - * type: string - * enum: [PAID, UNPAID] - * status_lowongan: - * type: string - * enum: [DIBUKA, DITUTUP] - * image: - * type: string - * format: binary - * responses: - * 200: - * description: Lowongan berhasil diperbarui - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * message: - * type: string - * example: Lowongan berhasil diupdate - * data: - * type: object - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 403: - * $ref: '#/components/responses/ForbiddenError' - * 404: - * description: Lowongan tidak ditemukan - * content: - * application/json: - * example: - * status: error - * statusCode: 404 - * message: Lowongan tidak ditemukan - * 500: - * description: Terjadi kesalahan server (termasuk validasi file upload) - * content: - * application/json: - * examples: - * file_bukan_gambar: - * value: - * status: error - * statusCode: 500 - * message: File upload harus berupa gambar! - * internal: - * value: - * status: error - * statusCode: 500 - * message: Internal Server Error - */ -router.patch('/update/:id', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const { - posisi, - kelompok_peminatan, - lokasi, - jobdesk, - kualifikasi, - benefit, - durasi_awal, - durasi_akhir, - paid, - status_lowongan - } = req.body; - - const existing = await prisma.lowonganMagang.findUnique({ - where: { id } - }); - - if (!existing) { - const error: CustomError = new Error('Lowongan tidak ditemukan'); - error.statusCode = 404; - return next(error); - } - - let imagePath = existing.imagePath; - if (req.file) { - imagePath = `/uploads/images/${req.file.filename}`; - } - - const updated = await prisma.lowonganMagang.update({ - where: { id }, - data: { - posisi: posisi ?? existing.posisi, - kelompokPeminatan: kelompok_peminatan ?? existing.kelompokPeminatan, - lokasi: lokasi ?? existing.lokasi, - jobdesk: jobdesk ?? existing.jobdesk, - kualifikasi: kualifikasi ?? existing.kualifikasi, - benefit: benefit ?? existing.benefit, - durasiAwal: durasi_awal ? new Date(durasi_awal) : existing.durasiAwal, - durasiAkhir: durasi_akhir ? new Date(durasi_akhir) : existing.durasiAkhir, - statusLowongan: (status_lowongan ?? existing.statusLowongan) === 'DIBUKA' ? 'DIBUKA' : 'DITUTUP', - paid: paid ? (paid.toUpperCase() === 'PAID' ? 'PAID' : 'UNPAID') : existing.paid, - imagePath - } - }); - - res.status(200).json({ - status: true, - message: 'Lowongan berhasil diupdate', - data: mapToFrontend(updated) - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/lowongan-magang-api/delete/{id}: - * delete: - * summary: Hapus lowongan magang - * description: Menghapus data lowongan berdasarkan ID. Hanya dapat diakses oleh admin. - * tags: [Lowongan Magang] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * description: ID lowongan - * responses: - * 200: - * description: Lowongan berhasil dihapus - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * message: - * type: string - * example: Lowongan berhasil dihapus - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 403: - * $ref: '#/components/responses/ForbiddenError' - * 404: - * description: Lowongan tidak ditemukan - * content: - * application/json: - * example: - * status: error - * statusCode: 404 - * message: Lowongan tidak ditemukan - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const existing = await prisma.lowonganMagang.findUnique({ - where: { id } - }); - - if (!existing) { - const error: CustomError = new Error('Lowongan tidak ditemukan'); - error.statusCode = 404; - return next(error); - } - - await prisma.lowonganMagang.delete({ - where: { id } - }); - - res.status(200).json({ - status: true, - message: 'Lowongan berhasil dihapus' - }); - } catch (error) { - next(error); - } -}); - -export default router; diff --git a/src/routes/lowonganMagang.routes.js b/src/routes/lowonganMagang.routes.js new file mode 100755 index 0000000..2da2f2c --- /dev/null +++ b/src/routes/lowonganMagang.routes.js @@ -0,0 +1,351 @@ +const express = require('express'); +const { lowonganMagangController } = require('../modules/lowonganMagang'); +const { verifyJWT } = require('../middleware/verifyJWT'); +const isAdmin = require('../middleware/isAdmin'); +const multer = require('../middleware/multer'); +const { multerErrorHandler } = require('../middleware/multer'); + +const router = express.Router(); +const lowonganMagang = lowonganMagangController; + +/** + * @swagger + * components: + * schemas: + * LowonganMagang: + * type: object + * properties: + * id: + * type: string + * example: "LWN-2025-001" + * posisi: + * type: string + * example: "Back End Developer" + * kelompok_peminatan: + * type: string + * example: "Software Engineer" + * image_path: + * type: string + * example: "/uploads/poster.png" + * jobdesk: + * type: string + * example: "Develop RESTful APIs and maintain databases" + * lokasi: + * type: string + * example: "Remote" + * kualifikasi: + * type: string + * example: "Familiarity with Node.js and Express" + * benefit: + * type: string + * example: "E-certificate, mentoring, flexible hours" + * durasi_awal: + * type: string + * format: date + * example: "2025-07-01" + * durasi_akhir: + * type: string + * format: date + * example: "2025-10-01" + * status_lowongan: + * type: string + * enum: [dibuka, ditutup] + * example: "dibuka" + * paid: + * type: string + * enum: [paid, unpaid] + * example: "paid" + * created_at: + * type: string + * format: date-time + * example: "2025-06-19T10:00:00Z" + */ + +/** + * @swagger + * /lowongan-magang-api/add: + * post: + * summary: "Create a new lowongan magang" + * tags: [Lowongan Magang] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * required: + * - posisi + * - kelompok_peminatan + * - jobdesk + * - lokasi + * - kualifikasi + * - benefit + * - durasi_awal + * - durasi_akhir + * - paid + * - image + * properties: + * posisi: + * type: string + * example: "Back End Development" + * kelompok_peminatan: + * type: string + * example: "Software Engineer" + * jobdesk: + * type: string + * example: "Membuat Schema database, membuat API, mengurus CI/CD" + * lokasi: + * type: string + * example: "Onsite" + * kualifikasi: + * type: string + * example: "Express" + * benefit: + * type: string + * example: "Pengalaman" + * durasi_awal: + * type: string + * format: date + * durasi_akhir: + * type: string + * format: date + * paid: + * type: string + * enum: [paid, unpaid] + * image: + * type: string + * format: binary + * responses: + * 200: + * description: "Lowongan magang created successfully" + * 403: + * description: "Access denied: Only admins can access this route" + * 413: + * description: "File size too large (max 5MB)" + * 500: + * description: "Internal server error" + */ +router.post("/add", verifyJWT, isAdmin, multer.single("image"), multerErrorHandler, lowonganMagang.addLowonganMagang); + +/** + * @swagger + * /lowongan-magang-api/get: + * get: + * summary: "Get all lowongan magang" + * tags: [Lowongan Magang] + * responses: + * 200: + * description: "Successfully retrieved all lowongan magang" + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * data: + * type: array + * items: + * $ref: '#/components/schemas/LowonganMagang' + * 404: + * description: "No internship vacancies found" + * 500: + * description: "Internal server error" + */ +router.get("/get", lowonganMagang.getAllLowonganMagang); + +/** + * @swagger + * /lowongan-magang-api/get/id/{id}: + * get: + * summary: "Get a specific lowongan magang by ID" + * tags: [Lowongan Magang] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: "The ID of the lowongan magang" + * responses: + * 200: + * description: "Successfully retrieved the lowongan magang" + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * data: + * $ref: '#/components/schemas/LowonganMagang' + * 404: + * description: "Internship vacancy not found" + * 500: + * description: "Internal server error" + */ +router.get("/get/id/:id", lowonganMagang.getLowonganMagangById); + +/** + * @swagger + * /lowongan-magang-api/get/kelompok/{kelompok_peminatan}: + * get: + * summary: Get all internship vacancies by specialization group (kelompok_peminatan) + * tags: [Lowongan Magang] + * parameters: + * - in: path + * name: kelompok_peminatan + * required: true + * schema: + * type: string + * description: The specialization group (e.g., Software Engineer, Data / AI, Design, Multimedia) + * responses: + * 200: + * description: Successfully retrieved internship vacancies by specialization group + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Successfully retrieved internship vacancies by specialization group" + * data: + * type: array + * items: + * $ref: '#/components/schemas/LowonganMagang' + * 404: + * description: "No internship vacancies found for this specialization group" + * 500: + * description: Internal server error + */ +router.get("/get/kelompok/:kelompok_peminatan", lowonganMagang.getLowonganByKelompokPeminatan); + +/** + * @swagger + * /lowongan-magang-api/get/kelompok-all: + * get: + * summary: Get all distinct specialization groups (kelompok_peminatan) + * tags: [Lowongan Magang] + * responses: + * 200: + * description: Successfully retrieved all specialization groups + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Successfully retrieved all specialization group" + * data: + * type: array + * items: + * type: object + * properties: + * kelompok_peminatan: + * type: string + * example: "Software Engineer" + * 404: + * description: No internship specialization group found + * 500: + * description: Internal server error + */ +router.get("/get/kelompok-all", lowonganMagang.getAllKelompokPeminatan); + +/** + * @swagger + * /lowongan-magang-api/update/{id}: + * patch: + * summary: "Update a specific lowongan magang by ID" + * tags: [Lowongan Magang] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: "The ID of the lowongan magang" + * requestBody: + * required: false + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * posisi: + * type: string + * example: "Full Stack Developer" + * kelompok_peminatan: + * type: string + * example: "Software Engineering" + * deskripsi: + * type: string + * example: "Develop web applications" + * persyaratan: + * type: string + * example: "Bachelor in Computer Science" + * paid: + * type: string + * enum: [paid, unpaid] + * example: "paid" + * durasi_awal: + * type: string + * format: date + * example: "2025-07-01" + * durasi_akhir: + * type: string + * format: date + * example: "2025-09-01" + * image: + * type: string + * format: binary + * description: "Optional: New image for the lowongan (JPG, PNG)" + * responses: + * 200: + * description: "Internship vacancy updated successfully" + * 403: + * description: "Access denied: Only admins can access this route" + * 404: + * description: "Internship vacancy not found" + * 413: + * description: "File size too large (max 5MB)" + * 417: + * description: "Validation error" + * 500: + * description: "Internal server error" + */ +router.patch("/update/:id", verifyJWT, isAdmin, multer.single("image"), multerErrorHandler, lowonganMagang.updateLowonganMagang); + +/** + * @swagger + * /lowongan-magang-api/delete/{id}: + * delete: + * summary: "Delete a specific lowongan magang by ID" + * tags: [Lowongan Magang] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: "The ID of the lowongan magang" + * responses: + * 200: + * description: "Internship vacancy deleted successfully" + * 403: + * description: "Access denied: Only admins can access this route" + * 404: + * description: "Internship vacancy not found" + * 500: + * description: "Internal server error" + */ +router.delete("/delete/:id", verifyJWT, isAdmin, lowonganMagang.deleteLowonganMagangById); + +module.exports = router; diff --git a/src/routes/mahasiswa.routes.js b/src/routes/mahasiswa.routes.js new file mode 100755 index 0000000..da21582 --- /dev/null +++ b/src/routes/mahasiswa.routes.js @@ -0,0 +1,77 @@ +const express = require('express'); +const { mahasiswaController } = require('../modules/mahasiswa'); +const { verifyJWT } = require('../middleware/verifyJWT'); + +const router = express.Router(); + +/** + * @swagger + * components: + * schemas: + * Mahasiswa: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * nama_depan: + * type: string + * example: "Wowo" + * nama_belakang: + * type: string + * example: "Wewe" + * email: + * type: string + * example: "yntkts@example.com" + * kontak: + * type: string + * example: "08123456789" + * jurusan: + * type: string + * example: "Informatika" + * role: + * type: string + * example: "mahasiswa" + * cv_path: + * type: string + * example: "/uploads/cv.pdf" + * portofolio_path: + * type: string + * example: "/uploads/portofolio.pdf" + * motivasi: + * type: string + * example: "I want to gain experience in software development." + * relevant_skills: + * type: string + * example: "JavaScript, Node.js" + */ + +/** + * @swagger + * /mahasiswa-api/get: + * get: + * summary: Get all mahasiswa + * tags: [Mahasiswa] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Successfully retrieved all mahasiswa + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Data mahasiswa berhasil diambil" + * data: + * type: array + * items: + * $ref: '#/components/schemas/Mahasiswa' + * 500: + * description: Internal server error + */ +router.get('/get', verifyJWT, mahasiswaController.getAllMahasiswa); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/partnership.routes.js b/src/routes/partnership.routes.js new file mode 100755 index 0000000..3cb7be5 --- /dev/null +++ b/src/routes/partnership.routes.js @@ -0,0 +1,249 @@ +const express = require('express'); +const { partnershipController } = require('../modules/partnership'); +const { verifyJWT } = require('../middleware/verifyJWT'); +const isAdmin = require('../middleware/isAdmin'); +const multer = require('../middleware/multer'); +const { multerErrorHandler } = require('../middleware/multer'); + +const router = express.Router(); + +/** + * @swagger + * components: + * schemas: + * Partnership: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * nama_partner: + * type: string + * example: "Umbrella Corp" + * image_path: + * type: string + * example: "/uploads/logo.png" + * created_at: + * type: string + * format: date-time + * example: "2025-06-19T10:00:00Z" + */ + +/** + * @swagger + * /partnership-api/add: + * post: + * summary: Add a new partnership + * tags: [Partnership] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * nama_partner: + * type: string + * description: Name of the partner + * example: "Tech Corp" + * image: + * type: string + * format: binary + * responses: + * 200: + * description: Partnership added successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Data partnership berhasil ditambahkan" + * data: + * type: object + * properties: + * nama_partner: + * type: string + * example: "Umbrella Corp" + * image_path: + * type: string + * example: "/uploads/logo.png" + * 413: + * description: File size too large (max 5MB) + * 500: + * description: Internal server error + */ +router.post('/add', verifyJWT, isAdmin, multer.single('image'), multerErrorHandler, partnershipController.addPartnership); + +/** + * @swagger + * /partnership-api/get: + * get: + * summary: Get all partnerships + * tags: [Partnership] + * responses: + * 200: + * description: Successfully retrieved all partnerships + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Data partnership berhasil diambil" + * data: + * type: array + * items: + * $ref: '#/components/schemas/Partnership' + * 404: + * description: No partnership data found + * 500: + * description: Internal server error + */ +router.get('/get', partnershipController.getPartnership); + +/** + * @swagger + * /partnership-api/get/{id}: + * get: + * summary: Get partnership data by ID + * tags: [Partnership] + * parameters: + * - in: path + * name: id + * required: true + * description: The ID of the partnership + * schema: + * type: string + * responses: + * 200: + * description: Partnership data was successfully collected + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "patnership data was successfully collected" + * data: + * type: array + * items: + * $ref: '#/components/schemas/Partnership' + * 404: + * description: Partnership not found + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "partnership not found" + * 500: + * description: Internal server error + */ +router.get('/get/:id', partnershipController.getPartnershipById); + +/** + * @swagger + * /partnership-api/update/{id}: + * patch: + * summary: Update a partnership's name or image + * tags: [Partnership] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * description: The ID of the partnership to update + * schema: + * type: string + * requestBody: + * required: false + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * nama_partner: + * type: string + * image: + * type: string + * format: binary + * responses: + * 200: + * description: Partnership successfully updated + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Partnership successfully updated" + * data: + * type: object + * properties: + * id: + * type: string + * nama_partner: + * type: string + * image_path: + * type: string + * 404: + * description: Partnership not found + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Partnership not found" + * 413: + * description: File size too large (max 5MB) + * 500: + * description: Internal server error + */ +router.patch('/update/:id', verifyJWT, isAdmin, multer.single('image'), multerErrorHandler, partnershipController.updatePartnership); + +/** + * @swagger + * /partnership-api/delete/{id}: + * delete: + * summary: Delete a partnership by ID + * tags: [Partnership] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: ID of the partnership to delete + * responses: + * 200: + * description: Partnership deleted successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Data partnership berhasil dihapus" + * 404: + * description: Partnership not found + * 500: + * description: Internal server error + */ +router.delete('/delete/:id', verifyJWT, isAdmin, partnershipController.deletePartnership); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/partnership.ts b/src/routes/partnership.ts deleted file mode 100644 index 0102df8..0000000 --- a/src/routes/partnership.ts +++ /dev/null @@ -1,401 +0,0 @@ -import { Router, Request, Response, NextFunction } from 'express'; -import { protect, restrictTo, AuthenticatedRequest } from '../middleware/auth'; -import prisma from '../lib/prisma'; -import { upload } from '../middleware/upload'; -import { CustomError } from '../middleware/errorHandler'; - -const router = Router(); - -// Map data partnership dari DB ke format frontend -function mapToFrontend(partner: any) { - return { - id: partner.id, - nama_partner: partner.namaPartner, - image_path: partner.imagePath, - createdAt: partner.createdAt - }; -} - -/** - * @swagger - * /api/partnership-api/get: - * get: - * summary: Ambil seluruh data partnership - * description: Mengembalikan daftar seluruh mitra partnership. - * tags: [Partnership] - * responses: - * 200: - * description: Data partnership berhasil diambil - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * data: - * type: array - * items: - * type: object - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.get('/get', async (_req: Request, res: Response, next: NextFunction) => { - try { - const partners = await prisma.partnership.findMany({ - orderBy: { createdAt: 'desc' } - }); - res.status(200).json({ - status: true, - data: partners.map(mapToFrontend) - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/partnership-api/get/{id}: - * get: - * summary: Ambil detail partnership berdasarkan ID - * description: Mengembalikan detail satu data partnership. - * tags: [Partnership] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * description: ID partnership - * responses: - * 200: - * description: Detail partnership berhasil diambil - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * data: - * type: array - * items: - * type: object - * 404: - * description: Partnership tidak ditemukan - * content: - * application/json: - * example: - * status: error - * statusCode: 404 - * message: Partnership tidak ditemukan - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.get('/get/:id', async (req: Request, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const partner = await prisma.partnership.findFirst({ - where: { id: parseInt(id, 10) } - }); - - if (!partner) { - const error: CustomError = new Error('Partnership tidak ditemukan'); - error.statusCode = 404; - return next(error); - } - - res.status(200).json({ - status: true, - data: [mapToFrontend(partner)] // Kadang list/detail di frontend minta dibungkus array - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/partnership-api/add: - * post: - * summary: Tambah partnership baru - * description: Menambahkan data partnership baru. Hanya dapat diakses oleh admin. - * tags: [Partnership] - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * multipart/form-data: - * schema: - * type: object - * required: - * - nama_partner - * - image - * properties: - * nama_partner: - * type: string - * example: PT Teknologi Nusantara - * image: - * type: string - * format: binary - * responses: - * 201: - * description: Partnership berhasil ditambahkan - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * message: - * type: string - * example: Partnership berhasil ditambahkan - * 400: - * description: Validasi input partnership gagal - * content: - * application/json: - * examples: - * nama_wajib: - * value: - * status: error - * statusCode: 400 - * message: Nama partnership harus diisi - * image_wajib: - * value: - * status: error - * statusCode: 400 - * message: Thumbnail/image harus diunggah - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 403: - * $ref: '#/components/responses/ForbiddenError' - * 500: - * description: Terjadi kesalahan server (termasuk validasi file upload) - * content: - * application/json: - * examples: - * file_bukan_gambar: - * value: - * status: error - * statusCode: 500 - * message: File upload harus berupa gambar! - * internal: - * value: - * status: error - * statusCode: 500 - * message: Internal Server Error - */ -router.post('/add', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - try { - const { nama_partner } = req.body; - if (!nama_partner) { - const error: CustomError = new Error('Nama partnership harus diisi'); - error.statusCode = 400; - return next(error); - } - - if (!req.file) { - const error: CustomError = new Error('Thumbnail/image harus diunggah'); - error.statusCode = 400; - return next(error); - } - - const imagePath = `/uploads/images/${req.file.filename}`; - - const partner = await prisma.partnership.create({ - data: { - namaPartner: nama_partner, - imagePath - } - }); - - res.status(201).json({ - status: true, - message: 'Partnership berhasil ditambahkan', - data: mapToFrontend(partner) - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/partnership-api/update/{id}: - * patch: - * summary: Perbarui data partnership - * description: Memperbarui data partnership berdasarkan ID. Hanya dapat diakses oleh admin. - * tags: [Partnership] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * description: ID partnership - * requestBody: - * required: false - * content: - * multipart/form-data: - * schema: - * type: object - * properties: - * nama_partner: - * type: string - * image: - * type: string - * format: binary - * responses: - * 200: - * description: Partnership berhasil diperbarui - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * message: - * type: string - * example: Partnership berhasil diupdate - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 403: - * $ref: '#/components/responses/ForbiddenError' - * 404: - * description: Partnership tidak ditemukan - * content: - * application/json: - * example: - * status: error - * statusCode: 404 - * message: Partnership tidak ditemukan - * 500: - * description: Terjadi kesalahan server (termasuk validasi file upload) - * content: - * application/json: - * examples: - * file_bukan_gambar: - * value: - * status: error - * statusCode: 500 - * message: File upload harus berupa gambar! - * internal: - * value: - * status: error - * statusCode: 500 - * message: Internal Server Error - */ -router.patch('/update/:id', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const { nama_partner } = req.body; - - const existing = await prisma.partnership.findUnique({ - where: { id: parseInt(id, 10) } - }); - - if (!existing) { - const error: CustomError = new Error('Partnership tidak ditemukan'); - error.statusCode = 404; - return next(error); - } - - let imagePath = existing.imagePath; - if (req.file) { - imagePath = `/uploads/images/${req.file.filename}`; - } - - const updated = await prisma.partnership.update({ - where: { id: parseInt(id, 10) }, - data: { - namaPartner: nama_partner ?? existing.namaPartner, - imagePath - } - }); - - res.status(200).json({ - status: true, - message: 'Partnership berhasil diupdate', - data: mapToFrontend(updated) - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/partnership-api/delete/{id}: - * delete: - * summary: Hapus partnership - * description: Menghapus data partnership berdasarkan ID. Hanya dapat diakses oleh admin. - * tags: [Partnership] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * description: ID partnership - * responses: - * 200: - * description: Partnership berhasil dihapus - * content: - * application/json: - * example: - * status: true - * message: Partnership berhasil dihapus - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 403: - * $ref: '#/components/responses/ForbiddenError' - * 404: - * description: Partnership tidak ditemukan - * content: - * application/json: - * example: - * status: error - * statusCode: 404 - * message: Partnership tidak ditemukan - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const existing = await prisma.partnership.findUnique({ - where: { id: parseInt(id, 10) } - }); - - if (!existing) { - const error: CustomError = new Error('Partnership tidak ditemukan'); - error.statusCode = 404; - return next(error); - } - - await prisma.partnership.delete({ - where: { id: parseInt(id, 10) } - }); - - res.status(200).json({ - status: true, - message: 'Partnership berhasil dihapus' - }); - } catch (error) { - next(error); - } -}); - -export default router; diff --git a/src/routes/research.ts b/src/routes/research.ts deleted file mode 100644 index 70de8c9..0000000 --- a/src/routes/research.ts +++ /dev/null @@ -1,404 +0,0 @@ -import { Router, Request, Response, NextFunction } from 'express'; -import { protect, restrictTo, AuthenticatedRequest } from '../middleware/auth'; -import prisma from '../lib/prisma'; -import { upload } from '../middleware/upload'; -import { CustomError } from '../middleware/errorHandler'; - -const router = Router(); - -// Map data research product dari DB ke format frontend -function mapToFrontend(product: any) { - return { - id: product.id, - nama_project: product.namaProject, - deskripsi: product.deskripsi, - link_project: product.linkProject, - image_path: product.imagePath, - createdAt: product.createdAt - }; -} - -/** - * @swagger - * /api/hasil-research-api/get: - * get: - * summary: Ambil semua hasil research - * description: Mengembalikan daftar seluruh project hasil research. - * tags: [Hasil Research] - * responses: - * 200: - * description: Data hasil research berhasil diambil - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * data: - * type: array - * items: - * type: object - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.get('/get', async (_req: Request, res: Response, next: NextFunction) => { - try { - const products = await prisma.hasilResearch.findMany({ - orderBy: { createdAt: 'desc' } - }); - res.status(200).json({ - status: true, - data: products.map(mapToFrontend) - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/hasil-research-api/get/{id}: - * get: - * summary: Ambil detail hasil research berdasarkan ID - * description: Mengembalikan detail satu project hasil research. - * tags: [Hasil Research] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * description: ID hasil research - * responses: - * 200: - * description: Detail hasil research berhasil diambil - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * data: - * type: object - * 404: - * description: Project tidak ditemukan - * content: - * application/json: - * example: - * status: error - * statusCode: 404 - * message: Project tidak ditemukan - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.get('/get/:id', async (req: Request, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const product = await prisma.hasilResearch.findUnique({ - where: { id: parseInt(id, 10) } - }); - - if (!product) { - const error: CustomError = new Error('Project tidak ditemukan'); - error.statusCode = 404; - return next(error); - } - - res.status(200).json({ - status: true, - data: mapToFrontend(product) - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/hasil-research-api/add: - * post: - * summary: Tambah hasil research baru - * description: Menambahkan data project hasil research baru. Hanya dapat diakses oleh admin. - * tags: [Hasil Research] - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * multipart/form-data: - * schema: - * type: object - * required: - * - nama_project - * - deskripsi - * - link_project - * properties: - * nama_project: - * type: string - * example: Sistem Monitoring Tanaman IoT - * deskripsi: - * type: string - * example: Project monitoring kelembapan tanah berbasis IoT. - * link_project: - * type: string - * example: https://example.com/project/monitoring-tanaman - * image: - * type: string - * format: binary - * responses: - * 201: - * description: Hasil research berhasil ditambahkan - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * message: - * type: string - * example: Research project berhasil ditambahkan - * 400: - * description: Validasi input hasil research gagal - * content: - * application/json: - * example: - * status: error - * statusCode: 400 - * message: Mohon lengkapi semua field yang wajib diisi - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 403: - * $ref: '#/components/responses/ForbiddenError' - * 500: - * description: Terjadi kesalahan server (termasuk validasi file upload) - * content: - * application/json: - * examples: - * file_bukan_gambar: - * value: - * status: error - * statusCode: 500 - * message: File upload harus berupa gambar! - * internal: - * value: - * status: error - * statusCode: 500 - * message: Internal Server Error - */ -router.post('/add', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - try { - const { nama_project, deskripsi, link_project } = req.body; - - if (!nama_project || !deskripsi || !link_project) { - const error: CustomError = new Error('Mohon lengkapi semua field yang wajib diisi'); - error.statusCode = 400; - return next(error); - } - - const imagePath = req.file ? `/uploads/images/${req.file.filename}` : ''; - - const product = await prisma.hasilResearch.create({ - data: { - namaProject: nama_project, - deskripsi, - linkProject: link_project, - imagePath - } - }); - - res.status(201).json({ - status: true, - message: 'Research project berhasil ditambahkan', - data: mapToFrontend(product) - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/hasil-research-api/update/{id}: - * patch: - * summary: Perbarui hasil research - * description: Memperbarui data hasil research berdasarkan ID. Hanya dapat diakses oleh admin. - * tags: [Hasil Research] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * description: ID hasil research - * requestBody: - * required: false - * content: - * multipart/form-data: - * schema: - * type: object - * properties: - * nama_project: - * type: string - * deskripsi: - * type: string - * link_project: - * type: string - * image: - * type: string - * format: binary - * responses: - * 200: - * description: Hasil research berhasil diperbarui - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: true - * message: - * type: string - * example: Research project berhasil diupdate - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 403: - * $ref: '#/components/responses/ForbiddenError' - * 404: - * description: Project tidak ditemukan - * content: - * application/json: - * example: - * status: error - * statusCode: 404 - * message: Project tidak ditemukan - * 500: - * description: Terjadi kesalahan server (termasuk validasi file upload) - * content: - * application/json: - * examples: - * file_bukan_gambar: - * value: - * status: error - * statusCode: 500 - * message: File upload harus berupa gambar! - * internal: - * value: - * status: error - * statusCode: 500 - * message: Internal Server Error - */ -router.patch('/update/:id', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const { nama_project, deskripsi, link_project } = req.body; - - const existing = await prisma.hasilResearch.findUnique({ - where: { id: parseInt(id, 10) } - }); - - if (!existing) { - const error: CustomError = new Error('Project tidak ditemukan'); - error.statusCode = 404; - return next(error); - } - - let imagePath = existing.imagePath; - if (req.file) { - imagePath = `/uploads/images/${req.file.filename}`; - } - - const updated = await prisma.hasilResearch.update({ - where: { id: parseInt(id, 10) }, - data: { - namaProject: nama_project ?? existing.namaProject, - deskripsi: deskripsi ?? existing.deskripsi, - linkProject: link_project ?? existing.linkProject, - imagePath - } - }); - - res.status(200).json({ - status: true, - message: 'Research project berhasil diupdate', - data: mapToFrontend(updated) - }); - } catch (error) { - next(error); - } -}); - -/** - * @swagger - * /api/hasil-research-api/delete/{id}: - * delete: - * summary: Hapus hasil research - * description: Menghapus data hasil research berdasarkan ID. Hanya dapat diakses oleh admin. - * tags: [Hasil Research] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * description: ID hasil research - * responses: - * 200: - * description: Hasil research berhasil dihapus - * content: - * application/json: - * example: - * status: true - * message: Research project berhasil dihapus - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 403: - * $ref: '#/components/responses/ForbiddenError' - * 404: - * description: Project tidak ditemukan - * content: - * application/json: - * example: - * status: error - * statusCode: 404 - * message: Project tidak ditemukan - * 500: - * $ref: '#/components/responses/InternalServerError' - */ -router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const existing = await prisma.hasilResearch.findUnique({ - where: { id: parseInt(id, 10) } - }); - - if (!existing) { - const error: CustomError = new Error('Project tidak ditemukan'); - error.statusCode = 404; - return next(error); - } - - await prisma.hasilResearch.delete({ - where: { id: parseInt(id, 10) } - }); - - res.status(200).json({ - status: true, - message: 'Research project berhasil dihapus' - }); - } catch (error) { - next(error); - } -}); - -export default router; diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index c9ea67b..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "compilerOptions": { - "target": "es2022", - "module": "commonjs", - "lib": [ - "es2022" - ], - "allowJs": true, - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "noImplicitAny": true, - "strictNullChecks": true, - "strictFunctionTypes": true, - "noImplicitThis": true, - "alwaysStrict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - }, - "include": [ - "src/**/*" - ], - "exclude": [ - "node_modules", - "dist" - ] -} \ No newline at end of file diff --git a/uploads/docs/.gitkeep b/uploads/docs/.gitkeep deleted file mode 100644 index 09c6916..0000000 --- a/uploads/docs/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# Keep directory diff --git a/uploads/docs/cv-1780226578695-76608645.pdf b/uploads/docs/cv-1780226578695-76608645.pdf deleted file mode 100644 index f43b717..0000000 --- a/uploads/docs/cv-1780226578695-76608645.pdf +++ /dev/null @@ -1 +0,0 @@ -%PDF-1.4 mock cv \ No newline at end of file diff --git a/uploads/docs/portofolio-1780226578696-235536074.pdf b/uploads/docs/portofolio-1780226578696-235536074.pdf deleted file mode 100644 index ccb4686..0000000 --- a/uploads/docs/portofolio-1780226578696-235536074.pdf +++ /dev/null @@ -1 +0,0 @@ -%PDF-1.4 mock portfolio \ No newline at end of file diff --git a/uploads/images/.gitkeep b/uploads/images/.gitkeep deleted file mode 100644 index 09c6916..0000000 --- a/uploads/images/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# Keep directory diff --git a/uploads/images/image-1780226578634-893133582.png b/uploads/images/image-1780226578634-893133582.png deleted file mode 100644 index c9d83be..0000000 --- a/uploads/images/image-1780226578634-893133582.png +++ /dev/null @@ -1 +0,0 @@ -fake-image-content \ No newline at end of file diff --git a/vercel.json b/vercel.json new file mode 100755 index 0000000..43b3d38 --- /dev/null +++ b/vercel.json @@ -0,0 +1,22 @@ +{ + "version": 2, + "buildCommand": "npm run vercel-build", + "builds": [ + { + "src": "src/index.js", + "use": "@vercel/node", + "config": { + "includeFiles": ["node_modules/swagger-ui-dist/**"] + } + } + ], + "routes": [ + { + "src": "/(.*)", + "dest": "src/index.js" + } + ], + "env": { + "NODE_ENV": "production" + } +}