Commit 28f4b1a0 authored by ThinhNC's avatar ThinhNC

refactor: remove applications, interns, and tasks modules; update user module and database schema

parent 34f8f91b
node_modules
dist
.env
.env.local
.DS_Store
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
coverage
.vscode
.idea
*.log
NODE_ENV=development
PORT=3000
PORT=7777
DATABASE_URL=postgresql://postgres.[project-ref]:[password]@aws-1-[region].pooler.supabase.com:6543/postgres?pgbouncer=true
DIRECT_URL=postgresql://postgres:[password]@db.[project-ref].supabase.co:5432/postgres
DB_HOST=aws-1-[region].pooler.supabase.com
DB_PORT=6543
DB_USER=postgres.[project-ref]
DB_PASSWORD=[password]
DB_NAME=postgres
SUPABASE_URL=https://[project-ref].supabase.co
SUPABASE_PUBLISHABLE_KEY=your_supabase_publishable_key
SUPABASE_SECRET_KEY=your_supabase_secret_key
SUPABASE_JWKS_URL=https://[project-ref].supabase.co/auth/v1/.well-known/jwks.json
DATABASE_URL="postgresql://postgres:[PASSWORD]@localhost:5432/datafinwise?schema=public"
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=[PASSWORD]
DB_NAME=datafinwise
JWT_ACCESS_SECRET=change_me_access_secret
JWT_REFRESH_SECRET=change_me_refresh_secret
JWT_ACCESS_EXPIRES_IN=1d
JWT_REFRESH_EXPIRES_IN=7d
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
\ No newline at end of file
REDIS_HOST=redis
REDIS_PORT=6381
\ No newline at end of file
# NexCampus BE - Intern Management System
# FinWise BE - Sổ tay Chi tiêu & Báo cáo Tài chính (Zalo Mini App)
Backend API service for managing interns, supervisors (leaders), tasks, submissions, evaluations, and notifications.
Backend API service for FinWise mini-app. Hiện tại backend cung cấp:
- Xác thực người dùng JWT
- Quản lý tài khoản người dùng
- Health check hệ thống
## Tech Stack
- **Node.js** & **Express** with **TypeScript**
- **Prisma ORM** with **PostgreSQL** (Supabase)
- **JWT Authentication** & **Role-Based Access Control**
......@@ -10,32 +15,31 @@ Backend API service for managing interns, supervisors (leaders), tasks, submissi
---
## Database Design & Models
The system is structured around 12 core models:
1. **Role**: Represents user authorizations (`ADMIN`, `LEADER`, `INTERN`).
2. **User**: Credentials, active status, and role reference.
3. **Application**: Registry/application form details. Spawns a User account upon approval.
4. **InternProfile**: Profile details for approved interns, linking them to their corresponding `User` and supervisor (`User` with role `LEADER`).
5. **NotificationSetting**: Communication channels configuration per user.
6. **Task**: Assignments created by administrators or leaders.
7. **TaskAssignment**: Task assignations linking exactly one task to one intern.
8. **TaskSubmission**: Intern submissions containing code links (PR), video demos, notes, and reviewer comments. Supports multiple attempts.
9. **DailyReport**: Log reports written daily by interns.
10. **WeeklyEvaluation**: Evaluative scoring (communication, attitude, learning, coding, total score) performed weekly by leaders.
11. **Notification**: In-app notifications.
12. **NotificationLog**: Delivery logs tracking web, email, and Discord channel notifications.
## Core Models
Dựa trên Prisma schema hiện tại, repository chứa các model chính sau:
- **User**: Email, mật khẩu, tên, trạng thái và role.
- **Wallet**: Ví người dùng.
- **Category**: Danh mục thu/chi.
- **Transaction**: Giao dịch thu/chi.
- **Budget**: Ngân sách theo danh mục.
- **RefreshToken**: Lưu refresh token cho đăng nhập lâu dài.
> Lưu ý: hiện tại các route public/active trong project chỉ bao gồm Auth và Users.
---
## Getting Started
### Prerequisites
- Node.js (v18+)
- pnpm (recommended) or npm
### Installation
1. Clone the repository
1. Clone the repository.
2. Install dependencies:
```bash
pnpm install
......@@ -51,38 +55,50 @@ The system is structured around 12 core models:
```
### Database Initialization
Apply migrations and run the seeding script to populate initial roles and create the default admin account:
Apply migrations and run the seeding script:
```bash
# Validate schema
# Validate Prisma schema
npx prisma validate
# Run migrations
npx prisma migrate dev --name init_intern_mgmt
npx prisma migrate dev --name init_finwise
# Seed initial data (Roles & Default Admin)
# Seed initial data
npm run db:seed
```
*Default Admin credentials:*
- **Email**: `admin@nexcampus.local`
_Default Admin credentials:_
- **Email**: `admin@finwise.local`
- **Password**: `Admin@123456`
### Running the App
Start the development server:
```bash
npm run dev
```
The server runs on http://localhost:3000 by default.
Swagger UI is available at `http://localhost:3000/api/docs`.
---
## API Endpoints
- **Auth Routing (`/api/v1/auth`)**:
- `POST /login`: User authentication.
- **System**:
- `GET /api/v1/health`: Health check.
- **Auth** (`/api/v1/auth`):
- `POST /login`: Authenticate user and receive access/refresh tokens.
- `GET /me`: Get current authenticated user details.
- **Users Routing (`/api/v1/users`)** (Admin only):
- `GET /`: Get all users.
- `POST /refresh`: Refresh JWT tokens.
- `POST /logout`: Invalidate refresh token.
- **Users** (`/api/v1/users`) (Admin only):
- `GET /`: Get all users with optional filters, sorting, and pagination.
- `GET /:id`: Get user details by ID.
- `POST /`: Create a new user account.
- `PUT /:id`: Update user details.
- `PUT /:id`: Update user status or role.
version: '3.8'
version: "3.8"
services:
postgres:
image: postgres:16-alpine
container_name: crawl_data_postgres
container_name: finwise_postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: crawl_data_db
POSTGRES_USER: "${DB_USER:-postgres}"
POSTGRES_PASSWORD: "${DB_PASSWORD:-postgres}"
POSTGRES_DB: "${DB_NAME:-datafinwise}"
ports:
- "5432:5432"
- "${DB_PORT:-5432}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
container_name: crawl_data_redis
container_name: finwise_redis
ports:
- "6379:6379"
- "${REDIS_PORT:-6381}:6379"
# app:
# build:
# context: .
# dockerfile: Dockerfile
# container_name: finwise_app
# env_file:
# - .env
# environment:
# NODE_ENV: "${NODE_ENV:-development}"
# PORT: "${PORT:-8888}"
# DB_HOST: postgres
# DB_PORT: "5432"
# DB_USER: "${DB_USER:-postgres}"
# DB_PASSWORD: "${DB_PASSWORD:-postgres}"
# DB_NAME: "${DB_NAME:-datafinwise}"
# DATABASE_URL: "postgresql://${DB_USER:-postgres}:${DB_PASSWORD:-postgres}@postgres:5432/${DB_NAME:-datafinwise}?schema=public"
# JWT_ACCESS_SECRET: "${JWT_ACCESS_SECRET:-default_access_secret}"
# JWT_REFRESH_SECRET: "${JWT_REFRESH_SECRET:-default_refresh_secret}"
# JWT_ACCESS_EXPIRES_IN: "${JWT_ACCESS_EXPIRES_IN:-1d}"
# JWT_REFRESH_EXPIRES_IN: "${JWT_REFRESH_EXPIRES_IN:-7d}"
# REDIS_HOST: redis
# REDIS_PORT: "6379"
# volumes:
# - ./:/usr/src/app
# - /usr/src/app/node_modules
# ports:
# - "${PORT:-8888}:${PORT:-8888}"
# depends_on:
# - postgres
# - redis
# command: sh -c "corepack enable && pnpm install && pnpm dev"
volumes:
postgres_data:
{
"name": "data-crawler-be",
"name": "finwise-miniapp-be",
"version": "1.0.0",
"description": "Backend API for data crawling service",
"description": "Backend API service for FinWise - Sổ tay Chi tiêu & Báo cáo Tài chính (Zalo Mini App)",
"main": "dist/server.js",
"packageManager": "pnpm@9.15.0",
"scripts": {
......
-- CreateEnum
CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'CRAWLER_USER', 'VIEWER');
-- CreateEnum
CREATE TYPE "CrawlJobStatus" AS ENUM ('PENDING', 'RUNNING', 'COMPLETED', 'PARTIAL_COMPLETED', 'FAILED', 'CANCELED', 'BLOCKED');
-- CreateEnum
CREATE TYPE "CrawlMode" AS ENUM ('SCRAPE', 'CRAWL', 'SITEMAP', 'URL_LIST');
-- CreateEnum
CREATE TYPE "CrawlPageStatus" AS ENUM ('PENDING', 'SUCCESS', 'FAILED', 'BLOCKED', 'REQUIRES_LOGIN', 'CAPTCHA_DETECTED', 'PAYWALL_DETECTED', 'TIMEOUT', 'SKIPPED');
-- CreateEnum
CREATE TYPE "ExportType" AS ENUM ('JSON', 'CSV', 'XLSX', 'MARKDOWN', 'MARKDOWN_ZIP', 'FULL_ZIP');
-- CreateEnum
CREATE TYPE "ExportStatus" AS ENUM ('PENDING', 'PROCESSING', 'COMPLETED', 'FAILED');
-- CreateEnum
CREATE TYPE "AssetType" AS ENUM ('IMAGE', 'LINK', 'PDF', 'FILE', 'VIDEO', 'OTHER');
-- CreateTable
CREATE TABLE "users" (
"id" UUID NOT NULL,
"email" TEXT NOT NULL,
"password_hash" TEXT NOT NULL,
"full_name" TEXT,
"role" "UserRole" NOT NULL DEFAULT 'CRAWLER_USER',
"is_active" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "crawl_jobs" (
"id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"start_url" TEXT NOT NULL,
"domain" TEXT,
"mode" "CrawlMode" NOT NULL DEFAULT 'SCRAPE',
"status" "CrawlJobStatus" NOT NULL DEFAULT 'PENDING',
"max_pages" INTEGER NOT NULL DEFAULT 20,
"max_depth" INTEGER NOT NULL DEFAULT 1,
"total_pages" INTEGER NOT NULL DEFAULT 0,
"success_pages" INTEGER NOT NULL DEFAULT 0,
"failed_pages" INTEGER NOT NULL DEFAULT 0,
"error_message" TEXT,
"started_at" TIMESTAMP(3),
"finished_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "crawl_jobs_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "crawl_pages" (
"id" UUID NOT NULL,
"job_id" UUID NOT NULL,
"url" TEXT NOT NULL,
"title" TEXT,
"description" TEXT,
"markdown_content" TEXT,
"html_content_path" TEXT,
"status" "CrawlPageStatus" NOT NULL DEFAULT 'PENDING',
"status_code" INTEGER,
"error_message" TEXT,
"crawled_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "crawl_pages_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "crawl_assets" (
"id" UUID NOT NULL,
"job_id" UUID NOT NULL,
"page_id" UUID,
"asset_type" "AssetType" NOT NULL,
"url" TEXT NOT NULL,
"source_url" TEXT,
"alt_text" TEXT,
"mime_type" TEXT,
"order_index" INTEGER,
"css_selector" TEXT,
"dom_path" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "crawl_assets_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "crawl_exports" (
"id" UUID NOT NULL,
"job_id" UUID NOT NULL,
"export_type" "ExportType" NOT NULL,
"status" "ExportStatus" NOT NULL DEFAULT 'PENDING',
"file_name" TEXT NOT NULL,
"file_path" TEXT NOT NULL,
"file_size" INTEGER,
"mime_type" TEXT,
"error_message" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "crawl_exports_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE INDEX "crawl_jobs_user_id_idx" ON "crawl_jobs"("user_id");
-- CreateIndex
CREATE INDEX "crawl_jobs_status_idx" ON "crawl_jobs"("status");
-- CreateIndex
CREATE INDEX "crawl_jobs_created_at_idx" ON "crawl_jobs"("created_at");
-- CreateIndex
CREATE INDEX "crawl_pages_job_id_idx" ON "crawl_pages"("job_id");
-- CreateIndex
CREATE INDEX "crawl_pages_status_idx" ON "crawl_pages"("status");
-- CreateIndex
CREATE INDEX "crawl_assets_job_id_idx" ON "crawl_assets"("job_id");
-- CreateIndex
CREATE INDEX "crawl_assets_page_id_idx" ON "crawl_assets"("page_id");
-- CreateIndex
CREATE INDEX "crawl_assets_asset_type_idx" ON "crawl_assets"("asset_type");
-- CreateIndex
CREATE INDEX "crawl_exports_job_id_idx" ON "crawl_exports"("job_id");
-- CreateIndex
CREATE INDEX "crawl_exports_export_type_idx" ON "crawl_exports"("export_type");
-- AddForeignKey
ALTER TABLE "crawl_jobs" ADD CONSTRAINT "crawl_jobs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "crawl_pages" ADD CONSTRAINT "crawl_pages_job_id_fkey" FOREIGN KEY ("job_id") REFERENCES "crawl_jobs"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "crawl_assets" ADD CONSTRAINT "crawl_assets_job_id_fkey" FOREIGN KEY ("job_id") REFERENCES "crawl_jobs"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "crawl_assets" ADD CONSTRAINT "crawl_assets_page_id_fkey" FOREIGN KEY ("page_id") REFERENCES "crawl_pages"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "crawl_exports" ADD CONSTRAINT "crawl_exports_job_id_fkey" FOREIGN KEY ("job_id") REFERENCES "crawl_jobs"("id") ON DELETE CASCADE ON UPDATE CASCADE;
/*
Warnings:
- You are about to drop the column `full_name` on the `users` table. All the data in the column will be lost.
- You are about to drop the column `password_hash` on the `users` table. All the data in the column will be lost.
- You are about to drop the column `role` on the `users` table. All the data in the column will be lost.
- You are about to drop the `crawl_assets` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `crawl_exports` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `crawl_jobs` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `crawl_pages` table. If the table is not empty, all the data it contains will be lost.
- Added the required column `password` to the `users` table without a default value. This is not possible if the table is not empty.
- Added the required column `role_id` to the `users` table without a default value. This is not possible if the table is not empty.
*/
-- CreateEnum
CREATE TYPE "ApplicationStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED');
-- CreateEnum
CREATE TYPE "InternStatus" AS ENUM ('ACTIVE', 'COMPLETED', 'DROPPED');
-- CreateEnum
CREATE TYPE "TaskPriority" AS ENUM ('LOW', 'MEDIUM', 'HIGH');
-- CreateEnum
CREATE TYPE "AssignmentStatus" AS ENUM ('TODO', 'IN_PROGRESS', 'REVIEW', 'DONE');
-- CreateEnum
CREATE TYPE "ReviewStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED');
-- CreateEnum
CREATE TYPE "NotificationChannel" AS ENUM ('WEB', 'EMAIL', 'DISCORD');
-- CreateEnum
CREATE TYPE "NotificationLogStatus" AS ENUM ('SUCCESS', 'FAILED');
-- DropForeignKey
ALTER TABLE "crawl_assets" DROP CONSTRAINT "crawl_assets_job_id_fkey";
-- DropForeignKey
ALTER TABLE "crawl_assets" DROP CONSTRAINT "crawl_assets_page_id_fkey";
-- DropForeignKey
ALTER TABLE "crawl_exports" DROP CONSTRAINT "crawl_exports_job_id_fkey";
-- DropForeignKey
ALTER TABLE "crawl_jobs" DROP CONSTRAINT "crawl_jobs_user_id_fkey";
-- DropForeignKey
ALTER TABLE "crawl_pages" DROP CONSTRAINT "crawl_pages_job_id_fkey";
-- AlterTable
ALTER TABLE "users" DROP COLUMN "full_name",
DROP COLUMN "password_hash",
DROP COLUMN "role",
ADD COLUMN "password" TEXT NOT NULL,
ADD COLUMN "role_id" UUID NOT NULL;
-- DropTable
DROP TABLE "crawl_assets";
-- DropTable
DROP TABLE "crawl_exports";
-- DropTable
DROP TABLE "crawl_jobs";
-- DropTable
DROP TABLE "crawl_pages";
-- DropEnum
DROP TYPE "AssetType";
-- DropEnum
DROP TYPE "CrawlJobStatus";
-- DropEnum
DROP TYPE "CrawlMode";
-- DropEnum
DROP TYPE "CrawlPageStatus";
-- DropEnum
DROP TYPE "ExportStatus";
-- DropEnum
DROP TYPE "ExportType";
-- DropEnum
DROP TYPE "UserRole";
-- CreateTable
CREATE TABLE "roles" (
"id" UUID NOT NULL,
"name" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "roles_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "applications" (
"id" UUID NOT NULL,
"full_name" TEXT NOT NULL,
"email" TEXT NOT NULL,
"phone" TEXT NOT NULL,
"department" TEXT NOT NULL,
"position" TEXT NOT NULL,
"start_date" TIMESTAMP(3) NOT NULL,
"duration" INTEGER NOT NULL,
"status" "ApplicationStatus" NOT NULL DEFAULT 'PENDING',
"approved_by" UUID,
"approved_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "applications_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "intern_profiles" (
"id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"leader_id" UUID,
"full_name" TEXT NOT NULL,
"phone" TEXT NOT NULL,
"department" TEXT NOT NULL,
"position" TEXT NOT NULL,
"start_date" TIMESTAMP(3) NOT NULL,
"duration" INTEGER NOT NULL,
"discord_username" TEXT,
"discord_role_granted" BOOLEAN NOT NULL DEFAULT false,
"status" "InternStatus" NOT NULL DEFAULT 'ACTIVE',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "intern_profiles_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "notification_settings" (
"id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"web_enabled" BOOLEAN NOT NULL DEFAULT true,
"email_enabled" BOOLEAN NOT NULL DEFAULT true,
"discord_enabled" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "notification_settings_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "tasks" (
"id" UUID NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"deadline" TIMESTAMP(3) NOT NULL,
"priority" "TaskPriority" NOT NULL DEFAULT 'MEDIUM',
"created_by" UUID NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "tasks_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "task_assignments" (
"id" UUID NOT NULL,
"task_id" UUID NOT NULL,
"intern_id" UUID NOT NULL,
"assigned_by" UUID NOT NULL,
"status" "AssignmentStatus" NOT NULL DEFAULT 'TODO',
"assigned_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "task_assignments_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "task_submissions" (
"id" UUID NOT NULL,
"assignment_id" UUID NOT NULL,
"attempt" INTEGER NOT NULL,
"pr_link" TEXT,
"video_demo" TEXT,
"note" TEXT,
"review_status" "ReviewStatus" NOT NULL DEFAULT 'PENDING',
"review_comment" TEXT,
"reviewed_by" UUID,
"reviewed_at" TIMESTAMP(3),
"submitted_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "task_submissions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "daily_reports" (
"id" UUID NOT NULL,
"intern_id" UUID NOT NULL,
"content" TEXT NOT NULL,
"pr_link" TEXT,
"video_demo" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "daily_reports_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "weekly_evaluations" (
"id" UUID NOT NULL,
"intern_id" UUID NOT NULL,
"leader_id" UUID NOT NULL,
"week" INTEGER NOT NULL,
"communication" DOUBLE PRECISION NOT NULL,
"attitude" DOUBLE PRECISION NOT NULL,
"learning" DOUBLE PRECISION NOT NULL,
"coding" DOUBLE PRECISION NOT NULL,
"total_score" DOUBLE PRECISION NOT NULL,
"comment" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "weekly_evaluations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "notifications" (
"id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"title" TEXT NOT NULL,
"content" TEXT NOT NULL,
"type" TEXT NOT NULL,
"is_read" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "notifications_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "notification_logs" (
"id" UUID NOT NULL,
"notification_id" UUID NOT NULL,
"channel" "NotificationChannel" NOT NULL,
"status" "NotificationLogStatus" NOT NULL,
"sent_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "notification_logs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "roles_name_key" ON "roles"("name");
-- CreateIndex
CREATE UNIQUE INDEX "intern_profiles_user_id_key" ON "intern_profiles"("user_id");
-- CreateIndex
CREATE UNIQUE INDEX "notification_settings_user_id_key" ON "notification_settings"("user_id");
-- CreateIndex
CREATE UNIQUE INDEX "task_assignments_task_id_key" ON "task_assignments"("task_id");
-- CreateIndex
CREATE UNIQUE INDEX "weekly_evaluations_intern_id_week_key" ON "weekly_evaluations"("intern_id", "week");
-- AddForeignKey
ALTER TABLE "users" ADD CONSTRAINT "users_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "applications" ADD CONSTRAINT "applications_approved_by_fkey" FOREIGN KEY ("approved_by") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "intern_profiles" ADD CONSTRAINT "intern_profiles_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "intern_profiles" ADD CONSTRAINT "intern_profiles_leader_id_fkey" FOREIGN KEY ("leader_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "notification_settings" ADD CONSTRAINT "notification_settings_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "task_assignments" ADD CONSTRAINT "task_assignments_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "tasks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "task_assignments" ADD CONSTRAINT "task_assignments_intern_id_fkey" FOREIGN KEY ("intern_id") REFERENCES "intern_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "task_assignments" ADD CONSTRAINT "task_assignments_assigned_by_fkey" FOREIGN KEY ("assigned_by") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "task_submissions" ADD CONSTRAINT "task_submissions_assignment_id_fkey" FOREIGN KEY ("assignment_id") REFERENCES "task_assignments"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "task_submissions" ADD CONSTRAINT "task_submissions_reviewed_by_fkey" FOREIGN KEY ("reviewed_by") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "daily_reports" ADD CONSTRAINT "daily_reports_intern_id_fkey" FOREIGN KEY ("intern_id") REFERENCES "intern_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "weekly_evaluations" ADD CONSTRAINT "weekly_evaluations_intern_id_fkey" FOREIGN KEY ("intern_id") REFERENCES "intern_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "weekly_evaluations" ADD CONSTRAINT "weekly_evaluations_leader_id_fkey" FOREIGN KEY ("leader_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "notifications" ADD CONSTRAINT "notifications_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "notification_logs" ADD CONSTRAINT "notification_logs_notification_id_fkey" FOREIGN KEY ("notification_id") REFERENCES "notifications"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- CreateTable
CREATE TABLE "refresh_tokens" (
"id" UUID NOT NULL,
"token" TEXT NOT NULL,
"user_id" UUID NOT NULL,
"expires_at" TIMESTAMP(3) NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "refresh_tokens_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "refresh_tokens_token_key" ON "refresh_tokens"("token");
-- AddForeignKey
ALTER TABLE "refresh_tokens" ADD CONSTRAINT "refresh_tokens_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AlterTable
ALTER TABLE "refresh_tokens" ADD COLUMN "ip_address" TEXT,
ADD COLUMN "user_agent" TEXT;
-- CreateIndex
CREATE INDEX "applications_approved_by_idx" ON "applications"("approved_by");
-- CreateIndex
CREATE INDEX "daily_reports_intern_id_idx" ON "daily_reports"("intern_id");
-- CreateIndex
CREATE INDEX "intern_profiles_leader_id_idx" ON "intern_profiles"("leader_id");
-- CreateIndex
CREATE INDEX "notification_logs_notification_id_idx" ON "notification_logs"("notification_id");
-- CreateIndex
CREATE INDEX "notifications_user_id_idx" ON "notifications"("user_id");
-- CreateIndex
CREATE INDEX "refresh_tokens_user_id_idx" ON "refresh_tokens"("user_id");
-- CreateIndex
CREATE INDEX "task_assignments_intern_id_idx" ON "task_assignments"("intern_id");
-- CreateIndex
CREATE INDEX "task_assignments_assigned_by_idx" ON "task_assignments"("assigned_by");
-- CreateIndex
CREATE INDEX "task_submissions_assignment_id_idx" ON "task_submissions"("assignment_id");
-- CreateIndex
CREATE INDEX "task_submissions_reviewed_by_idx" ON "task_submissions"("reviewed_by");
-- CreateIndex
CREATE INDEX "tasks_created_by_idx" ON "tasks"("created_by");
-- CreateIndex
CREATE INDEX "users_role_id_idx" ON "users"("role_id");
-- CreateIndex
CREATE INDEX "weekly_evaluations_leader_id_idx" ON "weekly_evaluations"("leader_id");
-- AlterTable
ALTER TABLE "users" ADD COLUMN "full_name" TEXT;
-- AlterTable
ALTER TABLE "applications" ADD COLUMN "deleted_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "intern_profiles" ADD COLUMN "deleted_at" TIMESTAMP(3);
/*
Warnings:
- You are about to drop the `intern_profiles` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "daily_reports" DROP CONSTRAINT "daily_reports_intern_id_fkey";
-- DropForeignKey
ALTER TABLE "intern_profiles" DROP CONSTRAINT "intern_profiles_leader_id_fkey";
-- DropForeignKey
ALTER TABLE "intern_profiles" DROP CONSTRAINT "intern_profiles_user_id_fkey";
-- DropForeignKey
ALTER TABLE "task_assignments" DROP CONSTRAINT "task_assignments_intern_id_fkey";
-- DropForeignKey
ALTER TABLE "weekly_evaluations" DROP CONSTRAINT "weekly_evaluations_intern_id_fkey";
-- DropTable
DROP TABLE "intern_profiles";
-- CreateTable
CREATE TABLE "interns" (
"id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"leader_id" UUID,
"full_name" TEXT NOT NULL,
"phone" TEXT NOT NULL,
"department" TEXT NOT NULL,
"position" TEXT NOT NULL,
"start_date" TIMESTAMP(3) NOT NULL,
"duration" INTEGER NOT NULL,
"discord_username" TEXT,
"discord_role_granted" BOOLEAN NOT NULL DEFAULT false,
"status" "InternStatus" NOT NULL DEFAULT 'ACTIVE',
"deleted_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "interns_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "interns_user_id_key" ON "interns"("user_id");
-- CreateIndex
CREATE INDEX "interns_leader_id_idx" ON "interns"("leader_id");
-- AddForeignKey
ALTER TABLE "interns" ADD CONSTRAINT "interns_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "interns" ADD CONSTRAINT "interns_leader_id_fkey" FOREIGN KEY ("leader_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "task_assignments" ADD CONSTRAINT "task_assignments_intern_id_fkey" FOREIGN KEY ("intern_id") REFERENCES "interns"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "daily_reports" ADD CONSTRAINT "daily_reports_intern_id_fkey" FOREIGN KEY ("intern_id") REFERENCES "interns"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "weekly_evaluations" ADD CONSTRAINT "weekly_evaluations_intern_id_fkey" FOREIGN KEY ("intern_id") REFERENCES "interns"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AlterTable
ALTER TABLE "tasks" ADD COLUMN "deleted_at" TIMESTAMP(3);
-- CreateEnum
CREATE TYPE "TransactionType" AS ENUM ('INCOME', 'EXPENSE');
-- CreateTable
CREATE TABLE "users" (
"id" UUID NOT NULL,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL,
"full_name" TEXT,
"is_active" BOOLEAN NOT NULL DEFAULT true,
"role_id" UUID NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "roles" (
"id" UUID NOT NULL,
"name" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "roles_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "wallets" (
"id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"name" TEXT NOT NULL,
"balance" DOUBLE PRECISION NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "wallets_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "categories" (
"id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"name" TEXT NOT NULL,
"type" "TransactionType" NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "categories_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "transactions" (
"id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"wallet_id" UUID NOT NULL,
"category_id" UUID NOT NULL,
"amount" DOUBLE PRECISION NOT NULL,
"type" "TransactionType" NOT NULL,
"description" TEXT,
"date" TIMESTAMP(3) NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "transactions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "budgets" (
"id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"category_id" UUID NOT NULL,
"name" TEXT NOT NULL,
"amount" DOUBLE PRECISION NOT NULL,
"start_date" TIMESTAMP(3) NOT NULL,
"end_date" TIMESTAMP(3) NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "budgets_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "refresh_tokens" (
"id" UUID NOT NULL,
"token" TEXT NOT NULL,
"user_id" UUID NOT NULL,
"user_agent" TEXT,
"ip_address" TEXT,
"expires_at" TIMESTAMP(3) NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "refresh_tokens_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE UNIQUE INDEX "roles_name_key" ON "roles"("name");
-- CreateIndex
CREATE UNIQUE INDEX "wallets_user_id_name_key" ON "wallets"("user_id", "name");
-- CreateIndex
CREATE UNIQUE INDEX "categories_user_id_name_type_key" ON "categories"("user_id", "name", "type");
-- CreateIndex
CREATE UNIQUE INDEX "refresh_tokens_token_key" ON "refresh_tokens"("token");
-- CreateIndex
CREATE INDEX "refresh_tokens_user_id_idx" ON "refresh_tokens"("user_id");
-- AddForeignKey
ALTER TABLE "users" ADD CONSTRAINT "users_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "roles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "wallets" ADD CONSTRAINT "wallets_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "categories" ADD CONSTRAINT "categories_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "transactions" ADD CONSTRAINT "transactions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "transactions" ADD CONSTRAINT "transactions_wallet_id_fkey" FOREIGN KEY ("wallet_id") REFERENCES "wallets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "transactions" ADD CONSTRAINT "transactions_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "categories"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "budgets" ADD CONSTRAINT "budgets_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "budgets" ADD CONSTRAINT "budgets_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "categories"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "refresh_tokens" ADD CONSTRAINT "refresh_tokens_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
provider = "postgresql"
url = env("DATABASE_URL")
}
// 1. Role
model Role {
id String @id @default(uuid()) @db.Uuid
name String @unique // ADMIN, LEADER, INTERN
users User[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("roles")
enum TransactionType {
INCOME
EXPENSE
}
// 2. User
model User {
id String @id @default(uuid()) @db.Uuid
email String @unique
password String
fullName String? @map("full_name")
roleId String @map("role_id") @db.Uuid
isActive Boolean @default(true) @map("is_active")
roleId String @map("role_id") @db.Uuid
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
role Role @relation(fields: [roleId], references: [id])
// Relations
approvedApplications Application[] @relation("ApprovedApplications")
intern Intern? @relation("InternUser")
leadedInterns Intern[] @relation("LeaderInterns")
notificationSetting NotificationSetting?
createdTasks Task[] @relation("CreatedTasks")
assignedTasks TaskAssignment[] @relation("AssignedTasks")
reviewedSubmissions TaskSubmission[] @relation("ReviewedSubmissions")
leaderEvaluations WeeklyEvaluation[] @relation("LeaderEvaluations")
notifications Notification[]
refreshTokens RefreshToken[]
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
wallets Wallet[]
categories Category[]
transactions Transaction[]
budgets Budget[]
refreshTokens RefreshToken[]
@@index([roleId])
@@map("users")
}
// 3. Application
enum ApplicationStatus {
PENDING
APPROVED
REJECTED
}
model Application {
id String @id @default(uuid()) @db.Uuid
fullName String @map("full_name")
email String
phone String
department String
position String
startDate DateTime @map("start_date")
duration Int // Duration of internship (months or weeks)
status ApplicationStatus @default(PENDING)
approvedBy String? @map("approved_by") @db.Uuid
approvedAt DateTime? @map("approved_at")
deletedAt DateTime? @map("deleted_at") // Soft delete
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
approver User? @relation("ApprovedApplications", fields: [approvedBy], references: [id], onDelete: SetNull)
@@index([approvedBy])
@@map("applications")
}
// 4. Intern
enum InternStatus {
ACTIVE
COMPLETED
DROPPED
}
model Intern {
id String @id @default(uuid()) @db.Uuid
userId String @unique @map("user_id") @db.Uuid
leaderId String? @map("leader_id") @db.Uuid
fullName String @map("full_name")
phone String
department String
position String
startDate DateTime @map("start_date")
duration Int // Duration of internship
discordUsername String? @map("discord_username")
discordRoleGranted Boolean @default(false) @map("discord_role_granted")
status InternStatus @default(ACTIVE)
deletedAt DateTime? @map("deleted_at") // Soft delete
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation("InternUser", fields: [userId], references: [id], onDelete: Cascade)
leader User? @relation("LeaderInterns", fields: [leaderId], references: [id], onDelete: SetNull)
// Relations
assignments TaskAssignment[]
dailyReports DailyReport[]
weeklyEvaluations WeeklyEvaluation[]
@@index([leaderId])
@@map("interns")
}
// 5. NotificationSetting
model NotificationSetting {
id String @id @default(uuid()) @db.Uuid
userId String @unique @map("user_id") @db.Uuid
webEnabled Boolean @default(true) @map("web_enabled")
emailEnabled Boolean @default(true) @map("email_enabled")
discordEnabled Boolean @default(false) @map("discord_enabled")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("notification_settings")
}
// 6. Task
enum TaskPriority {
LOW
MEDIUM
HIGH
}
model Task {
id String @id @default(uuid()) @db.Uuid
title String
description String?
deadline DateTime
priority TaskPriority @default(MEDIUM)
createdBy String @map("created_by") @db.Uuid
deletedAt DateTime? @map("deleted_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
creator User @relation("CreatedTasks", fields: [createdBy], references: [id], onDelete: Cascade)
assignment TaskAssignment?
@@index([createdBy])
@@map("tasks")
}
// 7. TaskAssignment
enum AssignmentStatus {
TODO
IN_PROGRESS
REVIEW
DONE
}
model TaskAssignment {
id String @id @default(uuid()) @db.Uuid
taskId String @unique @map("task_id") @db.Uuid
internId String @map("intern_id") @db.Uuid
assignedBy String @map("assigned_by") @db.Uuid
status AssignmentStatus @default(TODO)
assignedAt DateTime @default(now()) @map("assigned_at")
updatedAt DateTime @updatedAt @map("updated_at")
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
intern Intern @relation(fields: [internId], references: [id], onDelete: Cascade)
assigner User @relation("AssignedTasks", fields: [assignedBy], references: [id], onDelete: Cascade)
submissions TaskSubmission[]
@@index([internId])
@@index([assignedBy])
@@map("task_assignments")
}
// 8. TaskSubmission
enum ReviewStatus {
PENDING
APPROVED
REJECTED
}
model TaskSubmission {
id String @id @default(uuid()) @db.Uuid
assignmentId String @map("assignment_id") @db.Uuid
attempt Int
prLink String? @map("pr_link")
videoDemo String? @map("video_demo")
note String?
reviewStatus ReviewStatus @default(PENDING) @map("review_status")
reviewComment String? @map("review_comment")
reviewedBy String? @map("reviewed_by") @db.Uuid
reviewedAt DateTime? @map("reviewed_at")
submittedAt DateTime @default(now()) @map("submitted_at")
updatedAt DateTime @updatedAt @map("updated_at")
assignment TaskAssignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
reviewer User? @relation("ReviewedSubmissions", fields: [reviewedBy], references: [id], onDelete: SetNull)
@@index([assignmentId])
@@index([reviewedBy])
@@map("task_submissions")
}
// 9. DailyReport
model DailyReport {
model Role {
id String @id @default(uuid()) @db.Uuid
internId String @map("intern_id") @db.Uuid
content String
prLink String? @map("pr_link")
videoDemo String? @map("video_demo")
name String @unique
users User[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
intern Intern @relation(fields: [internId], references: [id], onDelete: Cascade)
@@index([internId])
@@map("daily_reports")
}
// 10. WeeklyEvaluation
model WeeklyEvaluation {
id String @id @default(uuid()) @db.Uuid
internId String @map("intern_id") @db.Uuid
leaderId String @map("leader_id") @db.Uuid
week Int
communication Float
attitude Float
learning Float
coding Float
totalScore Float @map("total_score")
comment String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
intern Intern @relation(fields: [internId], references: [id], onDelete: Cascade)
leader User @relation("LeaderEvaluations", fields: [leaderId], references: [id], onDelete: Cascade)
@@unique([internId, week])
@@index([leaderId])
@@map("weekly_evaluations")
@@map("roles")
}
// 11. Notification
model Notification {
model Wallet {
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid
title String
content String
type String
isRead Boolean @default(false) @map("is_read")
name String
balance Float
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
logs NotificationLog[]
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
transactions Transaction[]
@@index([userId])
@@map("notifications")
@@unique([userId, name])
@@map("wallets")
}
// 12. NotificationLog
enum NotificationChannel {
WEB
EMAIL
DISCORD
model Category {
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid
name String
type TransactionType
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
transactions Transaction[]
budgets Budget[]
@@unique([userId, name, type])
@@map("categories")
}
enum NotificationLogStatus {
SUCCESS
FAILED
model Transaction {
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid
walletId String @map("wallet_id") @db.Uuid
categoryId String @map("category_id") @db.Uuid
amount Float
type TransactionType
description String?
date DateTime
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
category Category @relation(fields: [categoryId], references: [id], onDelete: Restrict)
@@map("transactions")
}
model NotificationLog {
id String @id @default(uuid()) @db.Uuid
notificationId String @map("notification_id") @db.Uuid
channel NotificationChannel
status NotificationLogStatus
sentAt DateTime @default(now()) @map("sent_at")
model Budget {
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid
categoryId String @map("category_id") @db.Uuid
name String
amount Float
startDate DateTime @map("start_date")
endDate DateTime @map("end_date")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
notification Notification @relation(fields: [notificationId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
@@index([notificationId])
@@map("notification_logs")
@@map("budgets")
}
// 13. RefreshToken
model RefreshToken {
id String @id @default(uuid()) @db.Uuid
token String @unique
......
......@@ -17,13 +17,13 @@ async function main() {
console.log(`Role ${roleName} upserted with ID ${role.id}`);
}
const adminEmail = 'admin@nexcampus.local';
const adminEmail = 'admin@finwise.local';
const adminPassword = await bcrypt.hash('Admin@123456', 10);
const leaderEmail = 'leader@nexcampus.local';
const leaderEmail = 'leader@finwise.local';
const leaderPassword = await bcrypt.hash('Leader@123456', 10);
const internEmail = 'intern@nexcampus.local';
const internEmail = 'intern@finwise.local';
const internPassword = await bcrypt.hash('Intern@123456', 10);
await prisma.user.upsert({
......
......@@ -6,16 +6,13 @@ import swaggerUi from 'swagger-ui-express';
import { errorMiddleware, notFoundMiddleware } from './middlewares/error.middleware';
import routes from './routes';
import { swaggerSpec, swaggerOptions } from './config/swagger.config';
import { rateLimitMiddleware } from './middlewares/rate-limit.middleware';
const app = express();
app.set('trust proxy', true);
app.use(
helmet({
contentSecurityPolicy: false, // Tắt CSP để Swagger UI tải được stylesheet
}),
);
app.use(helmet({ contentSecurityPolicy: false, }),);
app.use(cors());
app.use(morgan('dev'));
app.use(express.json());
......@@ -23,7 +20,7 @@ app.use(express.urlencoded({ extended: true }));
app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, swaggerOptions));
app.use('/api/v1', routes);
app.use('/api/v1', rateLimitMiddleware, routes);
app.use(notFoundMiddleware);
app.use(errorMiddleware);
......
......@@ -4,19 +4,18 @@ export const swaggerOptions: SwaggerUiOptions = {
customCss: `
.swagger-ui .topbar { background-color: #1a1a2e; }
.swagger-ui .topbar-wrapper .link img { display: none; }
.swagger-ui .topbar-wrapper .link::after { content: 'NexCampus API'; color: white; font-size: 1.2rem; font-weight: bold; }
.swagger-ui .topbar-wrapper .link::after { content: 'FinWise API'; color: white; font-size: 1.2rem; font-weight: bold; }
`,
customSiteTitle: 'NexCampus API Docs',
customSiteTitle: 'FinWise API Docs',
};
export const swaggerSpec = {
openapi: '3.0.0',
info: {
title: 'NexCampus API',
title: 'FinWise API',
version: '1.0.0',
description:
'Backend API cho hệ thống quản lý thực tập sinh NexCampus. Bao gồm các module: Auth, Users, Applications.',
contact: { name: 'NexCampus Team' },
description: 'Backend API cho FinWise - Sổ tay Chi tiêu & Báo cáo Tài chính (Zalo Mini App). Hiện tại API cung cấp xác thực và quản lý người dùng.',
contact: { name: 'FinWise Team' },
},
servers: [
{ url: '/api/v1', description: 'Development server' },
......@@ -27,11 +26,10 @@ export const swaggerSpec = {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'Nhập Access Token nhận được từ POST /auth/login',
description: 'Sử dụng header Authorization: Bearer <accessToken>',
},
},
schemas: {
// ─── Common ───────────────────────────────────────────────────────────
SuccessResponse: {
type: 'object',
properties: {
......@@ -41,9 +39,9 @@ export const swaggerSpec = {
PaginationMeta: {
type: 'object',
properties: {
total: { type: 'integer', example: 42 },
page: { type: 'integer', example: 1 },
limit: { type: 'integer', example: 20 },
total: { type: 'integer', example: 42 },
page: { type: 'integer', example: 1 },
limit: { type: 'integer', example: 20 },
totalPages: { type: 'integer', example: 3 },
},
},
......@@ -52,27 +50,25 @@ export const swaggerSpec = {
properties: {
success: { type: 'boolean', example: false },
message: { type: 'string', example: 'Error message' },
code: { type: 'string', example: 'NOT_FOUND' },
code: { type: 'string', example: 'NOT_FOUND' },
},
},
// ─── Role ─────────────────────────────────────────────────────────────
Role: {
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
id: { type: 'string', format: 'uuid' },
name: { type: 'string', enum: ['ADMIN', 'LEADER', 'INTERN'] },
},
},
// ─── User ─────────────────────────────────────────────────────────────
User: {
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
email: { type: 'string', format: 'email' },
fullName: { type: 'string', nullable: true },
roleId: { type: 'string', format: 'uuid' },
role: { $ref: '#/components/schemas/Role' },
isActive: { type: 'boolean' },
id: { type: 'string', format: 'uuid' },
email: { type: 'string', format: 'email' },
fullName: { type: 'string', nullable: true },
roleId: { type: 'string', format: 'uuid' },
role: { $ref: '#/components/schemas/Role' },
isActive: { type: 'boolean' },
createdAt: { type: 'string', format: 'date-time' },
updatedAt: { type: 'string', format: 'date-time' },
},
......@@ -81,258 +77,117 @@ export const swaggerSpec = {
type: 'object',
required: ['email', 'password', 'roleId'],
properties: {
email: { type: 'string', format: 'email', example: 'intern@nexcampus.local' },
password: { type: 'string', minLength: 8, example: 'Intern@123456' },
roleId: { type: 'string', format: 'uuid', example: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' },
email: { type: 'string', format: 'email', example: 'admin@finwise.local' },
password: { type: 'string', minLength: 8, example: 'Admin@123456' },
roleId: { type: 'string', format: 'uuid', example: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' },
},
},
UpdateUserBody: {
type: 'object',
properties: {
isActive: { type: 'boolean' },
roleId: { type: 'string', format: 'uuid' },
roleId: { type: 'string', format: 'uuid' },
},
},
// ─── Auth ─────────────────────────────────────────────────────────────
LoginBody: {
type: 'object',
required: ['email', 'password'],
properties: {
email: { type: 'string', format: 'email', example: 'admin@nexcampus.local' },
email: { type: 'string', format: 'email', example: 'admin@finwise.local' },
password: { type: 'string', example: 'Admin@123456' },
},
},
TokenPair: {
LoginResponse: {
type: 'object',
properties: {
accessToken: { type: 'string' },
accessToken: { type: 'string' },
refreshToken: { type: 'string' },
user: { $ref: '#/components/schemas/User' },
},
},
RefreshBody: {
TokenPair: {
type: 'object',
required: ['refreshToken'],
properties: {
accessToken: { type: 'string' },
refreshToken: { type: 'string' },
},
},
LogoutBody: {
RefreshBody: {
type: 'object',
required: ['refreshToken'],
properties: {
refreshToken: { type: 'string' },
},
},
// ─── Application ──────────────────────────────────────────────────────
Application: {
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
fullName: { type: 'string', example: 'Nguyễn Văn A' },
email: { type: 'string', format: 'email' },
phone: { type: 'string', example: '0912345678' },
department: { type: 'string', example: 'Engineering' },
position: { type: 'string', example: 'Frontend Developer' },
startDate: { type: 'string', format: 'date-time' },
duration: { type: 'integer', example: 3, description: 'Số tháng thực tập' },
status: { type: 'string', enum: ['PENDING', 'APPROVED', 'REJECTED'] },
approvedBy: { type: 'string', format: 'uuid', nullable: true },
approvedAt: { type: 'string', format: 'date-time', nullable: true },
deletedAt: { type: 'string', format: 'date-time', nullable: true },
createdAt: { type: 'string', format: 'date-time' },
updatedAt: { type: 'string', format: 'date-time' },
approver: {
nullable: true,
allOf: [{
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
email: { type: 'string', format: 'email' },
fullName: { type: 'string', nullable: true },
},
}],
},
},
},
CreateApplicationBody: {
type: 'object',
required: ['fullName', 'email', 'phone', 'department', 'position', 'startDate', 'duration'],
properties: {
fullName: { type: 'string', example: 'Nguyễn Văn A' },
email: { type: 'string', format: 'email', example: 'vana@gmail.com' },
phone: { type: 'string', example: '0912345678' },
department: { type: 'string', example: 'Engineering' },
position: { type: 'string', example: 'Frontend Developer' },
startDate: { type: 'string', format: 'date', example: '2025-08-01' },
duration: { type: 'integer', example: 3, description: 'Số tháng thực tập' },
},
},
ReviewApplicationBody: {
type: 'object',
required: ['status'],
properties: {
status: { type: 'string', enum: ['APPROVED', 'REJECTED'] },
},
},
// ─── Intern ────────────────────────────────────────────────────
Intern: {
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
userId: { type: 'string', format: 'uuid' },
leaderId: { type: 'string', format: 'uuid', nullable: true },
fullName: { type: 'string', example: 'Nguyễn Chí Thịnh' },
phone: { type: 'string', example: '0912345678' },
department: { type: 'string', example: 'Engineering' },
position: { type: 'string', example: 'Frontend Developer' },
startDate: { type: 'string', format: 'date-time' },
duration: { type: 'integer', example: 3, description: 'Số tháng thực tập' },
discordUsername: { type: 'string', nullable: true },
discordRoleGranted: { type: 'boolean', example: false },
status: { type: 'string', enum: ['ACTIVE', 'COMPLETED', 'DROPPED'] },
deletedAt: { type: 'string', format: 'date-time', nullable: true },
createdAt: { type: 'string', format: 'date-time' },
updatedAt: { type: 'string', format: 'date-time' },
user: {
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
email: { type: 'string', format: 'email' },
fullName: { type: 'string', nullable: true },
isActive: { type: 'boolean' },
},
},
leader: {
nullable: true,
allOf: [{
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
email: { type: 'string', format: 'email' },
fullName: { type: 'string', nullable: true },
isActive: { type: 'boolean' },
},
}],
},
},
},
CreateInternBody: {
type: 'object',
required: ['userId', 'fullName', 'phone', 'department', 'position', 'startDate', 'duration'],
properties: {
userId: { type: 'string', format: 'uuid' },
leaderId: { type: 'string', format: 'uuid' },
fullName: { type: 'string', example: 'Nguyễn Chí Thịnh' },
phone: { type: 'string', example: '0912345678' },
department: { type: 'string', example: 'Engineering' },
position: { type: 'string', example: 'Frontend Developer' },
startDate: { type: 'string', format: 'date', example: '2025-08-01' },
duration: { type: 'integer', example: 3 },
discordUsername: { type: 'string', example: 'chithinhdev' },
},
},
UpdateInternBody: {
type: 'object',
properties: {
leaderId: { type: 'string', format: 'uuid', nullable: true },
fullName: { type: 'string' },
phone: { type: 'string' },
department: { type: 'string' },
position: { type: 'string' },
startDate: { type: 'string', format: 'date' },
duration: { type: 'integer' },
discordUsername: { type: 'string', nullable: true },
discordRoleGranted: { type: 'boolean' },
status: { type: 'string', enum: ['ACTIVE', 'COMPLETED', 'DROPPED'] },
},
},
Task: {
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
title: { type: 'string', example: 'Lập trình tính năng đăng nhập' },
description: { type: 'string', nullable: true, example: 'Sử dụng JWT và bcrypt để bảo mật mật khẩu' },
deadline: { type: 'string', format: 'date-time' },
priority: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH'] },
createdBy: { type: 'string', format: 'uuid' },
deletedAt: { type: 'string', format: 'date-time', nullable: true },
createdAt: { type: 'string', format: 'date-time' },
updatedAt: { type: 'string', format: 'date-time' },
creator: {
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
email: { type: 'string', format: 'email' },
fullName: { type: 'string', nullable: true },
},
},
},
},
CreateTaskBody: {
type: 'object',
required: ['title', 'deadline'],
properties: {
title: { type: 'string', example: 'Lập trình tính năng đăng nhập' },
description: { type: 'string', example: 'Sử dụng JWT và bcrypt' },
deadline: { type: 'string', format: 'date-time', example: '2025-08-10T17:00:00.000Z' },
priority: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH'], default: 'MEDIUM' },
},
},
UpdateTaskBody: {
LogoutBody: {
type: 'object',
required: ['refreshToken'],
properties: {
title: { type: 'string' },
description: { type: 'string', nullable: true },
deadline: { type: 'string', format: 'date-time' },
priority: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH'] },
refreshToken: { type: 'string' },
},
},
},
parameters: {
PageParam: { in: 'query', name: 'page', schema: { type: 'integer', default: 1 } },
PageParam: { in: 'query', name: 'page', schema: { type: 'integer', default: 1 } },
LimitParam: { in: 'query', name: 'limit', schema: { type: 'integer', default: 20, maximum: 100 } },
OrderParam: { in: 'query', name: 'order', schema: { type: 'string', enum: ['asc', 'desc'], default: 'desc' } },
},
// ─── Reusable responses ───────────────────────────────────────────────────
responses: {
Unauthorized: { description: 'Chưa đăng nhập', content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } } },
Forbidden: { description: 'Không có quyền', content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } } },
NotFound: { description: 'Không tìm thấy', content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } } },
Validation: { description: 'Dữ liệu không hợp lệ', content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } } },
Unauthorized: { description: 'Chưa đăng nhập', content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } } },
Forbidden: { description: 'Không có quyền', content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } } },
NotFound: { description: 'Không tìm thấy', content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } } },
Validation: { description: 'Dữ liệu không hợp lệ', content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } } },
Conflict: { description: 'Xung đột dữ liệu', content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } } },
},
},
tags: [
{ name: 'Auth', description: 'Đăng nhập, làm mới token, đăng xuất' },
{ name: 'Users', description: 'Quản lý tài khoản người dùng (Admin only)' },
{ name: 'Applications', description: 'Đơn xin thực tập (Onboarding)' },
{ name: 'Interns', description: 'Hồ sơ thực tập sinh (Interns)' },
{ name: 'Tasks', description: 'Quản lý công việc (Tasks)' },
{ name: 'System', description: 'Health check' },
{ name: 'System', description: 'Health check' },
{ name: 'Auth', description: 'Authentication endpoints' },
{ name: 'Users', description: 'User account management (Admin only)' },
],
paths: {
// ─── System ─────────────────────────────────────────────────────────────
'/health': {
get: {
tags: ['System'],
summary: 'Health check',
responses: {
200: { description: 'Server đang chạy', content: { 'application/json': { schema: { type: 'object', properties: { status: { type: 'string', example: 'ok' }, timestamp: { type: 'string' } } } } } },
200: {
description: 'Server đang chạy',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
status: { type: 'string', example: 'ok' },
timestamp: { type: 'string', format: 'date-time' },
},
},
},
},
},
},
},
},
// ─── Auth ────────────────────────────────────────────────────────────────
'/auth/login': {
post: {
tags: ['Auth'],
summary: 'Đăng nhập',
summary: 'Đăng nhập và nhận token',
requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/LoginBody' } } } },
responses: {
200: {
description: 'Đăng nhập thành công',
content: { 'application/json': { schema: { allOf: [{ $ref: '#/components/schemas/SuccessResponse' }, { type: 'object', properties: { data: { $ref: '#/components/schemas/TokenPair' } } }] } } },
content: {
'application/json': {
schema: {
allOf: [
{ $ref: '#/components/schemas/SuccessResponse' },
{ type: 'object', properties: { data: { $ref: '#/components/schemas/LoginResponse' } } },
],
},
},
},
},
401: { description: 'Sai thông tin đăng nhập', content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } } },
401: { $ref: '#/components/responses/Unauthorized' },
422: { $ref: '#/components/responses/Validation' },
},
},
......@@ -345,7 +200,16 @@ export const swaggerSpec = {
responses: {
200: {
description: 'Thông tin tài khoản đang đăng nhập',
content: { 'application/json': { schema: { allOf: [{ $ref: '#/components/schemas/SuccessResponse' }, { type: 'object', properties: { data: { $ref: '#/components/schemas/User' } } }] } } },
content: {
'application/json': {
schema: {
allOf: [
{ $ref: '#/components/schemas/SuccessResponse' },
{ type: 'object', properties: { data: { $ref: '#/components/schemas/User' } } },
],
},
},
},
},
401: { $ref: '#/components/responses/Unauthorized' },
},
......@@ -354,12 +218,21 @@ export const swaggerSpec = {
'/auth/refresh': {
post: {
tags: ['Auth'],
summary: 'Làm mới Access Token',
summary: 'Làm mới token',
requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/RefreshBody' } } } },
responses: {
200: {
description: 'Cặp token mới',
content: { 'application/json': { schema: { allOf: [{ $ref: '#/components/schemas/SuccessResponse' }, { type: 'object', properties: { data: { $ref: '#/components/schemas/TokenPair' } } }] } } },
content: {
'application/json': {
schema: {
allOf: [
{ $ref: '#/components/schemas/SuccessResponse' },
{ type: 'object', properties: { data: { $ref: '#/components/schemas/TokenPair' } } },
],
},
},
},
},
401: { $ref: '#/components/responses/Unauthorized' },
422: { $ref: '#/components/responses/Validation' },
......@@ -369,28 +242,38 @@ export const swaggerSpec = {
'/auth/logout': {
post: {
tags: ['Auth'],
summary: 'Đăng xuất (thu hồi Refresh Token)',
summary: 'Đăng xuất',
requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/LogoutBody' } } } },
responses: {
200: { description: 'Đăng xuất thành công', content: { 'application/json': { schema: { allOf: [{ $ref: '#/components/schemas/SuccessResponse' }, { type: 'object', properties: { message: { type: 'string' } } }] } } } },
200: {
description: 'Đăng xuất thành công',
content: {
'application/json': {
schema: {
allOf: [
{ $ref: '#/components/schemas/SuccessResponse' },
{ type: 'object', properties: { message: { type: 'string', example: 'Logged out successfully' } } },
],
},
},
},
},
400: { description: 'Token không tồn tại', content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } } },
422: { $ref: '#/components/responses/Validation' },
},
},
},
// ─── Users ───────────────────────────────────────────────────────────────
'/users': {
get: {
tags: ['Users'],
summary: 'Danh sách tài khoản (có filter, sort, phân trang)',
summary: 'Danh sách tài khoản',
security: [{ BearerAuth: [] }],
parameters: [
{ in: 'query', name: 'email', schema: { type: 'string' }, description: 'Tìm theo email (contains)' },
{ in: 'query', name: 'fullName', schema: { type: 'string' }, description: 'Tìm theo tên (contains)' },
{ in: 'query', name: 'email', schema: { type: 'string' }, description: 'Tìm theo email' },
{ in: 'query', name: 'fullName', schema: { type: 'string' }, description: 'Tìm theo tên' },
{ in: 'query', name: 'roleName', schema: { type: 'string', enum: ['ADMIN', 'LEADER', 'INTERN'] }, description: 'Lọc theo role' },
{ in: 'query', name: 'isActive', schema: { type: 'string', enum: ['true', 'false'] }, description: 'Lọc theo trạng thái' },
{ in: 'query', name: 'sortBy', schema: { type: 'string', enum: ['createdAt', 'email', 'fullName'], default: 'createdAt' } },
{ in: 'query', name: 'sortBy', schema: { type: 'string', enum: ['createdAt', 'email', 'fullName'], default: 'createdAt' } },
{ $ref: '#/components/parameters/OrderParam' },
{ $ref: '#/components/parameters/PageParam' },
{ $ref: '#/components/parameters/LimitParam' },
......@@ -398,7 +281,22 @@ export const swaggerSpec = {
responses: {
200: {
description: 'Danh sách user có phân trang',
content: { 'application/json': { schema: { allOf: [{ $ref: '#/components/schemas/SuccessResponse' }, { type: 'object', properties: { data: { type: 'array', items: { $ref: '#/components/schemas/User' } }, meta: { $ref: '#/components/schemas/PaginationMeta' } } }] } } },
content: {
'application/json': {
schema: {
allOf: [
{ $ref: '#/components/schemas/SuccessResponse' },
{
type: 'object',
properties: {
data: { type: 'array', items: { $ref: '#/components/schemas/User' } },
meta: { $ref: '#/components/schemas/PaginationMeta' },
},
},
],
},
},
},
},
401: { $ref: '#/components/responses/Unauthorized' },
403: { $ref: '#/components/responses/Forbidden' },
......@@ -406,17 +304,26 @@ export const swaggerSpec = {
},
post: {
tags: ['Users'],
summary: 'Tạo tài khoản mới (Admin only)',
summary: 'Tạo tài khoản mới',
security: [{ BearerAuth: [] }],
requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/CreateUserBody' } } } },
responses: {
201: {
description: 'Tài khoản đã tạo',
content: { 'application/json': { schema: { allOf: [{ $ref: '#/components/schemas/SuccessResponse' }, { type: 'object', properties: { data: { $ref: '#/components/schemas/User' } } }] } } },
content: {
'application/json': {
schema: {
allOf: [
{ $ref: '#/components/schemas/SuccessResponse' },
{ type: 'object', properties: { data: { $ref: '#/components/schemas/User' } } },
],
},
},
},
},
401: { $ref: '#/components/responses/Unauthorized' },
403: { $ref: '#/components/responses/Forbidden' },
409: { description: 'Email đã tồn tại', content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } } },
409: { $ref: '#/components/responses/Conflict' },
422: { $ref: '#/components/responses/Validation' },
},
},
......@@ -427,281 +334,51 @@ export const swaggerSpec = {
summary: 'Chi tiết tài khoản theo ID',
security: [{ BearerAuth: [] }],
parameters: [{ in: 'path', name: 'id', required: true, schema: { type: 'string', format: 'uuid' } }],
responses: {
200: { description: 'Thông tin user', content: { 'application/json': { schema: { allOf: [{ $ref: '#/components/schemas/SuccessResponse' }, { type: 'object', properties: { data: { $ref: '#/components/schemas/User' } } }] } } } },
401: { $ref: '#/components/responses/Unauthorized' },
403: { $ref: '#/components/responses/Forbidden' },
404: { $ref: '#/components/responses/NotFound' },
},
},
put: {
tags: ['Users'],
summary: 'Cập nhật tài khoản (kích hoạt/đổi role)',
security: [{ BearerAuth: [] }],
parameters: [{ in: 'path', name: 'id', required: true, schema: { type: 'string', format: 'uuid' } }],
requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/UpdateUserBody' } } } },
responses: {
200: { description: 'Cập nhật thành công', content: { 'application/json': { schema: { allOf: [{ $ref: '#/components/schemas/SuccessResponse' }, { type: 'object', properties: { data: { $ref: '#/components/schemas/User' } } }] } } } },
401: { $ref: '#/components/responses/Unauthorized' },
403: { $ref: '#/components/responses/Forbidden' },
404: { $ref: '#/components/responses/NotFound' },
422: { $ref: '#/components/responses/Validation' },
},
},
},
// ─── Applications ────────────────────────────────────────────────────────
'/applications': {
post: {
tags: ['Applications'],
summary: 'Nộp đơn xin thực tập (Public — không cần đăng nhập)',
requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/CreateApplicationBody' } } } },
responses: {
201: {
description: 'Đơn đã được tiếp nhận (status = PENDING)',
content: { 'application/json': { schema: { allOf: [{ $ref: '#/components/schemas/SuccessResponse' }, { type: 'object', properties: { data: { $ref: '#/components/schemas/Application' } } }] } } },
},
422: { $ref: '#/components/responses/Validation' },
},
},
get: {
tags: ['Applications'],
summary: 'Danh sách đơn (Admin / Leader)',
security: [{ BearerAuth: [] }],
parameters: [
{ in: 'query', name: 'status', schema: { type: 'string', enum: ['PENDING', 'APPROVED', 'REJECTED'] } },
{ in: 'query', name: 'department', schema: { type: 'string' }, description: 'Lọc bộ phận (contains)' },
{ in: 'query', name: 'position', schema: { type: 'string' }, description: 'Lọc vị trí (contains)' },
{ in: 'query', name: 'email', schema: { type: 'string' }, description: 'Tìm theo email (contains)' },
{ in: 'query', name: 'startDateFrom', schema: { type: 'string', format: 'date' }, description: 'Ngày bắt đầu từ' },
{ in: 'query', name: 'startDateTo', schema: { type: 'string', format: 'date' }, description: 'Ngày bắt đầu đến' },
{ in: 'query', name: 'sortBy', schema: { type: 'string', enum: ['createdAt', 'startDate', 'fullName', 'status'], default: 'createdAt' } },
{ $ref: '#/components/parameters/OrderParam' },
{ $ref: '#/components/parameters/PageParam' },
{ $ref: '#/components/parameters/LimitParam' },
],
responses: {
200: {
description: 'Danh sách đơn có phân trang',
content: { 'application/json': { schema: { allOf: [{ $ref: '#/components/schemas/SuccessResponse' }, { type: 'object', properties: { data: { type: 'array', items: { $ref: '#/components/schemas/Application' } }, meta: { $ref: '#/components/schemas/PaginationMeta' } } }] } } },
},
401: { $ref: '#/components/responses/Unauthorized' },
403: { $ref: '#/components/responses/Forbidden' },
},
},
},
'/applications/{id}': {
get: {
tags: ['Applications'],
summary: 'Chi tiết một đơn',
security: [{ BearerAuth: [] }],
parameters: [{ in: 'path', name: 'id', required: true, schema: { type: 'string', format: 'uuid' } }],
responses: {
200: { description: 'Thông tin đơn', content: { 'application/json': { schema: { allOf: [{ $ref: '#/components/schemas/SuccessResponse' }, { type: 'object', properties: { data: { $ref: '#/components/schemas/Application' } } }] } } } },
401: { $ref: '#/components/responses/Unauthorized' },
403: { $ref: '#/components/responses/Forbidden' },
404: { $ref: '#/components/responses/NotFound' },
},
},
delete: {
tags: ['Applications'],
summary: 'Xoá mềm đơn (Admin only — chỉ PENDING hoặc REJECTED)',
security: [{ BearerAuth: [] }],
parameters: [{ in: 'path', name: 'id', required: true, schema: { type: 'string', format: 'uuid' } }],
responses: {
200: { description: 'Đã xoá mềm', content: { 'application/json': { schema: { allOf: [{ $ref: '#/components/schemas/SuccessResponse' }, { type: 'object', properties: { message: { type: 'string' } } }] } } } },
401: { $ref: '#/components/responses/Unauthorized' },
403: { $ref: '#/components/responses/Forbidden' },
404: { $ref: '#/components/responses/NotFound' },
409: { description: 'Không thể xoá đơn đã APPROVED', content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } } },
},
},
},
'/applications/{id}/review': {
patch: {
tags: ['Applications'],
summary: 'Phê duyệt hoặc từ chối đơn (Admin / Leader)',
security: [{ BearerAuth: [] }],
parameters: [{ in: 'path', name: 'id', required: true, schema: { type: 'string', format: 'uuid' } }],
requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/ReviewApplicationBody' } } } },
responses: {
200: { description: 'Đơn đã được xử lý', content: { 'application/json': { schema: { allOf: [{ $ref: '#/components/schemas/SuccessResponse' }, { type: 'object', properties: { data: { $ref: '#/components/schemas/Application' } } }] } } } },
401: { $ref: '#/components/responses/Unauthorized' },
403: { $ref: '#/components/responses/Forbidden' },
404: { $ref: '#/components/responses/NotFound' },
409: { description: 'Đơn không còn ở trạng thái PENDING', content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } } },
422: { $ref: '#/components/responses/Validation' },
},
},
},
// ─── Interns ─────────────────────────────────────────────────────────────
'/interns': {
get: {
tags: ['Interns'],
summary: 'Danh sách thực tập sinh (Admin / Leader)',
security: [{ BearerAuth: [] }],
parameters: [
{ in: 'query', name: 'fullName', schema: { type: 'string' }, description: 'Tìm theo tên (contains)' },
{ in: 'query', name: 'department', schema: { type: 'string' }, description: 'Lọc bộ phận (contains)' },
{ in: 'query', name: 'position', schema: { type: 'string' }, description: 'Lọc vị trí (contains)' },
{ in: 'query', name: 'status', schema: { type: 'string', enum: ['ACTIVE', 'COMPLETED', 'DROPPED'] } },
{ in: 'query', name: 'leaderId', schema: { type: 'string', format: 'uuid' }, description: 'Lọc theo Leader' },
{ in: 'query', name: 'discordRoleGranted', schema: { type: 'string', enum: ['true', 'false'] } },
{ in: 'query', name: 'startDateFrom', schema: { type: 'string', format: 'date' } },
{ in: 'query', name: 'startDateTo', schema: { type: 'string', format: 'date' } },
{ in: 'query', name: 'sortBy', schema: { type: 'string', enum: ['createdAt', 'fullName', 'startDate', 'status'], default: 'createdAt' } },
{ $ref: '#/components/parameters/OrderParam' },
{ $ref: '#/components/parameters/PageParam' },
{ $ref: '#/components/parameters/LimitParam' },
],
responses: {
200: {
description: 'Danh sách hồ sơ có phân trang',
content: { 'application/json': { schema: { allOf: [{ $ref: '#/components/schemas/SuccessResponse' }, { type: 'object', properties: { data: { type: 'array', items: { $ref: '#/components/schemas/Intern' } }, meta: { $ref: '#/components/schemas/PaginationMeta' } } }] } } },
},
401: { $ref: '#/components/responses/Unauthorized' },
403: { $ref: '#/components/responses/Forbidden' },
},
},
post: {
tags: ['Interns'],
summary: 'Tạo hồ sơ thực tập sinh (Admin / Leader)',
description: 'Tạo sau khi đơn Application được phê duyệt. Mỗi userId chỉ có 1 hồ sơ.',
security: [{ BearerAuth: [] }],
requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/CreateInternBody' } } } },
responses: {
201: {
description: 'Hồ sơ đã tạo',
content: { 'application/json': { schema: { allOf: [{ $ref: '#/components/schemas/SuccessResponse' }, { type: 'object', properties: { data: { $ref: '#/components/schemas/Intern' } } }] } } },
description: 'Thông tin user',
content: {
'application/json': {
schema: {
allOf: [
{ $ref: '#/components/schemas/SuccessResponse' },
{ type: 'object', properties: { data: { $ref: '#/components/schemas/User' } } },
],
},
},
},
},
401: { $ref: '#/components/responses/Unauthorized' },
403: { $ref: '#/components/responses/Forbidden' },
409: { description: 'User đã có hồ sơ thực tập', content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } } },
422: { $ref: '#/components/responses/Validation' },
},
},
},
'/interns/{id}': {
get: {
tags: ['Interns'],
summary: 'Chi tiết hồ sơ theo ID',
security: [{ BearerAuth: [] }],
parameters: [{ in: 'path', name: 'id', required: true, schema: { type: 'string', format: 'uuid' } }],
responses: {
200: { description: 'Thông tin hồ sơ', content: { 'application/json': { schema: { allOf: [{ $ref: '#/components/schemas/SuccessResponse' }, { type: 'object', properties: { data: { $ref: '#/components/schemas/Intern' } } }] } } } },
401: { $ref: '#/components/responses/Unauthorized' },
403: { $ref: '#/components/responses/Forbidden' },
404: { $ref: '#/components/responses/NotFound' },
},
},
put: {
tags: ['Interns'],
summary: 'Cập nhật hồ sơ (Admin / Leader)',
security: [{ BearerAuth: [] }],
parameters: [{ in: 'path', name: 'id', required: true, schema: { type: 'string', format: 'uuid' } }],
requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/UpdateInternBody' } } } },
responses: {
200: { description: 'Hồ sơ đã cập nhật', content: { 'application/json': { schema: { allOf: [{ $ref: '#/components/schemas/SuccessResponse' }, { type: 'object', properties: { data: { $ref: '#/components/schemas/Intern' } } }] } } } },
401: { $ref: '#/components/responses/Unauthorized' },
403: { $ref: '#/components/responses/Forbidden' },
404: { $ref: '#/components/responses/NotFound' },
422: { $ref: '#/components/responses/Validation' },
},
},
delete: {
tags: ['Interns'],
summary: 'Xoá mềm hồ sơ (Admin only)',
tags: ['Users'],
summary: 'Cập nhật tài khoản',
security: [{ BearerAuth: [] }],
parameters: [{ in: 'path', name: 'id', required: true, schema: { type: 'string', format: 'uuid' } }],
responses: {
200: { description: 'Đã xoá mềm', content: { 'application/json': { schema: { allOf: [{ $ref: '#/components/schemas/SuccessResponse' }, { type: 'object', properties: { message: { type: 'string' } } }] } } } },
401: { $ref: '#/components/responses/Unauthorized' },
403: { $ref: '#/components/responses/Forbidden' },
404: { $ref: '#/components/responses/NotFound' },
},
},
},
'/tasks': {
get: {
tags: ['Tasks'],
summary: 'Danh sách công việc (Admin / Leader / Intern)',
security: [{ BearerAuth: [] }],
parameters: [
{ in: 'query', name: 'title', schema: { type: 'string' } },
{ in: 'query', name: 'priority', schema: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH'] } },
{ in: 'query', name: 'createdBy', schema: { type: 'string', format: 'uuid' } },
{ in: 'query', name: 'deadlineFrom', schema: { type: 'string', format: 'date-time' } },
{ in: 'query', name: 'deadlineTo', schema: { type: 'string', format: 'date-time' } },
{ in: 'query', name: 'sortBy', schema: { type: 'string', enum: ['createdAt', 'title', 'deadline', 'priority'], default: 'createdAt' } },
{ $ref: '#/components/parameters/OrderParam' },
{ $ref: '#/components/parameters/PageParam' },
{ $ref: '#/components/parameters/LimitParam' },
],
requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/UpdateUserBody' } } } },
responses: {
200: {
description: 'Danh sách công việc có phân trang',
content: { 'application/json': { schema: { allOf: [{ $ref: '#/components/schemas/SuccessResponse' }, { type: 'object', properties: { data: { type: 'array', items: { $ref: '#/components/schemas/Task' } }, meta: { $ref: '#/components/schemas/PaginationMeta' } } }] } } },
},
401: { $ref: '#/components/responses/Unauthorized' },
403: { $ref: '#/components/responses/Forbidden' },
},
},
post: {
tags: ['Tasks'],
summary: 'Tạo công việc mới (Admin / Leader)',
security: [{ BearerAuth: [] }],
requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/CreateTaskBody' } } } },
responses: {
201: {
description: 'Công việc đã được tạo',
content: { 'application/json': { schema: { allOf: [{ $ref: '#/components/schemas/SuccessResponse' }, { type: 'object', properties: { data: { $ref: '#/components/schemas/Task' } } }] } } },
description: 'Cập nhật thành công',
content: {
'application/json': {
schema: {
allOf: [
{ $ref: '#/components/schemas/SuccessResponse' },
{ type: 'object', properties: { data: { $ref: '#/components/schemas/User' } } },
],
},
},
},
},
401: { $ref: '#/components/responses/Unauthorized' },
403: { $ref: '#/components/responses/Forbidden' },
422: { $ref: '#/components/responses/Validation' },
},
},
},
'/tasks/{id}': {
get: {
tags: ['Tasks'],
summary: 'Chi tiết công việc theo ID',
security: [{ BearerAuth: [] }],
parameters: [{ in: 'path', name: 'id', required: true, schema: { type: 'string', format: 'uuid' } }],
responses: {
200: { description: 'Thông tin công việc', content: { 'application/json': { schema: { allOf: [{ $ref: '#/components/schemas/SuccessResponse' }, { type: 'object', properties: { data: { $ref: '#/components/schemas/Task' } } }] } } } },
401: { $ref: '#/components/responses/Unauthorized' },
403: { $ref: '#/components/responses/Forbidden' },
404: { $ref: '#/components/responses/NotFound' },
},
},
put: {
tags: ['Tasks'],
summary: 'Cập nhật công việc (Admin / Leader)',
security: [{ BearerAuth: [] }],
parameters: [{ in: 'path', name: 'id', required: true, schema: { type: 'string', format: 'uuid' } }],
requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/UpdateTaskBody' } } } },
responses: {
200: { description: 'Công việc đã được cập nhật', content: { 'application/json': { schema: { allOf: [{ $ref: '#/components/schemas/SuccessResponse' }, { type: 'object', properties: { data: { $ref: '#/components/schemas/Task' } } }] } } } },
401: { $ref: '#/components/responses/Unauthorized' },
403: { $ref: '#/components/responses/Forbidden' },
404: { $ref: '#/components/responses/NotFound' },
422: { $ref: '#/components/responses/Validation' },
},
},
delete: {
tags: ['Tasks'],
summary: 'Xoá mềm công việc (Admin / Leader)',
security: [{ BearerAuth: [] }],
parameters: [{ in: 'path', name: 'id', required: true, schema: { type: 'string', format: 'uuid' } }],
responses: {
200: { description: 'Đã xoá mềm', content: { 'application/json': { schema: { allOf: [{ $ref: '#/components/schemas/SuccessResponse' }, { type: 'object', properties: { message: { type: 'string' } } }] } } } },
401: { $ref: '#/components/responses/Unauthorized' },
403: { $ref: '#/components/responses/Forbidden' },
404: { $ref: '#/components/responses/NotFound' },
},
},
},
},
};
};
\ No newline at end of file
......@@ -3,7 +3,7 @@ import { Request, Response, NextFunction } from 'express';
const requestCounts = new Map<string, { count: number; resetAt: number }>();
const WINDOW_MS = 15 * 60 * 1000;
const MAX_REQUESTS = 100;
const MAX_REQUESTS = 1000;
export function rateLimitMiddleware(req: Request, res: Response, next: NextFunction): void {
const ip = req.ip || req.socket.remoteAddress || 'unknown';
......
import { Request, Response, NextFunction } from 'express';
import { ApplicationService } from './application.service';
import { ApplicationQueryDto } from './application.dto';
export class ApplicationController {
private readonly service = new ApplicationService();
findAll = async (req: Request, res: Response, next: NextFunction) => {
try {
const query = req.query as unknown as ApplicationQueryDto;
const result = await this.service.findAll(query);
res.json({ success: true, ...result });
} catch (error) {
next(error);
}
};
findById = async (req: Request, res: Response, next: NextFunction) => {
try {
const result = await this.service.findById(req.params.id);
res.json({ success: true, data: result });
} catch (error) {
next(error);
}
};
create = async (req: Request, res: Response, next: NextFunction) => {
try {
const result = await this.service.create(req.body);
res.status(201).json({ success: true, data: result });
} catch (error) {
next(error);
}
};
review = async (req: Request, res: Response, next: NextFunction) => {
try {
const approverId = req.user.id;
const result = await this.service.review(req.params.id, req.body, approverId);
res.json({ success: true, data: result });
} catch (error) {
next(error);
}
};
delete = async (req: Request, res: Response, next: NextFunction) => {
try {
await this.service.delete(req.params.id);
res.json({ success: true, message: 'Application deleted successfully' });
} catch (error) {
next(error);
}
};
}
import { ApplicationStatus } from '@prisma/client';
export interface ApplicationQueryDto {
status?: ApplicationStatus;
department?: string;
position?: string;
email?: string;
startDateFrom?: string;
startDateTo?: string;
sortBy?: 'createdAt' | 'startDate' | 'fullName' | 'status';
order?: 'asc' | 'desc';
page?: number;
limit?: number;
}
export interface CreateApplicationDto {
fullName: string;
email: string;
phone: string;
department: string;
position: string;
startDate: string;
duration: number;
}
export interface ReviewApplicationDto {
status: 'APPROVED' | 'REJECTED';
}
export interface ApplicationResponseDto {
id: string;
fullName: string;
email: string;
phone: string;
department: string;
position: string;
startDate: Date;
duration: number;
status: ApplicationStatus;
approvedBy: string | null;
approvedAt: Date | null;
createdAt: Date;
updatedAt: Date;
approver?: {
id: string;
email: string;
fullName: string | null;
} | null;
}
import { ApplicationStatus, Prisma } from '@prisma/client';
import { prisma } from '../../database/prisma.client';
import { ApplicationQueryDto } from './application.dto';
const approverSelect = {
id: true,
email: true,
fullName: true,
};
export class ApplicationRepository {
async findAll(query: ApplicationQueryDto) {
const {
status,
department,
position,
email,
startDateFrom,
startDateTo,
sortBy = 'createdAt',
order = 'desc',
page = 1,
limit = 20,
} = query;
const where: Prisma.ApplicationWhereInput = {
deletedAt: null,
...(status ? { status } : {}),
...(department ? { department: { contains: department, mode: 'insensitive' } } : {}),
...(position ? { position: { contains: position, mode: 'insensitive' } } : {}),
...(email ? { email: { contains: email, mode: 'insensitive' } } : {}),
...(startDateFrom || startDateTo
? {
startDate: {
...(startDateFrom ? { gte: new Date(startDateFrom) } : {}),
...(startDateTo ? { lte: new Date(startDateTo) } : {}),
},
}
: {}),
};
const skip = (page - 1) * limit;
const [data, total] = await prisma.$transaction([
prisma.application.findMany({
where,
include: { approver: { select: approverSelect } },
orderBy: { [sortBy]: order },
skip,
take: limit,
}),
prisma.application.count({ where }),
]);
return {
data,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
};
}
findById(id: string) {
return prisma.application.findFirst({
where: { id, deletedAt: null },
include: { approver: { select: approverSelect } },
});
}
create(data: {
fullName: string;
email: string;
phone: string;
department: string;
position: string;
startDate: Date;
duration: number;
}) {
return prisma.application.create({
data,
});
}
review(
id: string,
status: 'APPROVED' | 'REJECTED',
approverId: string,
) {
return prisma.application.update({
where: { id },
data: {
status,
approvedBy: approverId,
approvedAt: new Date(),
},
include: { approver: { select: approverSelect } },
});
}
softDelete(id: string) {
return prisma.application.update({
where: { id },
data: { deletedAt: new Date() },
});
}
}
import { Router } from 'express';
import { ApplicationController } from './application.controller';
import { authMiddleware } from '../../middlewares/auth.middleware';
import { requireRole } from '../../middlewares/role.middleware';
import { validate } from '../../middlewares/validate.middleware';
import {
createApplicationSchema,
findAllApplicationSchema,
reviewApplicationSchema,
} from './application.validation';
const router = Router();
const controller = new ApplicationController();
router.post('/', validate(createApplicationSchema), controller.create);
// GET /applications?status=...&department=...&page=...&limit=...&sortBy=...&order=...
router.get('/', authMiddleware, requireRole('ADMIN', 'LEADER'), validate(findAllApplicationSchema, 'query'), controller.findAll);
router.get('/:id', authMiddleware, requireRole('ADMIN', 'LEADER'), controller.findById);
router.patch('/:id/review', authMiddleware, requireRole('ADMIN', 'LEADER'), validate(reviewApplicationSchema), controller.review);
router.delete('/:id', authMiddleware, requireRole('ADMIN'), controller.delete);
export default router;
import { ApplicationRepository } from './application.repository';
import { AppError } from '../../common/errors/app-error';
import { ERROR_CODE } from '../../common/errors/error-code';
import { ApplicationQueryDto, CreateApplicationDto, ReviewApplicationDto } from './application.dto';
export class ApplicationService {
private readonly repository = new ApplicationRepository();
async findAll(query: ApplicationQueryDto) {
return this.repository.findAll(query);
}
async findById(id: string) {
const application = await this.repository.findById(id);
if (!application) {
throw new AppError('Application not found', 404, ERROR_CODE.NOT_FOUND);
}
return application;
}
async create(data: CreateApplicationDto) {
return this.repository.create({
fullName: data.fullName,
email: data.email,
phone: data.phone,
department: data.department,
position: data.position,
startDate: new Date(data.startDate),
duration: data.duration,
});
}
async review(id: string, dto: ReviewApplicationDto, approverId: string) {
const application = await this.findById(id);
if (application.status !== 'PENDING') {
throw new AppError(
`Application is already ${application.status.toLowerCase()}`,
409,
ERROR_CODE.DUPLICATE_ENTRY,
);
}
return this.repository.review(id, dto.status, approverId);
}
async delete(id: string) {
const application = await this.findById(id);
if (application.status === 'APPROVED') {
throw new AppError(
'Cannot delete an approved application',
409,
ERROR_CODE.DUPLICATE_ENTRY,
);
}
return this.repository.softDelete(id);
}
}
import { z } from 'zod';
export const findAllApplicationSchema = z.object({
status: z.enum(['PENDING', 'APPROVED', 'REJECTED']).optional(),
department: z.string().optional(),
position: z.string().optional(),
email: z.string().email('Invalid email').optional(),
startDateFrom: z
.string()
.refine((v) => !isNaN(Date.parse(v)), { message: 'startDateFrom must be a valid ISO date' })
.optional(),
startDateTo: z
.string()
.refine((v) => !isNaN(Date.parse(v)), { message: 'startDateTo must be a valid ISO date' })
.optional(),
sortBy: z.enum(['createdAt', 'startDate', 'fullName', 'status']).optional(),
order: z.enum(['asc', 'desc']).optional(),
page: z.coerce.number().int().positive().optional().default(1),
limit: z.coerce.number().int().min(1).max(100).optional().default(20),
});
export const createApplicationSchema = z.object({
fullName: z.string().min(1, 'Full name is required').max(100),
email: z.string().min(1, 'Email is required').email('Invalid email format'),
phone: z.string().min(9, 'Phone must be at least 9 digits').max(15),
department: z.string().min(1, 'Department is required'),
position: z.string().min(1, 'Position is required'),
startDate: z.string().refine((val) => !isNaN(Date.parse(val)), {
message: 'startDate must be a valid ISO date string',
}),
duration: z
.number({ invalid_type_error: 'Duration must be a number' })
.int()
.positive('Duration must be a positive integer'),
});
export const reviewApplicationSchema = z.object({
status: z.enum(['APPROVED', 'REJECTED'], {
errorMap: () => ({ message: 'status must be APPROVED or REJECTED' }),
}),
});
import { Request, Response, NextFunction } from 'express';
import { AuthService } from './auth.service';
import { LoginDto } from './auth.dto';
export class AuthController {
private readonly service = new AuthService();
login = async (req: Request, res: Response, next: NextFunction) => {
try {
const { email, password } = req.body;
const body = req.body as LoginDto;
const userAgent = req.headers['user-agent'];
const ipAddress = req.ip;
const result = await this.service.login(email, password, { userAgent, ipAddress });
const result = await this.service.login(body, { userAgent, ipAddress });
res.json({
success: true,
......
......@@ -16,3 +16,14 @@ export interface MeDto {
isActive: boolean;
createdAt: Date;
}
export interface LoginResponseDto {
accessToken: string;
refreshToken: string;
user: {
id: string;
email: string;
fullName: string | null;
role: string;
};
}
......@@ -4,11 +4,13 @@ import { AuthRepository } from './auth.repository';
import { AppError } from '../../common/errors/app-error';
import { ERROR_CODE } from '../../common/errors/error-code';
import { jwtConfig } from '../../config/jwt.config';
import { LoginDto, LoginResponseDto, AuthTokensDto, MeDto } from './auth.dto';
export class AuthService {
private readonly repository = new AuthRepository();
async login(email: string, password: string, metadata?: { userAgent?: string; ipAddress?: string }) {
async login(data: LoginDto, metadata?: { userAgent?: string; ipAddress?: string }): Promise<LoginResponseDto> {
const { email, password } = data;
const user = await this.repository.findByEmail(email);
if (!user) {
......@@ -51,7 +53,7 @@ export class AuthService {
};
}
async refresh(token: string, metadata?: { userAgent?: string; ipAddress?: string }) {
async refresh(token: string, metadata?: { userAgent?: string; ipAddress?: string }): Promise<AuthTokensDto> {
let payload: any;
try {
payload = jwt.verify(token, jwtConfig.refreshSecret);
......@@ -104,7 +106,7 @@ export class AuthService {
}
}
async getMe(userId: string) {
async getMe(userId: string): Promise<MeDto> {
const user = await this.repository.findById(userId);
if (!user || !user.isActive) {
......
import { Request, Response, NextFunction } from 'express';
import { InternService } from './intern.service';
import { InternQueryDto } from './intern.dto';
export class InternController {
private readonly service = new InternService();
findAll = async (req: Request, res: Response, next: NextFunction) => {
try {
const query = req.query as unknown as InternQueryDto;
const result = await this.service.findAll(query);
res.json({ success: true, ...result });
} catch (error) {
next(error);
}
};
findById = async (req: Request, res: Response, next: NextFunction) => {
try {
const result = await this.service.findById(req.params.id);
res.json({ success: true, data: result });
} catch (error) {
next(error);
}
};
create = async (req: Request, res: Response, next: NextFunction) => {
try {
const result = await this.service.create(req.body);
res.status(201).json({ success: true, data: result });
} catch (error) {
next(error);
}
};
update = async (req: Request, res: Response, next: NextFunction) => {
try {
const result = await this.service.update(req.params.id, req.body);
res.json({ success: true, data: result });
} catch (error) {
next(error);
}
};
delete = async (req: Request, res: Response, next: NextFunction) => {
try {
await this.service.delete(req.params.id);
res.json({ success: true, message: 'Intern deleted successfully' });
} catch (error) {
next(error);
}
};
}
import { InternStatus } from '@prisma/client';
export interface InternQueryDto {
fullName?: string;
department?: string;
position?: string;
status?: InternStatus;
leaderId?: string;
discordRoleGranted?: boolean;
startDateFrom?: string;
startDateTo?: string;
sortBy?: 'createdAt' | 'fullName' | 'startDate' | 'status';
order?: 'asc' | 'desc';
page?: number;
limit?: number;
}
export interface CreateInternDto {
userId: string;
leaderId?: string;
fullName: string;
phone: string;
department: string;
position: string;
startDate: string;
duration: number;
discordUsername?: string;
}
export interface UpdateInternDto {
leaderId?: string | null;
fullName?: string;
phone?: string;
department?: string;
position?: string;
startDate?: string;
duration?: number;
discordUsername?: string | null;
discordRoleGranted?: boolean;
status?: InternStatus;
}
import { InternStatus, Prisma } from '@prisma/client';
import { prisma } from '../../database/prisma.client';
import { InternQueryDto, CreateInternDto, UpdateInternDto } from './intern.dto';
const userSelect = {
id: true,
email: true,
fullName: true,
isActive: true,
};
const defaultInclude = {
user: { select: userSelect },
leader: { select: userSelect },
};
export class InternRepository {
async findAll(query: InternQueryDto) {
const {
fullName,
department,
position,
status,
leaderId,
discordRoleGranted,
startDateFrom,
startDateTo,
sortBy = 'createdAt',
order = 'desc',
page = 1,
limit = 20,
} = query;
const where: Prisma.InternWhereInput = {
deletedAt: null,
...(fullName ? { fullName: { contains: fullName, mode: 'insensitive' } } : {}),
...(department ? { department: { contains: department, mode: 'insensitive' } } : {}),
...(position ? { position: { contains: position, mode: 'insensitive' } } : {}),
...(status ? { status } : {}),
...(leaderId ? { leaderId } : {}),
...(discordRoleGranted !== undefined ? { discordRoleGranted } : {}),
...(startDateFrom || startDateTo
? {
startDate: {
...(startDateFrom ? { gte: new Date(startDateFrom) } : {}),
...(startDateTo ? { lte: new Date(startDateTo) } : {}),
},
}
: {}),
};
const skip = (page - 1) * limit;
const [data, total] = await prisma.$transaction([
prisma.intern.findMany({
where,
include: defaultInclude,
orderBy: { [sortBy]: order },
skip,
take: limit,
}),
prisma.intern.count({ where }),
]);
return {
data,
meta: { total, page, limit, totalPages: Math.ceil(total / limit) },
};
}
findById(id: string) {
return prisma.intern.findFirst({
where: { id, deletedAt: null },
include: defaultInclude,
});
}
findByUserId(userId: string) {
return prisma.intern.findFirst({
where: { userId, deletedAt: null },
include: defaultInclude,
});
}
create(data: CreateInternDto) {
return prisma.intern.create({
data: {
userId: data.userId,
leaderId: data.leaderId,
fullName: data.fullName,
phone: data.phone,
department: data.department,
position: data.position,
startDate: new Date(data.startDate),
duration: data.duration,
discordUsername: data.discordUsername,
},
include: defaultInclude,
});
}
update(id: string, data: UpdateInternDto) {
return prisma.intern.update({
where: { id },
data: {
...(data.leaderId !== undefined ? { leaderId: data.leaderId } : {}),
...(data.fullName !== undefined ? { fullName: data.fullName } : {}),
...(data.phone !== undefined ? { phone: data.phone } : {}),
...(data.department !== undefined ? { department: data.department } : {}),
...(data.position !== undefined ? { position: data.position } : {}),
...(data.startDate !== undefined ? { startDate: new Date(data.startDate)} : {}),
...(data.duration !== undefined ? { duration: data.duration } : {}),
...(data.discordUsername !== undefined ? { discordUsername: data.discordUsername } : {}),
...(data.discordRoleGranted !== undefined ? { discordRoleGranted: data.discordRoleGranted } : {}),
...(data.status !== undefined ? { status: data.status } : {}),
},
include: defaultInclude,
});
}
softDelete(id: string) {
return prisma.intern.update({
where: { id },
data: { deletedAt: new Date() },
include: defaultInclude,
});
}
}
import { Router } from 'express';
import { InternController } from './intern.controller';
import { authMiddleware } from '../../middlewares/auth.middleware';
import { requireRole } from '../../middlewares/role.middleware';
import { validate } from '../../middlewares/validate.middleware';
import {
findAllInternSchema,
createInternSchema,
updateInternSchema,
} from './intern.validation';
const router = Router();
const controller = new InternController();
// GET /interns?fullName=...&department=...&position=...&status=...&leaderId=...&discordRoleGranted=...&startDateFrom=...&startDateTo=...&sortBy=...&order=...&page=...&limit=...
router.get('/', authMiddleware, requireRole('ADMIN', 'LEADER'), validate(findAllInternSchema, 'query'), controller.findAll);
router.get('/:id', authMiddleware, requireRole('ADMIN', 'LEADER'), controller.findById);
router.post('/', authMiddleware, requireRole('ADMIN', 'LEADER'), validate(createInternSchema), controller.create);
router.put('/:id', authMiddleware, requireRole('ADMIN', 'LEADER'), validate(updateInternSchema), controller.update);
router.delete('/:id', authMiddleware, requireRole('ADMIN'), controller.delete);
export default router;
import { InternRepository } from './intern.repository';
import { AppError } from '../../common/errors/app-error';
import { ERROR_CODE } from '../../common/errors/error-code';
import {
InternQueryDto,
CreateInternDto,
UpdateInternDto,
} from './intern.dto';
export class InternService {
private readonly repository = new InternRepository();
async findAll(query: InternQueryDto) {
return this.repository.findAll(query);
}
async findById(id: string) {
const profile = await this.repository.findById(id);
if (!profile) {
throw new AppError('Intern not found', 404, ERROR_CODE.NOT_FOUND);
}
return profile;
}
async create(data: CreateInternDto) {
const existing = await this.repository.findByUserId(data.userId);
if (existing) {
throw new AppError(
'This user already has an intern profile',
409,
ERROR_CODE.DUPLICATE_ENTRY,
);
}
return this.repository.create(data);
}
async update(id: string, data: UpdateInternDto) {
await this.findById(id);
return this.repository.update(id, data);
}
async delete(id: string) {
await this.findById(id);
return this.repository.softDelete(id);
}
}
import { z } from 'zod';
export const findAllInternSchema = z.object({
fullName: z.string().optional(),
department: z.string().optional(),
position: z.string().optional(),
status: z.enum(['ACTIVE', 'COMPLETED', 'DROPPED']).optional(),
leaderId: z.string().uuid('Invalid leaderId').optional(),
discordRoleGranted: z
.enum(['true', 'false'])
.transform((v) => v === 'true')
.optional(),
startDateFrom: z
.string()
.refine((v) => !isNaN(Date.parse(v)), { message: 'startDateFrom must be a valid ISO date' })
.optional(),
startDateTo: z
.string()
.refine((v) => !isNaN(Date.parse(v)), { message: 'startDateTo must be a valid ISO date' })
.optional(),
sortBy: z.enum(['createdAt', 'fullName', 'startDate', 'status']).optional(),
order: z.enum(['asc', 'desc']).optional(),
page: z.coerce.number().int().positive().optional().default(1),
limit: z.coerce.number().int().min(1).max(100).optional().default(20),
});
export const createInternSchema = z.object({
userId: z.string().uuid('Invalid userId'),
leaderId: z.string().uuid('Invalid leaderId').optional(),
fullName: z.string().min(1, 'Full name is required').max(100),
phone: z.string().min(9).max(15),
department: z.string().min(1, 'Department is required'),
position: z.string().min(1, 'Position is required'),
startDate: z.string().refine((v) => !isNaN(Date.parse(v)), {
message: 'startDate must be a valid ISO date',
}),
duration: z.number().int().positive('Duration must be a positive integer'),
discordUsername: z.string().optional(),
});
export const updateInternSchema = z.object({
leaderId: z.string().uuid().nullable().optional(),
fullName: z.string().min(1).max(100).optional(),
phone: z.string().min(9).max(15).optional(),
department: z.string().optional(),
position: z.string().optional(),
startDate: z.string().refine((v) => !isNaN(Date.parse(v)), { message: 'Invalid date' }).optional(),
duration: z.number().int().positive().optional(),
discordUsername: z.string().nullable().optional(),
discordRoleGranted: z.boolean().optional(),
status: z.enum(['ACTIVE', 'COMPLETED', 'DROPPED']).optional(),
});
import { Request, Response, NextFunction } from 'express';
import { TaskService } from './task.service';
import { TaskQueryDto } from './task.dto';
export class TaskController {
private readonly service = new TaskService();
findAll = async (req: Request, res: Response, next: NextFunction) => {
try {
const query = req.query as unknown as TaskQueryDto;
const result = await this.service.findAll(query);
res.json({ success: true, ...result });
} catch (error) {
next(error);
}
};
findById = async (req: Request, res: Response, next: NextFunction) => {
try {
const result = await this.service.findById(req.params.id);
res.json({ success: true, data: result });
} catch (error) {
next(error);
}
};
create = async (req: Request, res: Response, next: NextFunction) => {
try {
const createdBy = req.user.id;
const result = await this.service.create(req.body, createdBy);
res.status(201).json({ success: true, data: result });
} catch (error) {
next(error);
}
};
update = async (req: Request, res: Response, next: NextFunction) => {
try {
const result = await this.service.update(req.params.id, req.body);
res.json({ success: true, data: result });
} catch (error) {
next(error);
}
};
delete = async (req: Request, res: Response, next: NextFunction) => {
try {
await this.service.delete(req.params.id);
res.json({ success: true, message: 'Task deleted successfully' });
} catch (error) {
next(error);
}
};
}
import { TaskPriority } from '@prisma/client';
export interface TaskQueryDto {
title?: string;
priority?: TaskPriority;
createdBy?: string;
deadlineFrom?: string;
deadlineTo?: string;
sortBy?: 'createdAt' | 'title' | 'deadline' | 'priority';
order?: 'asc' | 'desc';
page?: number;
limit?: number;
}
export interface CreateTaskDto {
title: string;
description?: string;
deadline: string;
priority?: TaskPriority;
}
export interface UpdateTaskDto {
title?: string;
description?: string | null;
deadline?: string;
priority?: TaskPriority;
}
import { Prisma } from '@prisma/client';
import { prisma } from '../../database/prisma.client';
import { TaskQueryDto, CreateTaskDto, UpdateTaskDto } from './task.dto';
const creatorSelect = {
id: true,
email: true,
fullName: true,
};
const defaultInclude = {
creator: { select: creatorSelect },
assignment: true,
};
export class TaskRepository {
async findAll(query: TaskQueryDto) {
const {
title,
priority,
createdBy,
deadlineFrom,
deadlineTo,
sortBy = 'createdAt',
order = 'desc',
page = 1,
limit = 20,
} = query;
const where: Prisma.TaskWhereInput = {
deletedAt: null,
...(title ? { title: { contains: title, mode: 'insensitive' } } : {}),
...(priority ? { priority } : {}),
...(createdBy ? { createdBy } : {}),
...(deadlineFrom || deadlineTo
? {
deadline: {
...(deadlineFrom ? { gte: new Date(deadlineFrom) } : {}),
...(deadlineTo ? { lte: new Date(deadlineTo) } : {}),
},
}
: {}),
};
const skip = (page - 1) * limit;
const [data, total] = await prisma.$transaction([
prisma.task.findMany({
where,
include: defaultInclude,
orderBy: { [sortBy]: order },
skip,
take: limit,
}),
prisma.task.count({ where }),
]);
return {
data,
meta: { total, page, limit, totalPages: Math.ceil(total / limit) },
};
}
findById(id: string) {
return prisma.task.findFirst({
where: { id, deletedAt: null },
include: defaultInclude,
});
}
create(data: CreateTaskDto, createdBy: string) {
return prisma.task.create({
data: {
title: data.title,
description: data.description,
deadline: new Date(data.deadline),
priority: data.priority,
createdBy,
},
include: defaultInclude,
});
}
update(id: string, data: UpdateTaskDto) {
return prisma.task.update({
where: { id },
data: {
...(data.title !== undefined ? { title: data.title } : {}),
...(data.description !== undefined ? { description: data.description } : {}),
...(data.deadline !== undefined ? { deadline: new Date(data.deadline) } : {}),
...(data.priority !== undefined ? { priority: data.priority } : {}),
},
include: defaultInclude,
});
}
softDelete(id: string) {
return prisma.task.update({
where: { id },
data: { deletedAt: new Date() },
include: defaultInclude,
});
}
}
import { Router } from 'express';
import { TaskController } from './task.controller';
import { authMiddleware } from '../../middlewares/auth.middleware';
import { requireRole } from '../../middlewares/role.middleware';
import { validate } from '../../middlewares/validate.middleware';
import {
findAllTaskSchema,
createTaskSchema,
updateTaskSchema,
} from './task.validation';
const router = Router();
const controller = new TaskController();
// GET /tasks?page=...&limit=...&sortBy=...&order=...&title=...&priority=...&createdBy=...&deadlineFrom=...&deadlineTo=...
router.get('/', authMiddleware, requireRole('ADMIN', 'LEADER', 'INTERN'), validate(findAllTaskSchema, 'query'), controller.findAll);
router.get('/:id', authMiddleware, requireRole('ADMIN', 'LEADER', 'INTERN'), controller.findById);
router.post('/', authMiddleware, requireRole('ADMIN', 'LEADER'), validate(createTaskSchema), controller.create);
router.put('/:id', authMiddleware, requireRole('ADMIN', 'LEADER'), validate(updateTaskSchema), controller.update);
router.delete('/:id', authMiddleware, requireRole('ADMIN', 'LEADER'), controller.delete);
export default router;
\ No newline at end of file
import { TaskRepository } from './task.repository';
import { AppError } from '../../common/errors/app-error';
import { ERROR_CODE } from '../../common/errors/error-code';
import { TaskQueryDto, CreateTaskDto, UpdateTaskDto } from './task.dto';
export class TaskService {
private readonly repository = new TaskRepository();
async findAll(query: TaskQueryDto) {
return this.repository.findAll(query);
}
async findById(id: string) {
const task = await this.repository.findById(id);
if (!task) {
throw new AppError('Task not found', 404, ERROR_CODE.NOT_FOUND);
}
return task;
}
async create(data: CreateTaskDto, createdBy: string) {
return this.repository.create(data, createdBy);
}
async update(id: string, data: UpdateTaskDto) {
await this.findById(id);
return this.repository.update(id, data);
}
async delete(id: string) {
await this.findById(id);
return this.repository.softDelete(id);
}
}
import { z } from 'zod';
export const findAllTaskSchema = z.object({
title: z.string().optional(),
priority: z.enum(['LOW', 'MEDIUM', 'HIGH']).optional(),
createdBy: z.string().uuid().optional(),
deadlineFrom: z
.string()
.refine((v) => !isNaN(Date.parse(v)), { message: 'Invalid deadlineFrom' })
.optional(),
deadlineTo: z
.string()
.refine((v) => !isNaN(Date.parse(v)), { message: 'Invalid deadlineTo' })
.optional(),
sortBy: z.enum(['createdAt', 'title', 'deadline', 'priority']).optional(),
order: z.enum(['asc', 'desc']).optional(),
page: z.coerce.number().int().positive().optional().default(1),
limit: z.coerce.number().int().min(1).max(100).optional().default(20),
});
export const createTaskSchema = z.object({
title: z.string().min(1).max(200),
description: z.string().optional(),
deadline: z.string().refine((v) => !isNaN(Date.parse(v)), {
message: 'Invalid deadline',
}),
priority: z.enum(['LOW', 'MEDIUM', 'HIGH']).optional(),
});
export const updateTaskSchema = z.object({
title: z.string().min(1).max(200).optional(),
description: z.string().nullable().optional(),
deadline: z
.string()
.refine((v) => !isNaN(Date.parse(v)), { message: 'Invalid deadline' })
.optional(),
priority: z.enum(['LOW', 'MEDIUM', 'HIGH']).optional(),
});
import { Request, Response, NextFunction } from 'express';
import { UserService } from './user.service';
import { UserQueryDto } from './user.dto';
import { UserQueryDto, CreateUserDto, UpdateUserDto } from './user.dto';
export class UserController {
private readonly service = new UserService();
......@@ -31,7 +31,8 @@ export class UserController {
create = async (req: Request, res: Response, next: NextFunction) => {
try {
const result = await this.service.create(req.body);
const body = req.body as CreateUserDto;
const result = await this.service.create(body);
res.status(201).json({
success: true,
......@@ -44,7 +45,8 @@ export class UserController {
update = async (req: Request, res: Response, next: NextFunction) => {
try {
const result = await this.service.update(req.params.id, req.body);
const body = req.body as UpdateUserDto;
const result = await this.service.update(req.params.id, body);
res.json({
success: true,
......
......@@ -2,7 +2,7 @@ import bcrypt from 'bcryptjs';
import { UserRepository } from './user.repository';
import { AppError } from '../../common/errors/app-error';
import { ERROR_CODE } from '../../common/errors/error-code';
import { UserQueryDto } from './user.dto';
import { UserQueryDto, CreateUserDto, UpdateUserDto } from './user.dto';
export class UserService {
private readonly repository = new UserRepository();
......@@ -21,7 +21,7 @@ export class UserService {
return user;
}
async create(data: { email: string; password: string; roleId: string }) {
async create(data: CreateUserDto) {
const existing = await this.repository.findByEmail(data.email);
if (existing) {
......@@ -37,7 +37,7 @@ export class UserService {
});
}
async update(id: string, data: { isActive?: boolean; roleId?: string }) {
async update(id: string, data: UpdateUserDto) {
await this.findById(id);
return this.repository.update(id, {
......
import { Router } from 'express';
import authRoute from '../modules/auth/auth.route';
import userRoute from '../modules/users/user.route';
import applicationRoute from '../modules/applications/application.route';
import internRoute from '../modules/interns/intern.route';
import taskRoute from '../modules/tasks/task.route';
const router = Router();
......@@ -13,8 +10,5 @@ router.get('/health', (req, res) => {
router.use('/auth', authRoute);
router.use('/users', userRoute);
router.use('/applications', applicationRoute);
router.use('/interns', internRoute);
router.use('/tasks', taskRoute);
export default router;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment