From ec8342b5e2a001616803633fe3d6dc0b13be4841 Mon Sep 17 00:00:00 2001 From: Nixon Date: Sat, 20 Sep 2025 10:45:21 +0200 Subject: [PATCH] Integrate complete Candidat platform with ASP.NET chatbot service - Added Next.js frontend for candidate interviews - Added Node.js backend with TypeScript and AI integration - Added ASP.NET Core chatbot service for specialized AI conversations - Added MySQL database with complete schema - Added Nginx reverse proxy configuration - Complete Docker Compose orchestration for all services - Environment configuration for production, development, and Cloudflare - Comprehensive documentation and setup instructions - Flattened nested folder structures for clean organization - Integrated chatbot service with fallback to direct AI calls --- TODO.md | 153 + backend/.barrels.json | 9 + backend/.dockerignore | 14 + backend/.gitignore | 61 + backend/.swcrc | 21 + backend/ADMIN_API.md | 306 + backend/AI_CONFIGURATION.md | 52 + backend/Dockerfile | 30 + backend/README.md | 67 + backend/add-icon-column.sql | 3 + backend/add-interview-style-column.sql | 7 + backend/add-tokens-used-column.sql | 7 + backend/create-job-links-table.sql | 13 + backend/docker-compose.yml | 46 + backend/nodemon.json | 9 + backend/package-lock.json | 5573 +++++++++++++++++ backend/package.json | 77 + backend/processes.config.cjs | 22 + backend/secret.txt | 2 + backend/src/Server.ts | 101 + backend/src/config/database.ts | 54 + backend/src/config/envs/index.ts | 7 + backend/src/config/index.ts | 14 + backend/src/config/logger/index.ts | 25 + .../src/controllers/pages/IndexController.ts | 29 + backend/src/controllers/pages/index.ts | 4 + backend/src/controllers/rest/AIController.ts | 644 ++ .../src/controllers/rest/AdminController.ts | 249 + .../src/controllers/rest/AuthController.ts | 167 + .../controllers/rest/HelloWorldController.ts | 10 + backend/src/controllers/rest/JobController.ts | 501 ++ .../src/controllers/rest/UserController.ts | 75 + backend/src/controllers/rest/index.ts | 9 + backend/src/index.ts | 36 + backend/src/middleware/adminAuth.ts | 51 + backend/src/models/User.ts | 66 + backend/src/services/AIService.ts | 294 + backend/src/services/AdminService.ts | 776 +++ backend/src/services/ChatbotService.ts | 170 + backend/src/services/JobService.ts | 1070 ++++ backend/src/services/TokenService.ts | 443 ++ backend/src/services/UserService.ts | 223 + backend/src/types/admin.ts | 171 + backend/src/types/auth.ts | 51 + backend/tsconfig.base.json | 29 + backend/tsconfig.json | 13 + backend/tsconfig.node.json | 20 + backend/update-admin-password.sql | 1 + backend/views/index.ejs | 95 + backend/views/swagger.ejs | 100 + database/Dockerfile | 15 + .../candidb_dump1/candidb_main_audit_logs.sql | 55 + .../candidb_main_candidate_responses.sql | 53 + .../candidb_dump1/candidb_main_candidates.sql | 63 + .../candidb_main_conversation_messages.sql | 52 + .../candidb_main_interview_events.sql | 52 + .../candidb_main_interview_questions.sql | 52 + .../candidb_main_interview_tokens.sql | 79 + .../candidb_dump1/candidb_main_interviews.sql | 96 + .../candidb_dump1/candidb_main_job_links.sql | 52 + database/candidb_dump1/candidb_main_jobs.sql | 85 + .../candidb_main_payment_records.sql | 59 + .../candidb_dump1/candidb_main_routines.sql | 631 ++ .../candidb_main_token_packages.sql | 55 + .../candidb_dump1/candidb_main_user_usage.sql | 53 + database/candidb_dump1/candidb_main_users.sql | 60 + database/deploy_dump.sql | 703 +++ docker-compose.yml | 198 +- env.cloudflare | 38 + env.example | 38 + env.production | 38 + frontend/.dockerignore | 13 + frontend/.gitignore | 41 + frontend/Dockerfile | 55 + frontend/FRONTEND_README.md | 258 + frontend/README.md | 36 + frontend/next.config.js | 24 + frontend/next.config.ts | 7 + frontend/package-lock.json | 3761 +++++++++++ frontend/package.json | 30 + frontend/postcss.config.mjs | 5 + frontend/public/file.svg | 1 + frontend/public/globe.svg | 1 + frontend/public/next.svg | 1 + frontend/public/vercel.svg | 1 + frontend/public/window.svg | 1 + frontend/src/app/admin/page.tsx | 145 + frontend/src/app/dashboard/page.tsx | 204 + frontend/src/app/docs/page.tsx | 101 + frontend/src/app/favicon.ico | Bin 0 -> 25931 bytes frontend/src/app/globals.css | 236 + frontend/src/app/interview/page.tsx | 275 + frontend/src/app/layout.tsx | 37 + frontend/src/app/login/page.tsx | 262 + frontend/src/app/page.tsx | 491 ++ frontend/src/components/AdminDashboard.tsx | 240 + frontend/src/components/AdminHeader.tsx | 102 + frontend/src/components/AdminLayout.tsx | 46 + frontend/src/components/AdminSidebar.tsx | 101 + frontend/src/components/AnimatedCounter.tsx | 55 + frontend/src/components/ChatScreen.tsx | 403 ++ frontend/src/components/ConsentScreen.tsx | 166 + frontend/src/components/CreateJobModal.tsx | 788 +++ frontend/src/components/DeveloperTools.tsx | 39 + frontend/src/components/FeatureCard.tsx | 59 + frontend/src/components/Header.tsx | 182 + frontend/src/components/JobCard.tsx | 1079 ++++ frontend/src/components/JobManagement.tsx | 495 ++ frontend/src/components/JobsList.tsx | 199 + frontend/src/components/Layout.tsx | 48 + .../components/MandatoryQuestionsScreen.tsx | 185 + frontend/src/components/NameInputScreen.tsx | 125 + frontend/src/components/PricingCard.tsx | 86 + frontend/src/components/Sidebar.tsx | 76 + frontend/src/components/SystemStats.tsx | 367 ++ frontend/src/components/TechStackCard.tsx | 52 + frontend/src/components/ThemeToggle.tsx | 41 + frontend/src/components/TokenManagement.tsx | 678 ++ frontend/src/components/TokenModal.tsx | 152 + frontend/src/components/UserManagement.tsx | 599 ++ frontend/src/components/index.ts | 29 + frontend/src/types/index.ts | 77 + frontend/tsconfig.json | 27 + nginx/CLOUDFLARE_SETUP.md | 176 + nginx/DNS_SETUP.md | 183 + nginx/nginx.conf | 44 + nginx/ssl-setup.ps1 | 116 + nginx/ssl-setup.sh | 93 + 128 files changed, 27581 insertions(+), 10 deletions(-) create mode 100644 TODO.md create mode 100644 backend/.barrels.json create mode 100644 backend/.dockerignore create mode 100644 backend/.gitignore create mode 100644 backend/.swcrc create mode 100644 backend/ADMIN_API.md create mode 100644 backend/AI_CONFIGURATION.md create mode 100644 backend/Dockerfile create mode 100644 backend/README.md create mode 100644 backend/add-icon-column.sql create mode 100644 backend/add-interview-style-column.sql create mode 100644 backend/add-tokens-used-column.sql create mode 100644 backend/create-job-links-table.sql create mode 100644 backend/docker-compose.yml create mode 100644 backend/nodemon.json create mode 100644 backend/package-lock.json create mode 100644 backend/package.json create mode 100644 backend/processes.config.cjs create mode 100644 backend/secret.txt create mode 100644 backend/src/Server.ts create mode 100644 backend/src/config/database.ts create mode 100644 backend/src/config/envs/index.ts create mode 100644 backend/src/config/index.ts create mode 100644 backend/src/config/logger/index.ts create mode 100644 backend/src/controllers/pages/IndexController.ts create mode 100644 backend/src/controllers/pages/index.ts create mode 100644 backend/src/controllers/rest/AIController.ts create mode 100644 backend/src/controllers/rest/AdminController.ts create mode 100644 backend/src/controllers/rest/AuthController.ts create mode 100644 backend/src/controllers/rest/HelloWorldController.ts create mode 100644 backend/src/controllers/rest/JobController.ts create mode 100644 backend/src/controllers/rest/UserController.ts create mode 100644 backend/src/controllers/rest/index.ts create mode 100644 backend/src/index.ts create mode 100644 backend/src/middleware/adminAuth.ts create mode 100644 backend/src/models/User.ts create mode 100644 backend/src/services/AIService.ts create mode 100644 backend/src/services/AdminService.ts create mode 100644 backend/src/services/ChatbotService.ts create mode 100644 backend/src/services/JobService.ts create mode 100644 backend/src/services/TokenService.ts create mode 100644 backend/src/services/UserService.ts create mode 100644 backend/src/types/admin.ts create mode 100644 backend/src/types/auth.ts create mode 100644 backend/tsconfig.base.json create mode 100644 backend/tsconfig.json create mode 100644 backend/tsconfig.node.json create mode 100644 backend/update-admin-password.sql create mode 100644 backend/views/index.ejs create mode 100644 backend/views/swagger.ejs create mode 100644 database/Dockerfile create mode 100644 database/candidb_dump1/candidb_main_audit_logs.sql create mode 100644 database/candidb_dump1/candidb_main_candidate_responses.sql create mode 100644 database/candidb_dump1/candidb_main_candidates.sql create mode 100644 database/candidb_dump1/candidb_main_conversation_messages.sql create mode 100644 database/candidb_dump1/candidb_main_interview_events.sql create mode 100644 database/candidb_dump1/candidb_main_interview_questions.sql create mode 100644 database/candidb_dump1/candidb_main_interview_tokens.sql create mode 100644 database/candidb_dump1/candidb_main_interviews.sql create mode 100644 database/candidb_dump1/candidb_main_job_links.sql create mode 100644 database/candidb_dump1/candidb_main_jobs.sql create mode 100644 database/candidb_dump1/candidb_main_payment_records.sql create mode 100644 database/candidb_dump1/candidb_main_routines.sql create mode 100644 database/candidb_dump1/candidb_main_token_packages.sql create mode 100644 database/candidb_dump1/candidb_main_user_usage.sql create mode 100644 database/candidb_dump1/candidb_main_users.sql create mode 100644 database/deploy_dump.sql create mode 100644 env.cloudflare create mode 100644 env.example create mode 100644 env.production create mode 100644 frontend/.dockerignore create mode 100644 frontend/.gitignore create mode 100644 frontend/Dockerfile create mode 100644 frontend/FRONTEND_README.md create mode 100644 frontend/README.md create mode 100644 frontend/next.config.js create mode 100644 frontend/next.config.ts create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.mjs create mode 100644 frontend/public/file.svg create mode 100644 frontend/public/globe.svg create mode 100644 frontend/public/next.svg create mode 100644 frontend/public/vercel.svg create mode 100644 frontend/public/window.svg create mode 100644 frontend/src/app/admin/page.tsx create mode 100644 frontend/src/app/dashboard/page.tsx create mode 100644 frontend/src/app/docs/page.tsx create mode 100644 frontend/src/app/favicon.ico create mode 100644 frontend/src/app/globals.css create mode 100644 frontend/src/app/interview/page.tsx create mode 100644 frontend/src/app/layout.tsx create mode 100644 frontend/src/app/login/page.tsx create mode 100644 frontend/src/app/page.tsx create mode 100644 frontend/src/components/AdminDashboard.tsx create mode 100644 frontend/src/components/AdminHeader.tsx create mode 100644 frontend/src/components/AdminLayout.tsx create mode 100644 frontend/src/components/AdminSidebar.tsx create mode 100644 frontend/src/components/AnimatedCounter.tsx create mode 100644 frontend/src/components/ChatScreen.tsx create mode 100644 frontend/src/components/ConsentScreen.tsx create mode 100644 frontend/src/components/CreateJobModal.tsx create mode 100644 frontend/src/components/DeveloperTools.tsx create mode 100644 frontend/src/components/FeatureCard.tsx create mode 100644 frontend/src/components/Header.tsx create mode 100644 frontend/src/components/JobCard.tsx create mode 100644 frontend/src/components/JobManagement.tsx create mode 100644 frontend/src/components/JobsList.tsx create mode 100644 frontend/src/components/Layout.tsx create mode 100644 frontend/src/components/MandatoryQuestionsScreen.tsx create mode 100644 frontend/src/components/NameInputScreen.tsx create mode 100644 frontend/src/components/PricingCard.tsx create mode 100644 frontend/src/components/Sidebar.tsx create mode 100644 frontend/src/components/SystemStats.tsx create mode 100644 frontend/src/components/TechStackCard.tsx create mode 100644 frontend/src/components/ThemeToggle.tsx create mode 100644 frontend/src/components/TokenManagement.tsx create mode 100644 frontend/src/components/TokenModal.tsx create mode 100644 frontend/src/components/UserManagement.tsx create mode 100644 frontend/src/components/index.ts create mode 100644 frontend/src/types/index.ts create mode 100644 frontend/tsconfig.json create mode 100644 nginx/CLOUDFLARE_SETUP.md create mode 100644 nginx/DNS_SETUP.md create mode 100644 nginx/nginx.conf create mode 100644 nginx/ssl-setup.ps1 create mode 100644 nginx/ssl-setup.sh diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..e120213 --- /dev/null +++ b/TODO.md @@ -0,0 +1,153 @@ +# System Integration TODO - Merging ASP.NET Chatbot with Existing Backend + +## Overview +Integrate the ASP.NET chatbot service into the existing Node.js backend system to provide specialized interview capabilities while maintaining the current architecture. + +## Phase 1: Container Integration + +### 1.1 Add Chatbot Service to Docker Compose +- **Status**: ✅ Completed +- **Description**: Add the ASP.NET chatbot service to the main docker-compose.yml file +- **Files**: `docker-compose.yml` +- **Details**: Configure service with proper networking, environment variables, and dependencies + +### 1.2 Update Backend Docker Compose +- **Status**: ✅ Completed +- **Description**: Update backend docker-compose.yml to include chatbot service dependency +- **Files**: `backend/docker-compose.yml` +- **Details**: Add chatbot service and ensure proper service communication + +### 1.3 Create Chatbot Service Dockerfile +- **Status**: ✅ Completed +- **Description**: Create proper Dockerfile for the ASP.NET chatbot service +- **Files**: `tuna/tuna/Dockerfile` +- **Details**: Multi-stage build with proper .NET 9.0 runtime and configuration + +## Phase 2: Backend Service Integration + +### 2.1 Create ChatbotService +- **Status**: ✅ Completed +- **Description**: Create new service to handle communication with ASP.NET chatbot +- **Files**: `backend/src/services/ChatbotService.ts` +- **Details**: HTTP client wrapper for chatbot service with error handling and fallback + +### 2.2 Update AIService to Use Chatbot +- **Status**: ✅ Completed +- **Description**: Modify AIService to proxy requests to chatbot service instead of direct OpenRouter +- **Files**: `backend/src/services/AIService.ts` +- **Details**: Add chatbot service integration while maintaining fallback to direct OpenRouter + +### 2.3 Update AIController +- **Status**: ✅ Completed +- **Description**: Modify AIController to use new chatbot service integration +- **Files**: `backend/src/controllers/rest/AIController.ts` +- **Details**: Update chat endpoints to use chatbot service, maintain existing interview flow + +### 2.4 Add Environment Variables +- **Status**: ✅ Completed +- **Description**: Add new environment variables for chatbot service configuration +- **Files**: `env.example`, `env.production`, `env.cloudflare` +- **Details**: Add chatbot service URL, timeout, and fallback configuration + +## Phase 3: ASP.NET Service Modifications + +### 3.1 Add MySQL Database Support +- **Status**: ✅ Completed +- **Description**: Replace SQLite with MySQL database connection in ASP.NET service +- **Files**: `tuna/tuna/AISApp/AIS.cs`, `tuna/tuna/AISApp/Program.cs` +- **Details**: Add MySQL connection string and update database operations + +### 3.2 Add Interview Context Endpoints +- **Status**: ✅ Completed +- **Description**: Create endpoints for interview initialization and context management +- **Files**: `tuna/tuna/AISApp/Program.cs` +- **Details**: Add endpoints for interview start, status, and completion + +### 3.3 Implement Conversation Sync +- **Status**: ✅ Completed +- **Description**: Sync conversation data between ASP.NET service and MySQL database +- **Files**: `tuna/tuna/AISApp/AIS.cs` +- **Details**: Update conversation persistence to use MySQL instead of SQLite + +### 3.4 Add Interview-Specific Prompts +- **Status**: ✅ Completed +- **Description**: Modify system prompts based on job requirements and interview context +- **Files**: `tuna/tuna/AISApp/prompt.txt`, `tuna/tuna/AISApp/AIS.cs` +- **Details**: Dynamic prompt generation based on job details and interview stage + +## Phase 4: Database Integration + +### 4.1 Update Database Schema +- **Status**: ✅ Completed +- **Description**: Add any required database changes for chatbot integration +- **Files**: `database/` (if needed) +- **Details**: Ensure conversation tables support chatbot service requirements + +### 4.2 Add Database Migration Scripts +- **Status**: ✅ Completed +- **Description**: Create migration scripts for any database schema changes +- **Files**: `backend/` (new migration files) +- **Details**: SQL scripts for any required table modifications + +## Phase 5: Configuration and Environment + +### 5.1 Update Nginx Configuration +- **Status**: ✅ Completed (Not Required) +- **Description**: Add nginx routing for chatbot service if needed +- **Files**: `nginx/nginx.conf` +- **Details**: Add proxy rules for chatbot service endpoints + +### 5.2 Update Environment Files +- **Status**: ✅ Completed +- **Description**: Update all environment files with chatbot service configuration +- **Files**: `env.example`, `env.production`, `env.cloudflare` +- **Details**: Add chatbot service environment variables + +### 5.3 Update Docker Compose Environment +- **Status**: ✅ Completed +- **Description**: Add chatbot service environment variables to docker-compose +- **Files**: `docker-compose.yml` +- **Details**: Add environment variable mapping for chatbot service + +## Phase 6: Testing and Validation + +### 6.1 Service Communication Test +- **Status**: Pending +- **Description**: Test communication between backend and chatbot service +- **Details**: Verify HTTP requests work correctly between services + +### 6.2 Database Integration Test +- **Status**: Pending +- **Description**: Test database operations in chatbot service +- **Details**: Verify conversation sync and data persistence + +### 6.3 End-to-End Interview Flow Test +- **Status**: Pending +- **Description**: Test complete interview flow with chatbot integration +- **Details**: Verify mandatory questions → chat → completion flow works + +## Phase 7: Documentation and Cleanup + +### 7.1 Update API Documentation +- **Status**: Pending +- **Description**: Update API documentation to reflect chatbot integration +- **Files**: `backend/ADMIN_API.md`, `backend/AI_CONFIGURATION.md` +- **Details**: Document new chatbot service endpoints and configuration + +### 7.2 Update Deployment Scripts +- **Status**: Pending +- **Description**: Update deployment scripts to include chatbot service +- **Files**: `deploy-production.ps1`, `deploy-production.sh` +- **Details**: Add chatbot service to deployment process + +### 7.3 Clean Up Temporary Files +- **Status**: Pending +- **Description**: Remove any temporary files created during integration +- **Details**: Clean up test files and temporary configurations + +## Notes +- All changes maintain backward compatibility +- Fallback to direct OpenRouter if chatbot service fails +- No frontend changes required initially +- Maintain existing interview flow and database structure +- Chatbot service runs on port 5000 internally, exposed via nginx if needed diff --git a/backend/.barrels.json b/backend/.barrels.json new file mode 100644 index 0000000..a98ada8 --- /dev/null +++ b/backend/.barrels.json @@ -0,0 +1,9 @@ +{ + "directory": ["./src/controllers/rest","./src/controllers/pages"], + "exclude": [ + "**/__mock__", + "**/__mocks__", + "**/*.spec.ts" + ], + "delete": true +} diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..50b6110 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,14 @@ +node_modules +npm-debug.log +.git +.gitignore +README.md +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +.nyc_output +coverage +.DS_Store +*.log \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..96756e7 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,61 @@ +### Node template +.DS_Store +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git +node_modules +.npmrc +*.log + +# Typings +typings/ + +# Typescript +/**/*.js +/**/*.js.map +test/**/*.js +test/**/*.js.map + +# Test +/.tmp +/.nyc_output + +# IDE +.vscode +.idea + +# Project +/public +/dist + +# Environment variables +.env +.env.local +.env.development +.env.production +.env.test +.env.*.local diff --git a/backend/.swcrc b/backend/.swcrc new file mode 100644 index 0000000..f686f85 --- /dev/null +++ b/backend/.swcrc @@ -0,0 +1,21 @@ +{ + "sourceMaps": true, + "jsc": { + "parser": { + "syntax": "typescript", + "decorators": true, + "dynamicImport": true + }, + "target": "es2022", + "externalHelpers": true, + "keepClassNames": true, + "transform": { + "useDefineForClassFields": false, + "legacyDecorator": true, + "decoratorMetadata": true + } + }, + "module": { + "type": "es6" + } +} diff --git a/backend/ADMIN_API.md b/backend/ADMIN_API.md new file mode 100644 index 0000000..2919db3 --- /dev/null +++ b/backend/ADMIN_API.md @@ -0,0 +1,306 @@ +# Admin API Endpoints + +This document describes the admin-specific API endpoints for the Candivista platform. + +## Authentication + +All admin endpoints require authentication with a valid JWT token from a user with `role: 'admin'`. + +**Headers:** +``` +Authorization: Bearer +Content-Type: application/json +``` + +## Base URL +``` +http://localhost:8083/rest/admin +``` + +## Endpoints + +### System Statistics + +#### GET /statistics +Get system-wide statistics and metrics. + +**Response:** +```json +{ + "total_users": 150, + "active_users": 142, + "total_jobs": 89, + "total_interviews": 234, + "total_tokens_purchased": 1250, + "total_tokens_used": 890, + "total_revenue": 12500.00, + "generated_at": "2024-01-15T10:30:00Z" +} +``` + +### User Management + +#### GET /users +Get all users in the system. + +**Response:** +```json +[ + { + "id": "user-uuid", + "email": "user@example.com", + "first_name": "John", + "last_name": "Doe", + "role": "recruiter", + "company_name": "Tech Corp", + "is_active": true, + "last_login_at": "2024-01-15T09:00:00Z", + "created_at": "2024-01-01T00:00:00Z" + } +] +``` + +#### GET /users/:id +Get a specific user by ID. + +#### PUT /users/:id +Update user information. + +**Request Body:** +```json +{ + "first_name": "John", + "last_name": "Doe", + "email": "john@example.com", + "role": "recruiter", + "company_name": "Tech Corp", + "is_active": true +} +``` + +#### PATCH /users/:id/toggle-status +Toggle user active/inactive status. + +**Response:** +```json +{ + "success": true, + "new_status": false +} +``` + +#### PATCH /users/:id/password +Change user password. + +**Request Body:** +```json +{ + "new_password": "newpassword123" +} +``` + +#### POST /users +Create a new user. + +**Request Body:** +```json +{ + "email": "newuser@example.com", + "password": "password123", + "first_name": "Jane", + "last_name": "Smith", + "role": "recruiter", + "company_name": "Startup Inc" +} +``` + +### Job Management + +#### GET /jobs +Get all jobs in the system with user information. + +**Response:** +```json +[ + { + "id": "job-uuid", + "user_id": "user-uuid", + "title": "Senior Developer", + "description": "Job description...", + "status": "active", + "created_at": "2024-01-15T10:00:00Z", + "first_name": "John", + "last_name": "Doe", + "email": "john@example.com", + "company_name": "Tech Corp" + } +] +``` + +#### GET /jobs/:id +Get a specific job by ID. + +#### PATCH /jobs/:id/status +Update job status. + +**Request Body:** +```json +{ + "status": "paused" +} +``` + +#### PUT /jobs/:id +Update job information. + +**Request Body:** +```json +{ + "title": "Updated Job Title", + "description": "Updated description...", + "status": "active" +} +``` + +### Token Management + +#### GET /user-token-summaries +Get token usage summaries for all users. + +**Response:** +```json +[ + { + "user_id": "user-uuid", + "first_name": "John", + "last_name": "Doe", + "email": "john@example.com", + "total_purchased": 50, + "total_used": 25, + "total_available": 25, + "utilization_percentage": 50.0 + } +] +``` + +#### POST /add-tokens +Add tokens to a specific user. + +**Request Body:** +```json +{ + "user_id": "user-uuid", + "quantity": 10, + "price_per_token": 5.00, + "total_price": 50.00 +} +``` + +### Token Packages + +#### GET /token-packages +Get all token packages. + +**Response:** +```json +[ + { + "id": "package-uuid", + "name": "Professional Pack", + "description": "Ideal for regular recruiters", + "quantity": 20, + "price_per_token": 4.00, + "total_price": 80.00, + "discount_percentage": 20, + "is_popular": true, + "is_active": true + } +] +``` + +#### POST /token-packages +Create a new token package. + +**Request Body:** +```json +{ + "name": "New Package", + "description": "Package description", + "quantity": 10, + "price_per_token": 4.50, + "total_price": 45.00, + "discount_percentage": 10, + "is_popular": false, + "is_active": true +} +``` + +#### PUT /token-packages/:id +Update a token package. + +#### PATCH /token-packages/:id/toggle-status +Toggle package active/inactive status. + +#### DELETE /token-packages/:id +Delete a token package. + +### Interview Management + +#### GET /interviews +Get all interviews in the system. + +#### GET /interviews/:id +Get a specific interview by ID. + +### Payment Records + +#### GET /payments +Get all payment records. + +#### GET /payments/:id +Get a specific payment record by ID. + +## Error Responses + +All endpoints return appropriate HTTP status codes and error messages: + +- `400 Bad Request` - Invalid request data +- `401 Unauthorized` - Invalid or missing authentication +- `403 Forbidden` - Insufficient permissions (non-admin user) +- `404 Not Found` - Resource not found +- `500 Internal Server Error` - Server error + +**Error Response Format:** +```json +{ + "message": "Error description", + "status": 400 +} +``` + +## Testing + +Use the provided test script to verify admin endpoints: + +```bash +node test-admin.js +``` + +## Security Notes + +1. All admin endpoints require admin role verification +2. JWT tokens are validated on every request +3. User passwords are hashed using bcrypt +4. All database queries use parameterized statements to prevent SQL injection +5. Admin actions are logged for audit purposes + +## Database Schema + +The admin endpoints interact with the following database tables: +- `users` - User accounts and profiles +- `jobs` - Job postings +- `interview_tokens` - Token purchases and usage +- `token_packages` - Available token packages +- `interviews` - Interview sessions +- `payment_records` - Payment history +- `user_usage` - Usage tracking and limits diff --git a/backend/AI_CONFIGURATION.md b/backend/AI_CONFIGURATION.md new file mode 100644 index 0000000..94f7a35 --- /dev/null +++ b/backend/AI_CONFIGURATION.md @@ -0,0 +1,52 @@ +# AI Configuration Guide + +## Environment Variables + +Add these to your `.env` file: + +```env +# AI Configuration +# Choose between 'ollama' or 'openrouter' +AI_PROVIDER=openrouter + +# Ollama Configuration (if AI_PROVIDER=ollama) +AI_PORT=11434 +AI_MODEL=gpt-oss:20b + +# OpenRouter Configuration (if AI_PROVIDER=openrouter) +OPENROUTER_API_KEY=sk-or-your-api-key-here +OPENROUTER_MODEL=gemma +OPENROUTER_BASE_URL=openrouter.ai +OPENROUTER_REL_PATH=/api +OPENROUTER_TEMPERATURE=0.7 +``` + +## Available OpenRouter Models + +Based on your C# implementation, these models are available: + +- `gemma` - google/gemma-3-12b-it +- `dolphin` - cognitivecomputations/dolphin-mixtral-8x22b +- `dolphin_free` - cognitivecomputations/dolphin3.0-mistral-24b:free +- `gpt-4o-mini` - openai/gpt-4o-mini +- `gpt-4.1-nano` - openai/gpt-4.1-nano +- `qwen` - qwen/qwen3-30b-a3b +- `unslop` - thedrummer/unslopnemo-12b +- `euryale` - sao10k/l3.3-euryale-70b +- `wizard` - microsoft/wizardlm-2-8x22b +- `deepseek` - deepseek/deepseek-chat-v3-0324 +- `dobby` - sentientagi/dobby-mini-unhinged-plus-llama-3.1-8b + +## Testing + +1. Set `AI_PROVIDER=openrouter` in your `.env` +2. Add your OpenRouter API key +3. Test the connection: `GET http://localhost:8083/rest/ai/test-ai` +4. Start an interview to test the full flow + +## Switching Back to Ollama + +To switch back to Ollama: +1. Set `AI_PROVIDER=ollama` in your `.env` +2. Make sure Ollama is running on the specified port +3. Test the connection: `GET http://localhost:8083/rest/ai/test-ai` diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..8b2de9b --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,30 @@ +# Use Node.js 18 Alpine for smaller image size +FROM node:18-alpine + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci --only=production + +# Copy source code +COPY . . + +# Build the application +RUN npm run build + +# Expose port +EXPOSE 8083 + +# Install curl for health checks +RUN apk add --no-cache curl + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8083/rest/ai/test-ai || exit 1 + +# Start the application +CMD ["npm", "run", "start:prod"] \ No newline at end of file diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..18889be --- /dev/null +++ b/backend/README.md @@ -0,0 +1,67 @@ +

+ Ts.ED logo +

+ +
+

Ts.ED - backend

+
+
+ Website +   •   + Getting started +   •   + Slack +   •   + Twitter +
+
+
+ +> An awesome project based on Ts.ED framework + +## Getting started + +> **Important!** Ts.ED requires Node >= 20.x or Bun.js and TypeScript >= 5. + +```batch +# install dependencies +$ npm install + +# serve +$ npm run start + +# build for production +$ npm run build +$ npm run start:prod +``` + +## Docker + +``` +# build docker image +docker compose build + +# start docker image +docker compose up +``` + +## Barrels + +This project uses [barrels](https://www.npmjs.com/package/@tsed/barrels) to generate index files to import the controllers. + +Edit `.barrels.json` to customize it: + +```json +{ + "directory": [ + "./src/controllers/rest", + "./src/controllers/pages" + ], + "exclude": [ + "**/__mock__", + "**/__mocks__", + "**/*.spec.ts" + ], + "delete": true +} +``` diff --git a/backend/add-icon-column.sql b/backend/add-icon-column.sql new file mode 100644 index 0000000..5cff3cc --- /dev/null +++ b/backend/add-icon-column.sql @@ -0,0 +1,3 @@ +-- Add icon column to jobs table +USE candidb_main; +ALTER TABLE jobs ADD COLUMN icon VARCHAR(50) DEFAULT 'briefcase' AFTER application_deadline; diff --git a/backend/add-interview-style-column.sql b/backend/add-interview-style-column.sql new file mode 100644 index 0000000..03abff6 --- /dev/null +++ b/backend/add-interview-style-column.sql @@ -0,0 +1,7 @@ +USE candidb_main; + +-- Add interview_style column to jobs table +ALTER TABLE jobs ADD COLUMN interview_style ENUM('personal', 'balanced', 'technical') DEFAULT 'balanced' AFTER interview_questions; + +-- Update existing records to have 'balanced' as default +UPDATE jobs SET interview_style = 'balanced' WHERE interview_style IS NULL; diff --git a/backend/add-tokens-used-column.sql b/backend/add-tokens-used-column.sql new file mode 100644 index 0000000..1a4ff45 --- /dev/null +++ b/backend/add-tokens-used-column.sql @@ -0,0 +1,7 @@ +USE candidb_main; + +-- Add tokens_used column to job_links table +ALTER TABLE job_links ADD COLUMN tokens_used INT DEFAULT 0 AFTER tokens_available; + +-- Update existing records to have 0 tokens_used +UPDATE job_links SET tokens_used = 0 WHERE tokens_used IS NULL; diff --git a/backend/create-job-links-table.sql b/backend/create-job-links-table.sql new file mode 100644 index 0000000..b291d43 --- /dev/null +++ b/backend/create-job-links-table.sql @@ -0,0 +1,13 @@ +USE candidb_main; + +CREATE TABLE IF NOT EXISTS job_links ( + id VARCHAR(36) PRIMARY KEY, + job_id VARCHAR(36) NOT NULL, + url_slug VARCHAR(50) NOT NULL UNIQUE, + tokens_available INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (job_id) REFERENCES jobs(id) ON DELETE CASCADE, + INDEX idx_job_id (job_id), + INDEX idx_url_slug (url_slug) +); diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..dbf7ad9 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,46 @@ +version: '3.5' +services: + server: + build: + context: . + dockerfile: ./Dockerfile + args: + - http_proxy + - https_proxy + - no_proxy + image: backend/server:latest + ports: + - "8081:8081" + environment: + - CHATBOT_SERVICE_URL=http://chatbot:80 + - CHATBOT_SERVICE_TIMEOUT=30000 + - CHATBOT_FALLBACK_ENABLED=true + depends_on: + - chatbot + networks: + - candidat-network + + chatbot: + build: + context: ../../tuna/tuna + dockerfile: Dockerfile + image: candidat/chatbot:latest + container_name: backend-chatbot + environment: + - ASPNETCORE_ENVIRONMENT=Production + - OPENROUTER_API_KEY=${OPENROUTER_API_KEY} + - CHATBOT_DB_HOST=database + - CHATBOT_DB_NAME=${MYSQL_DATABASE} + - CHATBOT_DB_USER=${MYSQL_USER} + - CHATBOT_DB_PASSWORD=${MYSQL_PASSWORD} + - CHATBOT_DB_PORT=3306 + ports: + - "5000:80" + networks: + - candidat-network + restart: unless-stopped + +networks: + candidat-network: + external: true + diff --git a/backend/nodemon.json b/backend/nodemon.json new file mode 100644 index 0000000..a153ba7 --- /dev/null +++ b/backend/nodemon.json @@ -0,0 +1,9 @@ +{ + "extensions": ["ts"], + "watch": ["src"], + "ignore": ["**/*.spec.ts"], + "delay": 100, + "execMap": { + "ts": "node --enable-source-maps --import @swc-node/register/esm-register" + } +} diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..0b8479e --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,5573 @@ +{ + "name": "backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "backend", + "version": "1.0.0", + "dependencies": { + "@swc-node/register": "^1.11.1", + "@swc/cli": "^0.7.8", + "@swc/core": "^1.13.5", + "@swc/helpers": "^0.5.17", + "@tsed/ajv": "^8.16.2", + "@tsed/barrels": "^6.6.3", + "@tsed/core": "^8.16.2", + "@tsed/di": "^8.16.2", + "@tsed/engines": "^8.16.2", + "@tsed/exceptions": "^8.16.2", + "@tsed/json-mapper": "^8.16.2", + "@tsed/logger": "^8.0.4", + "@tsed/openspec": "^8.16.2", + "@tsed/platform-cache": "^8.16.2", + "@tsed/platform-exceptions": "^8.16.2", + "@tsed/platform-express": "^8.16.2", + "@tsed/platform-http": "^8.16.2", + "@tsed/platform-log-request": "^8.16.2", + "@tsed/platform-middlewares": "^8.16.2", + "@tsed/platform-multer": "^8.16.2", + "@tsed/platform-params": "^8.16.2", + "@tsed/platform-response-filter": "^8.16.2", + "@tsed/platform-views": "^8.16.2", + "@tsed/scalar": "^8.16.2", + "@tsed/schema": "^8.16.2", + "@tsed/socketio": "^8.16.2", + "@tsed/swagger": "^8.16.2", + "@types/bcryptjs": "^3.0.0", + "@types/jsonwebtoken": "^9.0.10", + "ajv": "^8.17.1", + "axios": "^1.6.0", + "bcryptjs": "^3.0.2", + "body-parser": "^2.2.0", + "compression": "^1.8.1", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "cross-env": "^10.0.0", + "dotenv": "^17.2.2", + "dotenv-expand": "^12.0.3", + "dotenv-flow": "^4.1.0", + "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", + "method-override": "^3.0.0", + "mysql2": "^3.14.5", + "socket.io": "^4.8.1", + "typescript": "^5.9.2" + }, + "devDependencies": { + "@types/compression": "^1.8.1", + "@types/cookie-parser": "^1.4.9", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/method-override": "^3.0.0", + "@types/multer": "^2.0.0", + "@types/node": "^24.3.1", + "nodemon": "^3.1.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@borewit/text-codec": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.1.1.tgz", + "integrity": "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@emnapi/core": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", + "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==" + }, + "node_modules/@napi-rs/nice": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", + "integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.1.1", + "@napi-rs/nice-android-arm64": "1.1.1", + "@napi-rs/nice-darwin-arm64": "1.1.1", + "@napi-rs/nice-darwin-x64": "1.1.1", + "@napi-rs/nice-freebsd-x64": "1.1.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1", + "@napi-rs/nice-linux-arm64-gnu": "1.1.1", + "@napi-rs/nice-linux-arm64-musl": "1.1.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.1.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.1.1", + "@napi-rs/nice-linux-s390x-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-musl": "1.1.1", + "@napi-rs/nice-openharmony-arm64": "1.1.1", + "@napi-rs/nice-win32-arm64-msvc": "1.1.1", + "@napi-rs/nice-win32-ia32-msvc": "1.1.1", + "@napi-rs/nice-win32-x64-msvc": "1.1.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz", + "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz", + "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz", + "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz", + "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz", + "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz", + "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz", + "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz", + "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz", + "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz", + "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz", + "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz", + "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz", + "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-openharmony-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz", + "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz", + "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz", + "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz", + "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.3.tgz", + "integrity": "sha512-rZxtMsLwjdXkMUGC3WwsPwLNVqVqnTJT6MNIB6e+5fhMcSCPP0AOsNWuMQ5mdCq6HNjs/ZeWAEchpqeprqBD2Q==", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.5", + "@emnapi/runtime": "^1.4.5", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@oxc-resolver/binding-android-arm-eabi": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.7.1.tgz", + "integrity": "sha512-K0gF1mD6CYMAuX0dMWe6XW1Js00xCOBh/+ZAAJReQMa4+jmAk3bIeitsc8VnDthDbzOOKp3riizP3o/tBvNpgw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-android-arm64": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.7.1.tgz", + "integrity": "sha512-O1XEX/KxKX7baPgYHahP+3vT+9f4gasPA0px4DYrjy1mN9wWQqJPLLo/PO3cBw3qI3qRaaiAGT3eJSs8rKu8mA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-arm64": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.7.1.tgz", + "integrity": "sha512-OSCJlXUTvGoal5dTMkdacmXL2R3YQ+97R7NMSdjkUVnh3TxvGBhoF9OebqY3PR7w2gQaY5LX+Ju+dYeHGBCGgw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-x64": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.7.1.tgz", + "integrity": "sha512-d0jKwK4r4Yw19xSijyt7wHZT77xh3v4GnJSbvEiPavLms27zqc//BqYJUSp9XgOTOkyFQ+oHno47JNiLTnsSnQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-freebsd-x64": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.7.1.tgz", + "integrity": "sha512-oNch5OpAnxFjukDZ5GJkuEDEPPYDirm10q2cJcbK0SETVM0rY+ou1cLqJAJC9R/dULbqGKC9fv2kuyuw9M6Fig==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.7.1.tgz", + "integrity": "sha512-ldUPUfV/0L56fTSfzUo86Bmgov8SAfau8Q4Y3WiAiQi6WHLA239abTZZViLZuXvrC+4RQF/kD0ySqKfBjW/X9g==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.7.1.tgz", + "integrity": "sha512-M+ORXlPV0dXCHleqOYLjKHwxn9kDmcJqnJ7zGZ07vggaxOCnpM6zqyGS92YTTyeYre2AqO3Xrx1D4rnUeozI8g==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.7.1.tgz", + "integrity": "sha512-ukHZp9Vm07AlxqdOLFf8Bj4inzpt+ISbbODvwwHxX32GfcMLWYYJGAYWc13IGhWoElvWnI7D1M9ifDGyTNRGzg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-musl": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.7.1.tgz", + "integrity": "sha512-atkZ1OIt6t90kjQz1iqq6cN3OpfPG5zUJlO64Vd1ieYeqHRkOFeRgnWEobTePUHi34NlYr7mNZqIaAg7gjPUFg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.7.1.tgz", + "integrity": "sha512-HGgV4z3JwVF4Qvg2a1GhDnqn8mKLihy5Gp4rMfqNIAlERPSyIxo8oPQIL1XQKLYyyrkEEO99uwM+4cQGwhtbpQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.7.1.tgz", + "integrity": "sha512-+vCO7iOR1s6VGefV02R2a702IASNWhSNm/MrR8RcWjKChmU0G+d1iC0oToUrGC4ovAEfstx2/O8EkROnfcLgrA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.7.1.tgz", + "integrity": "sha512-3folNmS5gYNFy/9HYzLcdeThqAGvDJU0gQKrhHn7RPWQa58yZ0ZPpBMk6KRSSO61+wkchkL+0sdcLsoe5wZW8g==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.7.1.tgz", + "integrity": "sha512-Ceo4z6g8vqPUKADROFL0b7MoyXlUdOBYCxTDu/fhd/5I3Ydk2S6bxkjJdzpBdlu+h2Z+eS9lTHFvkwkaORMPzw==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-gnu": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.7.1.tgz", + "integrity": "sha512-QyFW5e43imQLxiBpCImhOiP4hY9coWGjroEm8elDqGNNaA7vXooaMQS2N3avMQawSaKhsb/3RemxaZ852XG38Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-musl": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.7.1.tgz", + "integrity": "sha512-JhuCqCqktqQyQVc37V+eDiP3buCIuyCLpb92tUEyAP8nY3dy2b/ojMrH1ZNnJUlfY/67AqoZPL6nQGAB2WA3Sg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.7.1.tgz", + "integrity": "sha512-sMXm5Z2rfBwkCUespZBJCPhCVbgh/fpYQ23BQs0PmnvWoXrGQHWvnvg1p/GYmleN+nwe8strBjfutirZFiC5lA==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.0.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.7.1.tgz", + "integrity": "sha512-C/Sam1RJi/h/F618IB/H3pCOhTf+2ArdTqrqQolN8ARV35iWTSezgy6qPjQGj7aWn/9M5vgtCInfS2SwnkBJ4w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxc-resolver/binding-win32-ia32-msvc": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-11.7.1.tgz", + "integrity": "sha512-iNRgJxOkfmxeq9DiF9S4jtw3vq5kkAm6dsP4RPxoAO/WsShPPHOSlTpOqyB8bSj5Bt9DBLRoI43XcNfDKgM+jA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxc-resolver/binding-win32-x64-msvc": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.7.1.tgz", + "integrity": "sha512-MXS81efp8pu2MkjEPu+nDhgoyHwdWUygXYSzIh3gV2A8/qF0PVEzH+EpmKR7Pl8dEZIaG1YXA+CO6bmNZT8oSw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@scalar/openapi-types": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.1.5.tgz", + "integrity": "sha512-6geH9ehvQ/sG/xUyy3e0lyOw3BaY5s6nn22wHjEJhcobdmWyFER0O6m7AU0ZN4QTjle/gYvFJOjj552l/rsNSw==", + "engines": { + "node": ">=18" + } + }, + "node_modules/@scalar/types": { + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/@scalar/types/-/types-0.0.23.tgz", + "integrity": "sha512-dOvQig4hyeVw1kXIo9MQAnM9tUt9vCOZs3zOe6oSqOUG8xY7+WXioirlRCsc+wcQegMbuNYOlNBXCDugOP1YJA==", + "dependencies": { + "@scalar/openapi-types": "0.1.5", + "@unhead/schema": "^1.11.11" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true + }, + "node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, + "node_modules/@swc-node/core": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@swc-node/core/-/core-1.14.1.tgz", + "integrity": "sha512-jrt5GUaZUU6cmMS+WTJEvGvaB6j1YNKPHPzC2PUi2BjaFbtxURHj6641Az6xN7b665hNniAIdvjxWcRml5yCnw==", + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@swc/core": ">= 1.13.3", + "@swc/types": ">= 0.1" + } + }, + "node_modules/@swc-node/register": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@swc-node/register/-/register-1.11.1.tgz", + "integrity": "sha512-VQ0hJ5jX31TVv/fhZx4xJRzd8pwn6VvzYd2tGOHHr2TfXGCBixZoqdPDXTiEoJLCTS2MmvBf6zyQZZ0M8aGQCQ==", + "dependencies": { + "@swc-node/core": "^1.14.1", + "@swc-node/sourcemap-support": "^0.6.1", + "colorette": "^2.0.20", + "debug": "^4.4.1", + "oxc-resolver": "^11.6.1", + "pirates": "^4.0.7", + "tslib": "^2.8.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@swc/core": ">= 1.4.13", + "typescript": ">= 4.3" + } + }, + "node_modules/@swc-node/sourcemap-support": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@swc-node/sourcemap-support/-/sourcemap-support-0.6.1.tgz", + "integrity": "sha512-ovltDVH5QpdHXZkW138vG4+dgcNsxfwxHVoV6BtmTbz2KKl1A8ZSlbdtxzzfNjCjbpayda8Us9eMtcHobm38dA==", + "dependencies": { + "source-map-support": "^0.5.21", + "tslib": "^2.8.1" + } + }, + "node_modules/@swc/cli": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/@swc/cli/-/cli-0.7.8.tgz", + "integrity": "sha512-27Ov4rm0s2C6LLX+NDXfDVB69LGs8K94sXtFhgeUyQ4DBywZuCgTBu2loCNHRr8JhT9DeQvJM5j9FAu/THbo4w==", + "dependencies": { + "@swc/counter": "^0.1.3", + "@xhmikosr/bin-wrapper": "^13.0.5", + "commander": "^8.3.0", + "minimatch": "^9.0.3", + "piscina": "^4.3.1", + "semver": "^7.3.8", + "slash": "3.0.0", + "source-map": "^0.7.3", + "tinyglobby": "^0.2.13" + }, + "bin": { + "spack": "bin/spack.js", + "swc": "bin/swc.js", + "swcx": "bin/swcx.js" + }, + "engines": { + "node": ">= 16.14.0" + }, + "peerDependencies": { + "@swc/core": "^1.2.66", + "chokidar": "^4.0.1" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@swc/core": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz", + "integrity": "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==", + "hasInstallScript": true, + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.24" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.13.5", + "@swc/core-darwin-x64": "1.13.5", + "@swc/core-linux-arm-gnueabihf": "1.13.5", + "@swc/core-linux-arm64-gnu": "1.13.5", + "@swc/core-linux-arm64-musl": "1.13.5", + "@swc/core-linux-x64-gnu": "1.13.5", + "@swc/core-linux-x64-musl": "1.13.5", + "@swc/core-win32-arm64-msvc": "1.13.5", + "@swc/core-win32-ia32-msvc": "1.13.5", + "@swc/core-win32-x64-msvc": "1.13.5" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.5.tgz", + "integrity": "sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.5.tgz", + "integrity": "sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.5.tgz", + "integrity": "sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.5.tgz", + "integrity": "sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.5.tgz", + "integrity": "sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.5.tgz", + "integrity": "sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.5.tgz", + "integrity": "sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.5.tgz", + "integrity": "sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.5.tgz", + "integrity": "sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.5.tgz", + "integrity": "sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" + }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" + }, + "node_modules/@tsed/ajv": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/ajv/-/ajv-8.16.2.tgz", + "integrity": "sha512-i37X0m+zusLwtsqsEyow3RMGPPLhg6/R7yu/knvdOZiM7P1fMp9WLhIcFZeLe2UQ+JmFol/Nr/I/rx7a784m4A==", + "dependencies": { + "ajv-errors": "3.0.0", + "ajv-formats": "2.1.1", + "tslib": "2.7.0" + }, + "peerDependencies": { + "@tsed/core": ">=8.0.0", + "@tsed/di": ">=8.0.0", + "@tsed/exceptions": ">=8.0.0", + "@tsed/schema": ">=8.0.0", + "ajv": ">=8.9.0", + "ajv-errors": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@tsed/core": { + "optional": false + }, + "@tsed/di": { + "optional": false + }, + "@tsed/exceptions": { + "optional": false + }, + "@tsed/schema": { + "optional": false + } + } + }, + "node_modules/@tsed/ajv/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tsed/barrels": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@tsed/barrels/-/barrels-6.6.3.tgz", + "integrity": "sha512-TSZHst1W5IiQasUmNiMS1dN69CbBV9L7T7baqmMGzYIhP1gZV2i5JUZi+xai0tH4tMzT1CYlvwNHq1QDXgQRaw==", + "dependencies": { + "globby": "14.0.2" + }, + "bin": { + "barrels": "bin/barrels.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@tsed/core": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/core/-/core-8.16.2.tgz", + "integrity": "sha512-xgiAfQNT7ExbAZrhAQZ177brtQuGT6A6fnIkdFVeA5vD7xJ6mGM/IKYKpkDYB6PgxjlmytvwVKfMabfIGBekkA==", + "dependencies": { + "reflect-metadata": "^0.2.2", + "tslib": "2.7.0" + } + }, + "node_modules/@tsed/core/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tsed/di": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/di/-/di-8.16.2.tgz", + "integrity": "sha512-5OxuHaSTSnurdQ6f/iQX7cE3sGa9p2wh18FIgJVDgRDPIx2xRCf248pLMnhfvigAcr7BaDOh1SNU231DR0X1Mw==", + "dependencies": { + "tslib": "2.7.0", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "@tsed/core": ">=8.0.0", + "@tsed/hooks": ">=8.0.0", + "@tsed/logger": ">=8.0.0" + }, + "peerDependenciesMeta": { + "@tsed/core": { + "optional": false + }, + "@tsed/hooks": { + "optional": false + }, + "@tsed/logger": { + "optional": false + } + } + }, + "node_modules/@tsed/di/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tsed/engines": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/engines/-/engines-8.16.2.tgz", + "integrity": "sha512-4GBd6uSEtm8H6bRQHCSAE1h0xtOedQDjTrF+Hwm1q6IaY8EEKL36sWKZgaDTb4olGLzCY0k3UJJlJFGBNpQ3Fg==", + "dependencies": { + "tslib": "2.7.0" + } + }, + "node_modules/@tsed/engines/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tsed/exceptions": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/exceptions/-/exceptions-8.16.2.tgz", + "integrity": "sha512-jBQMvWlZHCL7ZpG2bOYB2pBMF/2pl/k5fTHlWzUw4DNnKzj3acnMNYHVBIRBVEGuF19Sdn7lx4HRZUO9KgpRvA==", + "dependencies": { + "change-case": "^5.4.4", + "statuses": "^2.0.1", + "tslib": "2.7.0" + }, + "peerDependencies": { + "@tsed/core": ">=8.0.0" + } + }, + "node_modules/@tsed/exceptions/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tsed/hooks": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/hooks/-/hooks-8.16.2.tgz", + "integrity": "sha512-SzLpShjEa+GN2De8ewtvnbkiePt8J44a7WIOCK6FtuANq41kyRfsu9hxYTZKFtzBqwGWwtzP4Zkxz2HPTHczHQ==", + "dependencies": { + "reflect-metadata": "^0.2.2", + "tslib": "2.7.0" + } + }, + "node_modules/@tsed/hooks/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tsed/json-mapper": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/json-mapper/-/json-mapper-8.16.2.tgz", + "integrity": "sha512-eE9G8UI3ho05vgTe3i/WxqyxaBuUnb4YBWzkhxIJIYzwxRgUhKcODSCfmQhp62nwlVnUid3niaBriAZFnnzCtQ==", + "dependencies": { + "tslib": "2.7.0" + }, + "peerDependencies": { + "@tsed/core": ">=8.0.0", + "@tsed/schema": ">=8.0.0" + }, + "peerDependenciesMeta": { + "@tsed/core": { + "optional": false + }, + "@tsed/schema": { + "optional": false + } + } + }, + "node_modules/@tsed/json-mapper/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tsed/logger": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@tsed/logger/-/logger-8.0.4.tgz", + "integrity": "sha512-Ml7oqulFu2v2gZKly39zhpXVsi8G7ybu1NObn+oBI9CYDXqEyxyLbIzp3DB2kMo641ywrwIvdtq7j9d8xTtl9Q==", + "dependencies": { + "colors": "1.4.0", + "date-format": "^4.0.14", + "semver": "^7.7.2", + "tslib": "2.8.1" + } + }, + "node_modules/@tsed/logger-std": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@tsed/logger-std/-/logger-std-8.0.4.tgz", + "integrity": "sha512-rh+C+a92SFTqWdQVCdN80POXWAgFNMShCBaDV9YO8x9OSyRmR6EGrw8QRTFdv5nggHgFkCwXbRiZxS1QTJxfog==", + "dependencies": { + "colors": "1.4.0", + "date-format": "^4.0.14", + "semver": "^7.7.2", + "tslib": "2.8.1" + }, + "peerDependencies": { + "@tsed/logger": ">=8.0.0" + }, + "peerDependenciesMeta": { + "@tsed/logger": { + "optional": false + } + } + }, + "node_modules/@tsed/normalize-path": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/normalize-path/-/normalize-path-8.16.2.tgz", + "integrity": "sha512-hMlfyECX7/Fo0UQMsM30e8PSF/Ug7v4xn4na6et9NGg6Hble6ur7fiGVL+AfKgWOmlZZAVFwC0m9KUJt+KemHA==", + "dependencies": { + "normalize-path": "3.0.0", + "tslib": "2.7.0" + } + }, + "node_modules/@tsed/normalize-path/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tsed/openapi-utils": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/openapi-utils/-/openapi-utils-8.16.2.tgz", + "integrity": "sha512-gebytfJeZI8isREFq30d0lyNd/QIkCstJxJtWtHNhfLr/q6wfcI2N+Lzb6MgCObbYbBYRcqmy3ZW69T5f8y2hw==", + "dependencies": { + "@tsed/openspec": "8.16.2", + "micromatch": "4.0.8", + "tslib": "2.7.0" + }, + "peerDependencies": { + "@tsed/platform-http": ">=8.0.0" + }, + "peerDependenciesMeta": { + "@tsed/platform-http": { + "optional": false + } + } + }, + "node_modules/@tsed/openapi-utils/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tsed/openspec": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/openspec/-/openspec-8.16.2.tgz", + "integrity": "sha512-Q3BrTxsmvOX1MioTv1AIsGaoOzWWgsA827TOndNG61tYhuAP/BETmbYDNNIrlnKzDYvdMiyHl14wIEUYm15hrw==" + }, + "node_modules/@tsed/platform-accept-mimes": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/platform-accept-mimes/-/platform-accept-mimes-8.16.2.tgz", + "integrity": "sha512-fNQQhD12hWBZ1QKaCt8WJiPOT/1Xh9qedWSCggt799lRRFE7a8aMSp15gPxQtX2J1weWPd3gborKMEHqGzeuTQ==", + "dependencies": { + "tslib": "2.7.0" + }, + "peerDependencies": { + "@tsed/core": ">=8.0.0", + "@tsed/di": ">=8.0.0", + "@tsed/schema": ">=8.0.0" + }, + "peerDependenciesMeta": { + "@tsed/core": { + "optional": true + }, + "@tsed/di": { + "optional": true + }, + "@tsed/schema": { + "optional": true + } + } + }, + "node_modules/@tsed/platform-accept-mimes/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tsed/platform-cache": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/platform-cache/-/platform-cache-8.16.2.tgz", + "integrity": "sha512-o2yGu/hVzr8Gmldrx4jhijTlXnpB93JPXOVi/zXUe80gEX6PjT1RN99fSQdYad7ji/A3/tVx5m/EEIHKNYGgyA==", + "dependencies": { + "cache-manager": "^5.7.6", + "micromatch": "4.0.8", + "tslib": "2.7.0" + }, + "peerDependencies": { + "@tsed/core": ">=8.0.0", + "@tsed/di": ">=8.0.0", + "@tsed/json-mapper": ">=8.0.0", + "@tsed/schema": ">=8.0.0" + }, + "peerDependenciesMeta": { + "@tsed/core": { + "optional": false + }, + "@tsed/di": { + "optional": false + }, + "@tsed/json-mapper": { + "optional": false + }, + "@tsed/schema": { + "optional": false + } + } + }, + "node_modules/@tsed/platform-cache/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tsed/platform-exceptions": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/platform-exceptions/-/platform-exceptions-8.16.2.tgz", + "integrity": "sha512-/qgqqaZv3SfNbyWIKSnusSITVR+hZ+VfLWN1lnlO0IpgvL+wkPYbB5cHxGd+a5wnDDhZhMBSiFR4wLbSX870Uw==", + "dependencies": { + "tslib": "2.7.0" + }, + "peerDependencies": { + "@tsed/core": ">=8.0.0", + "@tsed/di": ">=8.0.0", + "@tsed/exceptions": ">=8.0.0", + "@tsed/schema": ">=8.0.0" + }, + "peerDependenciesMeta": { + "@tsed/core": { + "optional": false + }, + "@tsed/di": { + "optional": false + }, + "@tsed/exceptions": { + "optional": false + }, + "@tsed/schema": { + "optional": false + } + } + }, + "node_modules/@tsed/platform-exceptions/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tsed/platform-express": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/platform-express/-/platform-express-8.16.2.tgz", + "integrity": "sha512-lAtYBi60zeMlMgXR0WA0adqhW5LN1RjfK9XCd10jINHAQ6bem2PmukiJuTmBs2vVPEgCRCxNEasS63xuu76dFA==", + "dependencies": { + "multer": "^2.0.0", + "tslib": "2.7.0" + }, + "peerDependencies": { + "@tsed/core": ">=8.0.0", + "@tsed/di": ">=8.0.0", + "@tsed/json-mapper": ">=8.0.0", + "@tsed/logger": ">=8.0.0", + "@tsed/openspec": ">=8.0.0", + "@tsed/platform-http": ">=8.0.0", + "@tsed/platform-views": ">=8.0.0", + "@tsed/schema": ">=8.0.0", + "body-parser": ">=1.19.0", + "express": ">=4.17.1", + "multer": ">=2.0.0" + }, + "peerDependenciesMeta": { + "@tsed/core": { + "optional": false + }, + "@tsed/di": { + "optional": false + }, + "@tsed/json-mapper": { + "optional": false + }, + "@tsed/logger": { + "optional": false + }, + "@tsed/platform-http": { + "optional": false + }, + "@tsed/platform-views": { + "optional": true + }, + "@tsed/schema": { + "optional": false + }, + "express": { + "optional": false + }, + "multer": { + "optional": false + } + } + }, + "node_modules/@tsed/platform-express/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tsed/platform-http": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/platform-http/-/platform-http-8.16.2.tgz", + "integrity": "sha512-Ki3HOkzFmn6oAvzLu3dsuR/LsELiFjoWwVdOK1WwDzhL2Zufv3UXDzDaeAZR5OYJj1Px8X+SQzy6J9msF7fw3A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Romakita" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/tsed" + } + ], + "dependencies": { + "@tsed/core": "8.16.2", + "@tsed/di": "8.16.2", + "@tsed/exceptions": "8.16.2", + "@tsed/hooks": "8.16.2", + "@tsed/json-mapper": "8.16.2", + "@tsed/logger": "^8.0.0", + "@tsed/logger-std": "^8.0.0", + "@tsed/platform-accept-mimes": "8.16.2", + "@tsed/platform-exceptions": "8.16.2", + "@tsed/platform-middlewares": "8.16.2", + "@tsed/platform-multer": "8.16.2", + "@tsed/platform-params": "8.16.2", + "@tsed/platform-response-filter": "8.16.2", + "@tsed/platform-router": "8.16.2", + "@tsed/platform-views": "8.16.2", + "@tsed/schema": "8.16.2", + "@types/json-schema": "7.0.15", + "accepts": "^1.3.8", + "tslib": "2.7.0", + "uuid": "10.0.0" + }, + "peerDependencies": { + "@tsed/logger": ">=8.0.0" + }, + "peerDependenciesMeta": { + "@tsed/logger": { + "optional": false + } + } + }, + "node_modules/@tsed/platform-http/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tsed/platform-log-request": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/platform-log-request/-/platform-log-request-8.16.2.tgz", + "integrity": "sha512-HAjKqa8l/Ublk4gD7s/N7+OOIKyLfwG9mWSEzns1twC75zmCXgvAu6gPB2iNj1W4FWQzpnAE2Xr9cY+kXUTGpQ==", + "dependencies": { + "tslib": "2.7.0" + }, + "peerDependencies": { + "@tsed/di": ">=8.0.0", + "@tsed/platform-middlewares": ">=8.0.0", + "@tsed/platform-params": ">=8.0.0" + }, + "peerDependenciesMeta": { + "@tsed/di": { + "optional": false + }, + "@tsed/platform-middlewares": { + "optional": false + }, + "@tsed/platform-params": { + "optional": false + } + } + }, + "node_modules/@tsed/platform-log-request/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tsed/platform-middlewares": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/platform-middlewares/-/platform-middlewares-8.16.2.tgz", + "integrity": "sha512-uiNIC+6K5jI4av5soPJnVd87ODJmbwcm6FlW4SsPB69YPfFwfQcCoh+dUN8vp2ZFth120iAACfRXrxF6zTGgtg==", + "dependencies": { + "tslib": "2.7.0" + }, + "peerDependencies": { + "@tsed/core": ">=8.0.0", + "@tsed/di": ">=8.0.0", + "@tsed/schema": ">=8.0.0" + }, + "peerDependenciesMeta": { + "@tsed/core": { + "optional": true + }, + "@tsed/di": { + "optional": true + }, + "@tsed/schema": { + "optional": true + } + } + }, + "node_modules/@tsed/platform-middlewares/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tsed/platform-multer": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/platform-multer/-/platform-multer-8.16.2.tgz", + "integrity": "sha512-e9FE9NnnmJz4p9MQDyhyF3U21XXZpy7Te7Yn/PZLE7Hh5MiLNK5grUuM33KUiKRB8j7IDvASfUmVhj6s2IwHng==", + "dependencies": { + "multer": "^2.0.0", + "tslib": "2.7.0" + }, + "peerDependencies": { + "@tsed/core": ">=8.0.0", + "@tsed/di": ">=8.0.0", + "@tsed/json-mapper": ">=8.0.0", + "@tsed/logger": ">=8.0.0", + "@tsed/openspec": ">=8.0.0", + "@tsed/platform-middlewares": ">=8.0.0", + "@tsed/schema": ">=8.0.0", + "@types/multer": ">=2.0.0" + }, + "peerDependenciesMeta": { + "@tsed/core": { + "optional": false + }, + "@tsed/di": { + "optional": false + }, + "@tsed/json-mapper": { + "optional": false + }, + "@tsed/logger": { + "optional": false + }, + "@tsed/openspec": { + "optional": false + }, + "@tsed/platform-middlewares": { + "optional": false + }, + "@tsed/schema": { + "optional": false + } + } + }, + "node_modules/@tsed/platform-multer/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tsed/platform-params": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/platform-params/-/platform-params-8.16.2.tgz", + "integrity": "sha512-gGqSeBT3gE+NWuyrPU42UAhPBJRFPHgJbEX/2u4Nq2zg/4EytuQmVYpa+h9ogqZUyUPJZpzMxNPn7KQGs3kSJg==", + "dependencies": { + "tslib": "2.7.0" + }, + "peerDependencies": { + "@tsed/core": ">=8.0.0", + "@tsed/di": ">=8.0.0", + "@tsed/exceptions": ">=8.0.0", + "@tsed/json-mapper": ">=8.0.0", + "@tsed/schema": ">=8.0.0" + }, + "peerDependenciesMeta": { + "@tsed/core": { + "optional": false + }, + "@tsed/di": { + "optional": false + }, + "@tsed/exceptions": { + "optional": false + }, + "@tsed/json-mapper": { + "optional": false + }, + "@tsed/schema": { + "optional": false + } + } + }, + "node_modules/@tsed/platform-params/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tsed/platform-response-filter": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/platform-response-filter/-/platform-response-filter-8.16.2.tgz", + "integrity": "sha512-uzKmYXsj1ulNV29yMiC2Q8fG0WCXW2NWy2K18Qf/jMY71D1sQ8c9y+dzfZqUswGZCDb1BivN5+vCOaNDnFfYxA==", + "dependencies": { + "tslib": "2.7.0" + }, + "peerDependencies": { + "@tsed/core": ">=8.0.0", + "@tsed/di": ">=8.0.0", + "@tsed/exceptions": ">=8.0.0", + "@tsed/json-mapper": ">=8.0.0", + "@tsed/schema": ">=8.0.0" + }, + "peerDependenciesMeta": { + "@tsed/core": { + "optional": false + }, + "@tsed/di": { + "optional": false + }, + "@tsed/exceptions": { + "optional": false + }, + "@tsed/json-mapper": { + "optional": false + }, + "@tsed/schema": { + "optional": false + } + } + }, + "node_modules/@tsed/platform-response-filter/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tsed/platform-router": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/platform-router/-/platform-router-8.16.2.tgz", + "integrity": "sha512-UJkmjG80ew78VEKx+i1VOmIHj349T4TZfcD/TXwWRskkyZALQIAZD6H3rictctCRCd3sTT0Xq57hH9qXIxkmpA==", + "dependencies": { + "tslib": "2.7.0" + }, + "peerDependencies": { + "@tsed/core": ">=8.0.0", + "@tsed/di": ">=8.0.0", + "@tsed/exceptions": ">=8.0.0", + "@tsed/json-mapper": ">=8.0.0", + "@tsed/platform-params": ">=8.0.0", + "@tsed/schema": ">=8.0.0" + }, + "peerDependenciesMeta": { + "@tsed/core": { + "optional": false + }, + "@tsed/di": { + "optional": false + }, + "@tsed/exceptions": { + "optional": false + }, + "@tsed/json-mapper": { + "optional": false + }, + "@tsed/platform-params": { + "optional": false + }, + "@tsed/schema": { + "optional": false + } + } + }, + "node_modules/@tsed/platform-router/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tsed/platform-views": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/platform-views/-/platform-views-8.16.2.tgz", + "integrity": "sha512-p0SG7pdbmeLMAR7HE3ZeTItT6sS2GmpJmkqDxI74auMXG4gcfzHpi2ViFu3mUY8uJWftRvXpCxwm+Ixcar9yVw==", + "dependencies": { + "@tsed/engines": "8.16.2", + "ejs": "^3.1.10", + "tslib": "2.7.0" + }, + "peerDependencies": { + "@tsed/core": ">=8.0.0", + "@tsed/di": ">=8.0.0", + "@tsed/engines": ">=8.0.0", + "@tsed/exceptions": ">=8.0.0", + "@tsed/schema": ">=8.0.0" + }, + "peerDependenciesMeta": { + "@tsed/core": { + "optional": false + }, + "@tsed/di": { + "optional": false + }, + "@tsed/exceptions": { + "optional": false + }, + "@tsed/schema": { + "optional": false + } + } + }, + "node_modules/@tsed/platform-views/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tsed/scalar": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/scalar/-/scalar-8.16.2.tgz", + "integrity": "sha512-C+J0U84R88fzNbyr9YjOGC+0CLPtiVweIwy0r9B8n/g2i/AgOOydiZDOc2mzlbyT/hLHKQHe+Vhr6+tC+Q1jAw==", + "dependencies": { + "@scalar/types": "^0.0.23", + "@tsed/normalize-path": "8.16.2", + "@tsed/openapi-utils": "8.16.2", + "@tsed/openspec": "8.16.2", + "tslib": "2.7.0" + }, + "peerDependencies": { + "@tsed/platform-http": ">=8.0.0" + }, + "peerDependenciesMeta": { + "@tsed/platform-http": { + "optional": false + } + } + }, + "node_modules/@tsed/scalar/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tsed/schema": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/schema/-/schema-8.16.2.tgz", + "integrity": "sha512-daJ+tYUJc8Jz2hPP8YgSTsYqfIomRH7GiMl2bVtfN8eSZP36+n05lWSPBGrPCb8SdxZAJl3ZqsRDyOaLCfi+Sw==", + "dependencies": { + "@tsed/openspec": "8.16.2", + "change-case": "^5.4.4", + "json-schema": "0.4.0", + "picomatch": "4.0.2", + "statuses": "^2.0.1", + "tslib": "2.7.0" + }, + "peerDependencies": { + "@tsed/core": ">=8.0.0", + "@tsed/hooks": ">=8.0.0", + "@tsed/openspec": ">=8.0.0" + }, + "peerDependenciesMeta": { + "@tsed/core": { + "optional": false + }, + "@tsed/openspec": { + "optional": false + } + } + }, + "node_modules/@tsed/schema/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tsed/socketio": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/socketio/-/socketio-8.16.2.tgz", + "integrity": "sha512-JnskUYvQZf+JAfVUmagNYGoHRBDZlv2dCkBtnMzFuGmeQKi+DYMbILdIIH0nS2X33MEm6pNIfFZLGghwWDQnUQ==", + "dependencies": { + "tslib": "2.7.0" + }, + "peerDependencies": { + "@tsed/di": ">=8.0.0", + "@tsed/json-mapper": ">=8.0.0", + "@tsed/logger": ">=8.0.0", + "@tsed/platform-middlewares": ">=8.0.0", + "@tsed/schema": ">=8.0.0", + "socket.io": ">=4.0.0" + }, + "peerDependenciesMeta": { + "@tsed/di": { + "optional": false + }, + "@tsed/json-mapper": { + "optional": false + }, + "@tsed/platform-middlewares": { + "optional": false + }, + "@tsed/schema": { + "optional": false + } + } + }, + "node_modules/@tsed/socketio/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tsed/swagger": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@tsed/swagger/-/swagger-8.16.2.tgz", + "integrity": "sha512-DOkRJRSeL6FHTbF3yAPN0XnzmpHvR8LLM3XDhDZqOpXxVeiTNuD9uuTeiBiEEtuh8Aa3h27/A013LMMXvV+1eQ==", + "dependencies": { + "@tsed/normalize-path": "8.16.2", + "@tsed/openapi-utils": "8.16.2", + "@tsed/openspec": "8.16.2", + "swagger-ui-dist": ">=5.17.14", + "tslib": "2.7.0" + }, + "peerDependencies": { + "@tsed/platform-http": ">=8.0.0" + }, + "peerDependenciesMeta": { + "@tsed/platform-http": { + "optional": false + } + } + }, + "node_modules/@tsed/swagger/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", + "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/bcryptjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-3.0.0.tgz", + "integrity": "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==", + "deprecated": "This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed.", + "dependencies": { + "bcryptjs": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.9.tgz", + "integrity": "sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==", + "dev": true, + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", + "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/method-override": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/method-override/-/method-override-3.0.0.tgz", + "integrity": "sha512-7XFHR6j7JljprBpzzRZatakUXm1kEGAM3PL/GSsGRHtDvOAKYCdmnXX/5YSl1eQrpJymGs9tRekSWEGaG+Ntjw==", + "dev": true, + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" + }, + "node_modules/@types/multer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "24.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", + "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@unhead/schema": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/@unhead/schema/-/schema-1.11.20.tgz", + "integrity": "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA==", + "dependencies": { + "hookable": "^5.5.3", + "zhead": "^2.2.4" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/@xhmikosr/archive-type": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@xhmikosr/archive-type/-/archive-type-7.1.0.tgz", + "integrity": "sha512-xZEpnGplg1sNPyEgFh0zbHxqlw5dtYg6viplmWSxUj12+QjU9SKu3U/2G73a15pEjLaOqTefNSZ1fOPUOT4Xgg==", + "dependencies": { + "file-type": "^20.5.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/bin-check": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@xhmikosr/bin-check/-/bin-check-7.1.0.tgz", + "integrity": "sha512-y1O95J4mnl+6MpVmKfMYXec17hMEwE/yeCglFNdx+QvLLtP0yN4rSYcbkXnth+lElBuKKek2NbvOfOGPpUXCvw==", + "dependencies": { + "execa": "^5.1.1", + "isexe": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/bin-wrapper": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@xhmikosr/bin-wrapper/-/bin-wrapper-13.2.0.tgz", + "integrity": "sha512-t9U9X0sDPRGDk5TGx4dv5xiOvniVJpXnfTuynVKwHgtib95NYEw4MkZdJqhoSiz820D9m0o6PCqOPMXz0N9fIw==", + "dependencies": { + "@xhmikosr/bin-check": "^7.1.0", + "@xhmikosr/downloader": "^15.2.0", + "@xhmikosr/os-filter-obj": "^3.0.0", + "bin-version-check": "^5.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/decompress": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress/-/decompress-10.2.0.tgz", + "integrity": "sha512-MmDBvu0+GmADyQWHolcZuIWffgfnuTo4xpr2I/Qw5Ox0gt+e1Be7oYqJM4te5ylL6mzlcoicnHVDvP27zft8tg==", + "dependencies": { + "@xhmikosr/decompress-tar": "^8.1.0", + "@xhmikosr/decompress-tarbz2": "^8.1.0", + "@xhmikosr/decompress-targz": "^8.1.0", + "@xhmikosr/decompress-unzip": "^7.1.0", + "graceful-fs": "^4.2.11", + "strip-dirs": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/decompress-tar": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-tar/-/decompress-tar-8.1.0.tgz", + "integrity": "sha512-m0q8x6lwxenh1CrsTby0Jrjq4vzW/QU1OLhTHMQLEdHpmjR1lgahGz++seZI0bXF3XcZw3U3xHfqZSz+JPP2Gg==", + "dependencies": { + "file-type": "^20.5.0", + "is-stream": "^2.0.1", + "tar-stream": "^3.1.7" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/decompress-tarbz2": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-tarbz2/-/decompress-tarbz2-8.1.0.tgz", + "integrity": "sha512-aCLfr3A/FWZnOu5eqnJfme1Z1aumai/WRw55pCvBP+hCGnTFrcpsuiaVN5zmWTR53a8umxncY2JuYsD42QQEbw==", + "dependencies": { + "@xhmikosr/decompress-tar": "^8.0.1", + "file-type": "^20.5.0", + "is-stream": "^2.0.1", + "seek-bzip": "^2.0.0", + "unbzip2-stream": "^1.4.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/decompress-targz": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-targz/-/decompress-targz-8.1.0.tgz", + "integrity": "sha512-fhClQ2wTmzxzdz2OhSQNo9ExefrAagw93qaG1YggoIz/QpI7atSRa7eOHv4JZkpHWs91XNn8Hry3CwUlBQhfPA==", + "dependencies": { + "@xhmikosr/decompress-tar": "^8.0.1", + "file-type": "^20.5.0", + "is-stream": "^2.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/decompress-unzip": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-unzip/-/decompress-unzip-7.1.0.tgz", + "integrity": "sha512-oqTYAcObqTlg8owulxFTqiaJkfv2SHsxxxz9Wg4krJAHVzGWlZsU8tAB30R6ow+aHrfv4Kub6WQ8u04NWVPUpA==", + "dependencies": { + "file-type": "^20.5.0", + "get-stream": "^6.0.1", + "yauzl": "^3.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/downloader": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/@xhmikosr/downloader/-/downloader-15.2.0.tgz", + "integrity": "sha512-lAqbig3uRGTt0sHNIM4vUG9HoM+mRl8K28WuYxyXLCUT6pyzl4Y4i0LZ3jMEsCYZ6zjPZbO9XkG91OSTd4si7g==", + "dependencies": { + "@xhmikosr/archive-type": "^7.1.0", + "@xhmikosr/decompress": "^10.2.0", + "content-disposition": "^0.5.4", + "defaults": "^2.0.2", + "ext-name": "^5.0.0", + "file-type": "^20.5.0", + "filenamify": "^6.0.0", + "get-stream": "^6.0.1", + "got": "^13.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/os-filter-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@xhmikosr/os-filter-obj/-/os-filter-obj-3.0.0.tgz", + "integrity": "sha512-siPY6BD5dQ2SZPl3I0OZBHL27ZqZvLEosObsZRQ1NUB8qcxegwt0T9eKtV96JMFQpIz1elhkzqOg4c/Ri6Dp9A==", + "dependencies": { + "arch": "^3.0.0" + }, + "engines": { + "node": "^14.14.0 || >=16.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", + "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==", + "peerDependencies": { + "ajv": "^8.0.1" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, + "node_modules/arch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-3.0.0.tgz", + "integrity": "sha512-AmIAC+Wtm2AU8lGfTtHsw0Y9Qtftx2YXEEtiBP10xFUtMOA+sHHx6OAddyL52mUKh1vsXQ6/w1mVDptZCyUt4Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.1.tgz", + "integrity": "sha512-Kn4kbSXpkFHCGE6rBFNwIv0GQs4AvDT80jlveJDKFxjbTYMUeB4QtsdPCv6H8Cm19Je7IU6VFtRl2zWZI0rudQ==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/b4a": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.0.tgz", + "integrity": "sha512-KtsH1alSKomfNi/yDAFaD8PPFfi0LxJCEbPuzogcXrMF+yH40Z1ykTDo2vyxuQfN1FLjv0LFM7CadLHEPrVifw==", + "peerDependencies": { + "react-native-b4a": "^0.0.0" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/bare-events": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.6.1.tgz", + "integrity": "sha512-AuTJkq9XmE6Vk0FJVNq5QxETrSA/vKHarWVBG5l/JbdCL1prJemiyJqUS0jrlXO0MftuPq4m3YVYhoNc5+aE/g==", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/bin-version": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bin-version/-/bin-version-6.0.0.tgz", + "integrity": "sha512-nk5wEsP4RiKjG+vF+uG8lFsEn4d7Y6FVDamzzftSunXOoOcOOkzcWdKVlGgFFwlUQCj63SgnUkLLGF8v7lufhw==", + "dependencies": { + "execa": "^5.0.0", + "find-versions": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bin-version-check": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bin-version-check/-/bin-version-check-5.1.0.tgz", + "integrity": "sha512-bYsvMqJ8yNGILLz1KP9zKLzQ6YpljV3ln1gqhuLkUtyfGi3qXKGuK2p+U4NAvjVFzDFiBBtOpCOSFNuYYEGZ5g==", + "dependencies": { + "bin-version": "^6.0.0", + "semver": "^7.5.3", + "semver-truncate": "^3.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cache-manager": { + "version": "5.7.6", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.7.6.tgz", + "integrity": "sha512-wBxnBHjDxF1RXpHCBD6HGvKER003Ts7IIm0CHpggliHzN1RZditb7rXoduE1rplc2DEFYKxhLKgFuchXMJje9w==", + "dependencies": { + "eventemitter3": "^5.0.1", + "lodash.clonedeep": "^4.5.0", + "lru-cache": "^10.2.2", + "promise-coalesce": "^1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "dependencies": { + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-env": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.0.0.tgz", + "integrity": "sha512-aU8qlEK/nHYtVuN4p7UQgAwVljzMg8hB4YK5ThRqD2l/ziSnryncPNn7bMLt5cFYsKVKBh8HqLqyCoTupEUu7Q==", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-2.0.2.tgz", + "integrity": "sha512-cuIw0PImdp76AOfgkjbW4VhQODRmNNcKR73vdCH5cLd/ifj7aamfoXvYgfGkEAjNJZ3ozMIy9Gu2LutUkGEPbA==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dotenv": { + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz", + "integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz", + "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-flow": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/dotenv-flow/-/dotenv-flow-4.1.0.tgz", + "integrity": "sha512-0cwP9jpQBQfyHwvE0cRhraZMkdV45TQedA8AAUZMsFzvmLcQyc1HPv+oX0OOYwLFjIlvgVepQ+WuQHbqDaHJZg==", + "dependencies": { + "dotenv": "^16.0.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/dotenv-flow/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ext-list": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", + "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", + "dependencies": { + "mime-db": "^1.28.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ext-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", + "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", + "dependencies": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==" + }, + "node_modules/file-type": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.5.0.tgz", + "integrity": "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg==", + "dependencies": { + "@tokenizer/inflate": "^0.2.6", + "strtok3": "^10.2.0", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/filename-reserved-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz", + "integrity": "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/filenamify": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-6.0.0.tgz", + "integrity": "sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ==", + "dependencies": { + "filename-reserved-regex": "^3.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-versions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-5.1.0.tgz", + "integrity": "sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg==", + "dependencies": { + "semver-regex": "^4.0.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz", + "integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==" + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/inspect-with-kind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/inspect-with-kind/-/inspect-with-kind-1.0.5.tgz", + "integrity": "sha512-MAQUJuIo7Xqk8EVNP+6d3CKq9c80hi4tjIbIAT6lmGW9W6WzlHiu9PS8uSuUYU+Do+j1baiFp3H25XEVxDIG2g==", + "dependencies": { + "kind-of": "^6.0.2" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/lru.min": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz", + "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/method-override": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/method-override/-/method-override-3.0.0.tgz", + "integrity": "sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==", + "dependencies": { + "debug": "3.1.0", + "methods": "~1.1.2", + "parseurl": "~1.3.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/method-override/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/method-override/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mysql2": { + "version": "3.14.5", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.5.tgz", + "integrity": "sha512-40hDf8LPUsuuJ2hFq+UgOuPwt2IFLIRDvMv6ez9hKbXeYuZPxDDwiJW7KdknvOsQqKznaKczOT1kELgFkhDvFg==", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", + "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nodemon/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nodemon/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/nodemon/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.0.tgz", + "integrity": "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/oxc-resolver": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.7.1.tgz", + "integrity": "sha512-PzbEnD6NKTCFVKkUZtmQcX69ajdfM33RqI5kyb8mH9EdIqEUS00cWSXN0lsgYrtdTMzwo0EKKoH7hnGg6EDraQ==", + "hasInstallScript": true, + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-resolver/binding-android-arm-eabi": "11.7.1", + "@oxc-resolver/binding-android-arm64": "11.7.1", + "@oxc-resolver/binding-darwin-arm64": "11.7.1", + "@oxc-resolver/binding-darwin-x64": "11.7.1", + "@oxc-resolver/binding-freebsd-x64": "11.7.1", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.7.1", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.7.1", + "@oxc-resolver/binding-linux-arm64-gnu": "11.7.1", + "@oxc-resolver/binding-linux-arm64-musl": "11.7.1", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.7.1", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.7.1", + "@oxc-resolver/binding-linux-riscv64-musl": "11.7.1", + "@oxc-resolver/binding-linux-s390x-gnu": "11.7.1", + "@oxc-resolver/binding-linux-x64-gnu": "11.7.1", + "@oxc-resolver/binding-linux-x64-musl": "11.7.1", + "@oxc-resolver/binding-wasm32-wasi": "11.7.1", + "@oxc-resolver/binding-win32-arm64-msvc": "11.7.1", + "@oxc-resolver/binding-win32-ia32-msvc": "11.7.1", + "@oxc-resolver/binding-win32-x64-msvc": "11.7.1" + } + }, + "node_modules/p-cancelable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/piscina": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.9.2.tgz", + "integrity": "sha512-Fq0FERJWFEUpB4eSY59wSNwXD4RYqR+nR/WiEVcZW8IWfVBxJJafcgTEZDQo8k3w0sUarJ8RyVbbUF4GQ2LGbQ==", + "optionalDependencies": { + "@napi-rs/nice": "^1.0.1" + } + }, + "node_modules/promise-coalesce": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/promise-coalesce/-/promise-coalesce-1.1.2.tgz", + "integrity": "sha512-zLaJ9b8hnC564fnJH6NFSOGZYYdzrAJn2JUUIwzoQb32fG2QAakpDNM+CZo1km6keXkRXRM+hml1BFAPVnPkxg==", + "engines": { + "node": ">=16" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, + "node_modules/responselike": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/seek-bzip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-2.0.0.tgz", + "integrity": "sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg==", + "dependencies": { + "commander": "^6.0.0" + }, + "bin": { + "seek-bunzip": "bin/seek-bunzip", + "seek-table": "bin/seek-bzip-table" + } + }, + "node_modules/seek-bzip/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-regex": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-4.0.5.tgz", + "integrity": "sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver-truncate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/semver-truncate/-/semver-truncate-3.0.0.tgz", + "integrity": "sha512-LJWA9kSvMolR51oDE6PN3kALBNaUdkxzAGcexw8gjMA8xr5zUqK0JiR3CgARSqanYF3Z1YHvsErb1KDgh+v7Rg==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-keys-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==", + "dependencies": { + "sort-keys": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/streamx": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-dirs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-3.0.0.tgz", + "integrity": "sha512-I0sdgcFTfKQlUPZyAqPJmSG3HLO9rWDFnxonnIbskYNM3DwFOeTNB5KzVq3dA1GdRAc/25b5Y7UO2TQfKWw4aQ==", + "dependencies": { + "inspect-with-kind": "^1.0.5", + "is-plain-obj": "^1.1.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/strtok3": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.29.0.tgz", + "integrity": "sha512-gqs7Md3AxP4mbpXAq31o5QW+wGUZsUzVatg70yXpUR245dfIis5jAzufBd+UQM/w2xSfrhvA1eqsrgnl2PbezQ==", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.1.tgz", + "integrity": "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==", + "dependencies": { + "@borewit/text-codec": "^0.1.0", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==" + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yauzl": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.0.tgz", + "integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/zhead": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/zhead/-/zhead-2.2.4.tgz", + "integrity": "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==", + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..51f536b --- /dev/null +++ b/backend/package.json @@ -0,0 +1,77 @@ +{ + "name": "backend", + "version": "1.0.0", + "description": "", + "scripts": { + "build": "npm run barrels && swc src --out-dir dist -s --strip-leading-paths", + "barrels": "barrels", + "start": "npm run barrels && nodemon src/index.ts", + "start:prod": "cross-env NODE_ENV=production node dist/index.js" + }, + "dependencies": { + "@swc-node/register": "^1.11.1", + "@swc/cli": "^0.7.8", + "@swc/core": "^1.13.5", + "@swc/helpers": "^0.5.17", + "@tsed/ajv": "^8.16.2", + "@tsed/barrels": "^6.6.3", + "@tsed/core": "^8.16.2", + "@tsed/di": "^8.16.2", + "@tsed/engines": "^8.16.2", + "@tsed/exceptions": "^8.16.2", + "@tsed/json-mapper": "^8.16.2", + "@tsed/logger": "^8.0.4", + "@tsed/openspec": "^8.16.2", + "@tsed/platform-cache": "^8.16.2", + "@tsed/platform-exceptions": "^8.16.2", + "@tsed/platform-express": "^8.16.2", + "@tsed/platform-http": "^8.16.2", + "@tsed/platform-log-request": "^8.16.2", + "@tsed/platform-middlewares": "^8.16.2", + "@tsed/platform-multer": "^8.16.2", + "@tsed/platform-params": "^8.16.2", + "@tsed/platform-response-filter": "^8.16.2", + "@tsed/platform-views": "^8.16.2", + "@tsed/scalar": "^8.16.2", + "@tsed/schema": "^8.16.2", + "@tsed/socketio": "^8.16.2", + "@tsed/swagger": "^8.16.2", + "@types/bcryptjs": "^3.0.0", + "@types/jsonwebtoken": "^9.0.10", + "ajv": "^8.17.1", + "axios": "^1.6.0", + "bcryptjs": "^3.0.2", + "body-parser": "^2.2.0", + "compression": "^1.8.1", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "cross-env": "^10.0.0", + "dotenv": "^17.2.2", + "dotenv-expand": "^12.0.3", + "dotenv-flow": "^4.1.0", + "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", + "method-override": "^3.0.0", + "mysql2": "^3.14.5", + "socket.io": "^4.8.1", + "typescript": "^5.9.2" + }, + "devDependencies": { + "@types/compression": "^1.8.1", + "@types/cookie-parser": "^1.4.9", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/method-override": "^3.0.0", + "@types/multer": "^2.0.0", + "@types/node": "^24.3.1", + "nodemon": "^3.1.10", + "tslib": "^2.8.1" + }, + "tsed": { + "convention": "conv_default", + "architecture": "arc_default", + "packageManager": "npm", + "runtime": "node" + }, + "type": "module" +} diff --git a/backend/processes.config.cjs b/backend/processes.config.cjs new file mode 100644 index 0000000..b1e2ccb --- /dev/null +++ b/backend/processes.config.cjs @@ -0,0 +1,22 @@ +'use strict' + +const path = require('path') +const defaultLogFile = path.join(__dirname, '/logs/project-server.log') + +module.exports = { + 'apps': [ + { + name: 'api', + 'script': `${process.env.WORKDIR}/dist/index.js`, + 'cwd': process.env.WORKDIR, + exec_mode: "cluster", + instances: process.env.NODE_ENV === 'test' ? 1 : process.env.NB_INSTANCES || 2, + autorestart: true, + max_memory_restart: process.env.MAX_MEMORY_RESTART || '750M', + 'out_file': defaultLogFile, + 'error_file': defaultLogFile, + 'merge_logs': true, + 'kill_timeout': 30000, + } + ] +} diff --git a/backend/secret.txt b/backend/secret.txt new file mode 100644 index 0000000..a0800b1 --- /dev/null +++ b/backend/secret.txt @@ -0,0 +1,2 @@ +Portainer: 168.231.108.135:9443 / tundaadmin / retoortunapass1 + diff --git a/backend/src/Server.ts b/backend/src/Server.ts new file mode 100644 index 0000000..e89a2ba --- /dev/null +++ b/backend/src/Server.ts @@ -0,0 +1,101 @@ +import {join} from "node:path"; +import {Configuration} from "@tsed/di"; +import {application} from "@tsed/platform-http"; +import "@tsed/platform-log-request"; // remove this import if you don't want log request +import "@tsed/platform-express"; // /!\ keep this import +import "@tsed/ajv"; +import "@tsed/swagger"; +import "@tsed/scalar"; +import {config} from "./config/index.js"; +import * as rest from "./controllers/rest/index.js"; +import * as pages from "./controllers/pages/index.js"; +import {testConnection, closePool} from "./config/database.js"; +import {$log} from "@tsed/logger"; + +@Configuration({ + ...config, + acceptMimes: ["application/json"], + httpPort: process.env.PORT || 8083, + httpsPort: false, // CHANGE + mount: { + "/rest": [ + ...Object.values(rest) + ], + "/": [ + ...Object.values(pages) + ] + }, + swagger: [ + { + path: "/doc", + specVersion: "3.0.1", + spec: { + info: { + title: "Candivista API", + version: process.env.APP_VERSION || "1.0.0", + description: + "REST API for Candivista. Authentication via JWT Bearer tokens.\n\n" + + "Includes endpoints for auth, users, jobs, tokens, AI, and admin reporting.", + contact: { + name: "Candivista Team", + url: "https://candivista.com", + email: "support@candivista.com" + }, + license: { name: "Proprietary" } + }, + servers: [ + { url: "http://localhost:8083", description: "Local" } + ], + tags: [ + { name: "Auth", description: "Authentication and session management" }, + { name: "Users", description: "User profile and token summary" }, + { name: "Jobs", description: "Job posting and interview token operations" }, + { name: "Admin", description: "Administrative statistics and management" }, + { name: "AI", description: "AI provider tests and operations" } + ], + components: { + securitySchemes: { + bearerAuth: { type: "http", scheme: "bearer", bearerFormat: "JWT" } + } + }, + security: [{ bearerAuth: [] }] + } + } + ], + scalar: [ + { + path: "/scalar/doc", + specVersion: "3.0.1" + } + ], + middlewares: [ + "cors", + "cookie-parser", + "compression", + "method-override", + "json-parser", + { use: "urlencoded-parser", options: { extended: true }} + ], + views: { + root: join(process.cwd(), "views"), + extensions: { + ejs: "ejs" + } + } +}) +export class Server { + protected app = application(); + + async $onInit() { + // Test database connection on startup + const isConnected = await testConnection(); + if (!isConnected) { + $log.error("Failed to connect to database. Server will continue but database operations may fail."); + } + } + + async $onDestroy() { + // Close database pool on shutdown + await closePool(); + } +} diff --git a/backend/src/config/database.ts b/backend/src/config/database.ts new file mode 100644 index 0000000..0d66fe0 --- /dev/null +++ b/backend/src/config/database.ts @@ -0,0 +1,54 @@ +import mysql from 'mysql2/promise'; +import { $log } from '@tsed/logger'; + +export interface DatabaseConfig { + host: string; + port: number; + user: string; + password: string; + database: string; + connectionLimit: number; +} + +const config: DatabaseConfig = { + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT || '3306'), + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || 'candidb_main', + connectionLimit: parseInt(process.env.DB_CONNECTION_LIMIT || '10') +}; + +// Create connection pool +export const pool = mysql.createPool({ + ...config, + waitForConnections: true, + queueLimit: 0, + acquireTimeout: 60000, + timeout: 60000, + reconnect: true +}); + +// Test database connection +export async function testConnection(): Promise { + try { + const connection = await pool.getConnection(); + await connection.ping(); + connection.release(); + $log.info('Database connection established successfully'); + return true; + } catch (error) { + $log.error('Database connection failed:', error); + return false; + } +} + +// Graceful shutdown +export async function closePool(): Promise { + try { + await pool.end(); + $log.info('Database pool closed'); + } catch (error) { + $log.error('Error closing database pool:', error); + } +} diff --git a/backend/src/config/envs/index.ts b/backend/src/config/envs/index.ts new file mode 100644 index 0000000..c4ef2eb --- /dev/null +++ b/backend/src/config/envs/index.ts @@ -0,0 +1,7 @@ +import dotenv from "dotenv-flow"; + +process.env.NODE_ENV = process.env.NODE_ENV || "development"; + +export const config = dotenv.config(); +export const isProduction = process.env.NODE_ENV === "production"; +export const envs = process.env diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts new file mode 100644 index 0000000..60ec5c8 --- /dev/null +++ b/backend/src/config/index.ts @@ -0,0 +1,14 @@ +import {readFileSync} from "node:fs"; +import {envs} from "./envs/index.js"; +import loggerConfig from "./logger/index.js"; +const pkg = JSON.parse(readFileSync("./package.json", {encoding: "utf8"})); + +export const config: Partial = { + version: pkg.version, + envs, + ajv: { + returnsCoercedValues: true + }, + logger: loggerConfig, + // additional shared configuration +}; diff --git a/backend/src/config/logger/index.ts b/backend/src/config/logger/index.ts new file mode 100644 index 0000000..b800621 --- /dev/null +++ b/backend/src/config/logger/index.ts @@ -0,0 +1,25 @@ +import {DILoggerOptions} from "@tsed/di"; +import {$log} from "@tsed/logger"; +import {isProduction} from "../envs/index.js"; + +if (isProduction) { + $log.appenders.set("stdout", { + type: "stdout", + levels: ["info", "debug"], + layout: { + type: "json" + } + }); + + $log.appenders.set("stderr", { + levels: ["trace", "fatal", "error", "warn"], + type: "stderr", + layout: { + type: "json" + } + }); +} + +export default { + disableRoutesSummary: isProduction +}; diff --git a/backend/src/controllers/pages/IndexController.ts b/backend/src/controllers/pages/IndexController.ts new file mode 100644 index 0000000..679bbf1 --- /dev/null +++ b/backend/src/controllers/pages/IndexController.ts @@ -0,0 +1,29 @@ +import {Constant, Controller} from "@tsed/di"; +import {HeaderParams} from "@tsed/platform-params"; +import {View} from "@tsed/platform-views"; +import {SwaggerSettings} from "@tsed/swagger"; +import {Hidden, Get, Returns} from "@tsed/schema"; + +@Hidden() +@Controller("/") +export class IndexController { + @Constant("swagger", []) + private swagger: SwaggerSettings[]; + + @Get("/") + @View("swagger.ejs") + @(Returns(200, String).ContentType("text/html")) + get(@HeaderParams("x-forwarded-proto") protocol: string, @HeaderParams("host") host: string) { + const hostUrl = `${protocol || "http"}://${host}`; + + return { + BASE_URL: hostUrl, + docs: this.swagger.map((conf) => { + return { + url: hostUrl + conf.path, + ...conf + }; + }) + }; + } +} diff --git a/backend/src/controllers/pages/index.ts b/backend/src/controllers/pages/index.ts new file mode 100644 index 0000000..594d33d --- /dev/null +++ b/backend/src/controllers/pages/index.ts @@ -0,0 +1,4 @@ +/** + * @file Automatically generated by @tsed/barrels. + */ +export * from "./IndexController.js"; diff --git a/backend/src/controllers/rest/AIController.ts b/backend/src/controllers/rest/AIController.ts new file mode 100644 index 0000000..dc814f1 --- /dev/null +++ b/backend/src/controllers/rest/AIController.ts @@ -0,0 +1,644 @@ +import { Controller } from "@tsed/di"; +import { Post, Get } from "@tsed/schema"; +import { BodyParams, PathParams, QueryParams } from "@tsed/platform-params"; +import { Req } from "@tsed/platform-http"; +import { BadRequest, NotFound } from "@tsed/exceptions"; +import { JobService } from "../../services/JobService.js"; +import { AIService } from "../../services/AIService.js"; +import axios from "axios"; + +@Controller("/ai") +export class AIController { + private jobService = new JobService(); + private aiService = new AIService(); + private aiProvider = process.env.AI_PROVIDER || 'ollama'; // 'ollama' or 'openrouter' + private aiPort = process.env.AI_PORT || '11434'; + private aiModel = process.env.AI_MODEL || 'gpt-oss:20b'; + + // Test AI connection + @Get("/test-ai") + async testAI() { + try { + if (this.aiProvider === 'openrouter') { + const response = await this.aiService.generateResponse("Hello, please respond with exactly: 'AI is working'"); + return { + success: true, + aiResponse: response, + provider: 'openrouter', + model: process.env.OPENROUTER_MODEL || 'gemma' + }; + } else { + // Ollama test + const response = await axios.post(`http://localhost:${this.aiPort}/api/generate`, { + model: this.aiModel, + prompt: "Hello, please respond with exactly: 'AI is working'", + stream: false, + options: { + temperature: 0.1, + max_tokens: 50 + } + }); + + return { + success: true, + aiResponse: response.data.response, + provider: 'ollama', + model: this.aiModel, + port: this.aiPort + }; + } + } catch (error) { + console.error('AI test failed:', error); + return { + success: false, + error: error.message, + provider: this.aiProvider, + model: this.aiProvider === 'openrouter' ? process.env.OPENROUTER_MODEL : this.aiModel + }; + } + } + + // Get mandatory questions for the job + @Get("/mandatory-questions/:linkId") + async getMandatoryQuestions(@PathParams("linkId") linkId: string) { + try { + // Verify the job exists and link is valid + const jobData = await this.jobService.getJobByLinkId(linkId); + if (!jobData) { + throw new NotFound("Interview link not found or expired"); + } + + const mandatoryQuestions = jobData.interview_questions || []; + + return { + success: true, + questions: mandatoryQuestions, + hasMandatoryQuestions: mandatoryQuestions.length > 0 + }; + } catch (error) { + console.error('Error getting mandatory questions:', error); + throw error; + } + } + + // Submit mandatory question answers + @Post("/submit-mandatory-answers") + async submitMandatoryAnswers(@BodyParams() body: any, @QueryParams() query: any) { + try { + const { candidateName, job, linkId, answers } = body; + const isTestMode = query.test === 'true' || body.test === true; + + if (!candidateName || !job || !linkId || !answers) { + throw new BadRequest("Missing required fields: candidateName, job, linkId, answers"); + } + + // Verify the job exists and link is valid + const jobData = await this.jobService.getJobByLinkId(linkId); + if (!jobData) { + throw new NotFound("Interview link not found or expired"); + } + + // Create or get interview record (skip DB writes in test mode) + const interviewId = await this.jobService.getOrCreateInterview(linkId, candidateName, isTestMode); + + // Save all mandatory question answers + for (let i = 0; i < answers.length; i++) { + const question = jobData.interview_questions[i]; + const answer = answers[i]; + + if (question && answer) { + // Save as AI message (question) + await this.jobService.saveConversationMessage(interviewId, linkId, 'ai', `Question ${i + 1}: ${question}`, isTestMode); + // Save as candidate message (answer) + await this.jobService.saveConversationMessage(interviewId, linkId, 'candidate', answer, isTestMode); + } + } + + // Log mandatory questions completed + await this.jobService.logInterviewEvent(linkId, 'mandatory_questions_completed', { + candidateName, + interviewId, + questionsAnswered: answers.length, + timestamp: new Date().toISOString() + }); + + return { + success: true, + message: "Mandatory questions answered successfully", + interviewId + }; + } catch (error) { + console.error('Error submitting mandatory answers:', error); + throw error; + } + } + + // Start interview with AI agent (only after mandatory questions) + @Post("/start-interview") + async startInterview(@BodyParams() body: any, @QueryParams() query: any) { + try { + const { candidateName, job, linkId } = body; + const isTestMode = query.test === 'true' || body.test === true; + + if (!candidateName || !job || !linkId) { + throw new BadRequest("Missing required fields: candidateName, job, linkId"); + } + + // Verify the job exists and link is valid + const jobData = await this.jobService.getJobByLinkId(linkId); + if (!jobData) { + throw new NotFound("Interview link not found or expired"); + } + + // Create or get interview record (skip DB writes in test mode) + const interviewId = await this.jobService.getOrCreateInterview(linkId, candidateName, isTestMode); + + // Get conversation history to include mandatory question answers + let conversationHistory; + if (isTestMode) { + // In test mode, we can't get conversation history from DB since we don't save + // The frontend should pass the mandatory question answers in the request + conversationHistory = []; + console.log(`[DEBUG] Starting AI in test mode - no conversation history available`); + } else { + // In production mode, get from database + conversationHistory = await this.jobService.getConversationHistory(interviewId); + console.log(`[DEBUG] Starting AI with conversation history: ${JSON.stringify(conversationHistory, null, 2)}`); + } + + // Generate initial AI message using chatbot service (fail if AI unavailable) + const initialMessage = await this.aiService.initializeInterviewWithChatbot(job, candidateName, linkId, conversationHistory); + console.log(`[DEBUG] initializeInterviewWithChatbot returned: "${initialMessage}"`); + if (!initialMessage) { + throw new Error("AI service is currently unavailable. Please try again later."); + } + + // Save AI message to conversation + await this.jobService.saveConversationMessage(interviewId, linkId, 'ai', initialMessage, isTestMode); + + // Log interview start + await this.jobService.logInterviewEvent(linkId, 'started', { + candidateName, + interviewId, + timestamp: new Date().toISOString() + }); + + return { + success: true, + message: initialMessage, + job: jobData, + interviewId + }; + } catch (error: any) { + console.error('Error starting interview:', error); + throw error; + } + } + + // Handle chat messages + @Post("/chat") + async handleChat(@BodyParams() body: any, @QueryParams() query: any) { + try { + const { message, candidateName, job, linkId, conversationHistory } = body; + const isTestMode = query.test === 'true' || body.test === true; + + if (!message || !candidateName || !job || !linkId) { + throw new BadRequest("Missing required fields: message, candidateName, job, linkId"); + } + + // Verify the job exists and link is valid + const jobData = await this.jobService.getJobByLinkId(linkId); + if (!jobData) { + throw new NotFound("Interview link not found or expired"); + } + + // Get or create interview record + const interviewId = await this.jobService.getOrCreateInterview(linkId, candidateName, isTestMode); + + // Save user message to conversation + await this.jobService.saveConversationMessage(interviewId, linkId, 'candidate', message, isTestMode); + + // Get conversation history - use frontend data in test mode, database in production + let conversationHistoryToUse; + if (isTestMode) { + // In test mode, use the conversation history passed from frontend + conversationHistoryToUse = conversationHistory || []; + console.log(`[DEBUG] Using frontend conversation history (test mode): ${JSON.stringify(conversationHistoryToUse, null, 2)}`); + + // Filter out any messages with undefined content + conversationHistoryToUse = conversationHistoryToUse.filter(msg => + msg && msg.message && msg.message !== 'undefined' && msg.sender + ); + console.log(`[DEBUG] Filtered conversation history: ${JSON.stringify(conversationHistoryToUse, null, 2)}`); + } else { + // In production mode, get from database + conversationHistoryToUse = await this.jobService.getConversationHistory(interviewId); + console.log(`[DEBUG] Retrieved conversation history from database: ${JSON.stringify(conversationHistoryToUse, null, 2)}`); + } + + // Generate AI response using chatbot service + const aiResponse = await this.generateAIResponseWithChatbot(message, job, conversationHistoryToUse, candidateName, linkId); + console.log(`[DEBUG] generateAIResponseWithChatbot returned:`, aiResponse); + + if (!aiResponse) { + throw new Error("AI service is currently unavailable. Please try again later."); + } + + // Save AI response to conversation + await this.jobService.saveConversationMessage(interviewId, linkId, 'ai', aiResponse.message, isTestMode); + + // Log the messages + await this.jobService.logInterviewEvent(linkId, 'user_message', { + candidateName, + message, + interviewId, + timestamp: new Date().toISOString() + }); + + await this.jobService.logInterviewEvent(linkId, 'ai_message', { + candidateName, + message: aiResponse.message, + interviewId, + timestamp: new Date().toISOString() + }); + + return { + success: true, + message: aiResponse.message, + isComplete: aiResponse.isComplete + }; + } catch (error: any) { + console.error('Error handling chat:', error); + throw error; + } + } + + // Get conversation history + @Get("/conversation/:linkId") + async getConversation(@PathParams("linkId") linkId: string) { + try { + const jobData = await this.jobService.getJobByLinkId(linkId); + if (!jobData) { + throw new NotFound("Interview link not found or expired"); + } + + const interviewId = await this.jobService.getInterviewIdByLink(linkId); + if (!interviewId) { + return { + success: true, + messages: [] + }; + } + + const messages = await this.jobService.getConversationHistory(interviewId); + + return { + success: true, + messages: messages + }; + } catch (error: any) { + console.error('Error getting conversation:', error); + throw error; + } + } + + // End interview + @Post("/end-interview/:linkId") + async endInterview(@PathParams("linkId") linkId: string) { + try { + const jobData = await this.jobService.getJobByLinkId(linkId); + if (!jobData) { + throw new NotFound("Interview link not found or expired"); + } + + const interviewId = await this.jobService.getInterviewIdByLink(linkId); + if (!interviewId) { + throw new NotFound("Interview not found"); + } + + // End interview with chatbot service + await this.aiService.endInterviewWithChatbot(linkId); + + // Mark interview as completed + await this.jobService.completeInterview(interviewId); + + // Log interview completion + await this.jobService.logInterviewEvent(linkId, 'completed', { + interviewId, + timestamp: new Date().toISOString() + }); + + return { + success: true, + message: "Interview completed successfully" + }; + } catch (error: any) { + console.error('Error ending interview:', error); + throw error; + } + } + + private async generateInitialMessage(job: any, candidateName: string, conversationHistory: any[] = []): Promise { + const skills = job.skills_required ? job.skills_required.join(', ') : 'various technical skills'; + const experience = job.experience_level.replace('_', ' '); + + // Build context from conversation history (mandatory question answers) + const conversationContext = conversationHistory + .map(msg => `${msg.sender === 'candidate' ? 'Candidate' : 'Interviewer'}: ${msg.message}`) + .join('\n'); + + const systemMessage = `You are an AI interview agent conducting an interview for the position: ${job.title} + +Job Description: ${job.description} +Requirements: ${job.requirements} +Required Skills: ${skills} +Experience Level: ${experience} +Location: ${job.location || 'Remote'} + +${conversationContext ? `Previous conversation (mandatory questions answered): +${conversationContext} + +Based on the candidate's answers to the mandatory questions above, you should now conduct a deeper interview.` : ''} + +Your task is to: +1. Greet the candidate warmly and professionally +2. Introduce yourself as their evaluation agent +3. ${conversationContext ? 'Acknowledge their previous answers and build upon them' : 'Explain that you\'ll be conducting a comprehensive interview'} +4. Ask them to tell you about themselves and their interest in this role +5. Keep your response conversational and engaging +6. Don't ask multiple questions at once - start with one open-ended question + +Respond in a friendly, professional tone. Keep it concise but welcoming.`; + + const userPrompt = `The candidate's name is ${candidateName}. Please start the interview.`; + + try { + if (this.aiProvider === 'openrouter') { + const response = await this.aiService.generateResponse(userPrompt, systemMessage); + if (response) { + return response; + } else { + console.log('[WARN] OpenRouter failed, falling back to Ollama'); + // Fallback to Ollama if OpenRouter fails + } + } + + // Ollama fallback (either configured or as fallback) + const response = await axios.post(`http://localhost:${this.aiPort}/api/generate`, { + model: this.aiModel, + prompt: `${systemMessage}\n\n${userPrompt}`, + stream: false, + options: { + temperature: 0.7, + max_tokens: 500 + } + }); + + return response.data.response || null; + } catch (error) { + console.error('Error calling AI:', error); + return null; // Return null instead of fallback message + } + } + + private async generateAIResponseWithChatbot(userMessage: string, job: any, conversationHistory: any[], candidateName: string, linkId: string): Promise<{ message: string; isComplete: boolean } | null> { + // Check if we should end the interview (after 10+ exchanges) + const userMessages = conversationHistory.filter(msg => msg.sender === 'candidate').length; + const shouldEnd = userMessages >= 10; + + if (shouldEnd) { + const endPrompt = `The interview is coming to a close. The candidate has provided comprehensive responses about their background and experience for the ${job.title} position. + +Please provide a professional closing message that: +1. Thanks the candidate for their time and thoughtful responses +2. Acknowledges their qualifications and interest +3. Explains that their responses will be reviewed by the hiring team +4. Mentions they should expect to hear back within a few business days +5. Keeps it warm and professional + +Keep it concise and professional.`; + + try { + const response = await this.aiService.generateResponseWithChatbot( + endPrompt, + conversationHistory, + undefined, + job, + candidateName, + linkId + ); + + return { + message: response || "Thank you for your time and detailed responses. That concludes our interview. We'll review your answers and get back to you within a few business days.", + isComplete: true + }; + } catch (error) { + console.error('Error calling chatbot for end message:', error); + return { + message: "Thank you for your time and detailed responses. That concludes our interview. We'll review your answers and get back to you within a few business days.", + isComplete: true + }; + } + } + + // Build context for ongoing conversation + const conversationContext = conversationHistory + .slice(-6) // Last 6 messages for context + .map(msg => `${msg.sender === 'candidate' ? 'Candidate' : 'Interviewer'}: ${msg.message}`) + .join('\n'); + + // Debug logging + console.log(`[DEBUG] Conversation history length: ${conversationHistory.length}`); + console.log(`[DEBUG] Conversation context: ${conversationContext}`); + console.log(`[DEBUG] User message: ${userMessage}`); + + const systemMessage = `You are an AI interview agent conducting an interview for the position: ${job.title} + +Job Details: +- Title: ${job.title} +- Description: ${job.description} +- Requirements: ${job.requirements} +- Required Skills: ${job.skills_required ? job.skills_required.join(', ') : 'Various technical skills'} +- Experience Level: ${job.experience_level.replace('_', ' ')} + +CRITICAL INSTRUCTIONS: +1. You MUST acknowledge the candidate's response first +2. You MUST then ask ONE specific follow-up question +3. The question should be relevant to their answer and help evaluate their fit for the ${job.title} role +4. Focus on technical skills, experience, problem-solving, or behavioral aspects +5. Keep the question specific and engaging +6. Do NOT repeat the same question +7. Do NOT ask multiple questions at once +8. Maintain a professional but conversational tone + +RESPONSE FORMAT: +- Start with a brief acknowledgment of their answer +- Then ask exactly one follow-up question +- End your response after the question + +Example: +"Thanks for sharing that experience with React. That's exactly the kind of hands-on development we're looking for. Can you tell me about a specific challenge you faced while building that application and how you solved it?"`; + + const userPrompt = `Recent conversation: +${conversationContext} + +Candidate's latest response: ${userMessage} + +Please respond with an acknowledgment and follow-up question.`; + + try { + const aiResponse = await this.aiService.generateResponseWithChatbot( + userMessage, + conversationHistory, + systemMessage, + job, + candidateName, + linkId + ); + + if (aiResponse) { + console.log(`[DEBUG] Chatbot Response: ${aiResponse}`); + return { + message: aiResponse, + isComplete: false + }; + } else { + console.log('[WARN] Chatbot service failed, falling back to direct OpenRouter'); + // Fallback to original method + return await this.generateAIResponse(userMessage, job, conversationHistory, candidateName); + } + } catch (error) { + console.error('Error calling chatbot service:', error); + // Fallback to original method + return await this.generateAIResponse(userMessage, job, conversationHistory, candidateName); + } + } + + private async generateAIResponse(userMessage: string, job: any, conversationHistory: any[], candidateName: string): Promise<{ message: string; isComplete: boolean } | null> { + // Check if we should end the interview (after 10+ exchanges) + const userMessages = conversationHistory.filter(msg => msg.sender === 'candidate').length; + const shouldEnd = userMessages >= 10; + + if (shouldEnd) { + const endPrompt = `The interview is coming to a close. The candidate has provided comprehensive responses about their background and experience for the ${job.title} position. + +Please provide a professional closing message that: +1. Thanks the candidate for their time and thoughtful responses +2. Acknowledges their qualifications and interest +3. Explains that their responses will be reviewed by the hiring team +4. Mentions they should expect to hear back within a few business days +5. Keeps it warm and professional + +Keep it concise and professional.`; + + try { + const response = await axios.post(`http://localhost:${this.aiPort}/api/generate`, { + model: this.aiModel, + prompt: endPrompt, + stream: false, + options: { + temperature: 0.7, + max_tokens: 300 + } + }); + + return { + message: response.data.response || null, + isComplete: true + }; + } catch (error) { + console.error('Error calling Ollama for end message:', error); + return null; // Return null instead of fallback message + } + } + + // Build context for ongoing conversation + const conversationContext = conversationHistory + .slice(-6) // Last 6 messages for context + .map(msg => `${msg.sender === 'candidate' ? 'Candidate' : 'Interviewer'}: ${msg.message}`) + .join('\n'); + + // Debug logging + console.log(`[DEBUG] Conversation history length: ${conversationHistory.length}`); + console.log(`[DEBUG] Conversation context: ${conversationContext}`); + console.log(`[DEBUG] User message: ${userMessage}`); + + const systemMessage = `You are an AI interview agent conducting an interview for the position: ${job.title} + +Job Details: +- Title: ${job.title} +- Description: ${job.description} +- Requirements: ${job.requirements} +- Required Skills: ${job.skills_required ? job.skills_required.join(', ') : 'Various technical skills'} +- Experience Level: ${job.experience_level.replace('_', ' ')} + +CRITICAL INSTRUCTIONS: +1. You MUST acknowledge the candidate's response first +2. You MUST then ask ONE specific follow-up question +3. The question should be relevant to their answer and help evaluate their fit for the ${job.title} role +4. Focus on technical skills, experience, problem-solving, or behavioral aspects +5. Keep the question specific and engaging +6. Do NOT repeat the same question +7. Do NOT ask multiple questions at once +8. Maintain a professional but conversational tone + +RESPONSE FORMAT: +- Start with a brief acknowledgment of their answer +- Then ask exactly one follow-up question +- End your response after the question + +Example: +"Thanks for sharing that experience with React. That's exactly the kind of hands-on development we're looking for. Can you tell me about a specific challenge you faced while building that application and how you solved it?"`; + + const userPrompt = `Recent conversation: +${conversationContext} + +Candidate's latest response: ${userMessage} + +Please respond with an acknowledgment and follow-up question.`; + + try { + if (this.aiProvider === 'openrouter') { + const aiResponse = await this.aiService.generateResponseWithHistory(userMessage, conversationHistory, systemMessage); + if (aiResponse) { + console.log(`[DEBUG] OpenRouter Response: ${aiResponse}`); + return { + message: aiResponse, + isComplete: false + }; + } else { + console.log('[WARN] OpenRouter failed, falling back to Ollama'); + // Fallback to Ollama if OpenRouter fails + } + } + + // Ollama fallback (either configured or as fallback) + console.log(`[DEBUG] Sending to Ollama - Port: ${this.aiPort}, Model: ${this.aiModel}`); + console.log(`[DEBUG] Prompt length: ${systemMessage.length + userPrompt.length} characters`); + + const response = await axios.post(`http://localhost:${this.aiPort}/api/generate`, { + model: this.aiModel, + prompt: `${systemMessage}\n\n${userPrompt}`, + stream: false, + options: { + temperature: 0.7, + max_tokens: 400 + } + }); + + const aiResponse = response.data.response || null; + console.log(`[DEBUG] Ollama Response: ${aiResponse}`); + + return { + message: aiResponse, + isComplete: false + }; + } catch (error) { + console.error('Error calling AI:', error); + return null; // Return null instead of fallback message + } + } + + + +} diff --git a/backend/src/controllers/rest/AdminController.ts b/backend/src/controllers/rest/AdminController.ts new file mode 100644 index 0000000..43bf3a1 --- /dev/null +++ b/backend/src/controllers/rest/AdminController.ts @@ -0,0 +1,249 @@ +import { Controller } from "@tsed/di"; +import { Get, Post, Put, Patch, Delete, Tags, Summary, Description, Returns, Security } from "@tsed/schema"; +import { BodyParams, PathParams, QueryParams } from "@tsed/platform-params"; +import { Req } from "@tsed/platform-http"; +import { BadRequest, Unauthorized, NotFound } from "@tsed/exceptions"; +import jwt from "jsonwebtoken"; +import { AdminService } from "../../services/AdminService.js"; +import { UserService } from "../../services/UserService.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; + +@Controller("/admin") +@Tags("Admin") +@Security("bearerAuth") +export class AdminController { + private adminService = new AdminService(); + private userService = new UserService(); + + // Middleware to check if user is admin + private async checkAdmin(req: any) { + const token = req.headers.authorization?.replace("Bearer ", ""); + + if (!token) { + throw new Unauthorized("No token provided"); + } + + try { + const decoded = jwt.verify(token, JWT_SECRET) as any; + const user = await this.userService.getUserById(decoded.userId); + + if (!user) { + throw new Unauthorized("User not found"); + } + + if (user.role !== 'admin') { + throw new Unauthorized("Admin access required"); + } + + return user; + } catch (error) { + throw new Unauthorized("Invalid token or insufficient permissions"); + } + } + + // System Statistics + @Get("/statistics") + @Summary("Get system statistics") + @Description("High-level metrics: users, jobs, interviews, tokens, revenue") + @(Returns(200).Description("Statistics returned")) + @(Returns(401).Description("Unauthorized")) + async getSystemStatistics(@Req() req: any) { + await this.checkAdmin(req); + return await this.adminService.getSystemStatistics(); + } + + // User Management + @Get("/users") + @Summary("List all users") + @(Returns(200).Description("Users returned")) + async getAllUsers(@Req() req: any) { + await this.checkAdmin(req); + return await this.adminService.getAllUsers(); + } + + @Get("/users/:id") + @Summary("Get a user by ID") + @(Returns(200).Description("User returned")) + @(Returns(404).Description("User not found")) + async getUserById(@Req() req: any, @PathParams("id") id: string) { + await this.checkAdmin(req); + return await this.adminService.getUserById(id); + } + + @Put("/users/:id") + @Summary("Update a user") + @(Returns(200).Description("User updated")) + async updateUser( + @Req() req: any, + @PathParams("id") id: string, + @BodyParams() userData: any + ) { + await this.checkAdmin(req); + return await this.adminService.updateUser(id, userData); + } + + @Patch("/users/:id/toggle-status") + @Summary("Toggle user active status") + @(Returns(200).Description("User status toggled")) + async toggleUserStatus(@Req() req: any, @PathParams("id") id: string) { + await this.checkAdmin(req); + return await this.adminService.toggleUserStatus(id); + } + + @Patch("/users/:id/password") + @Summary("Change user password") + @(Returns(200).Description("Password updated")) + async changeUserPassword( + @Req() req: any, + @PathParams("id") id: string, + @BodyParams() body: { new_password: string } + ) { + await this.checkAdmin(req); + return await this.adminService.changeUserPassword(id, body.new_password); + } + + @Post("/users") + @Summary("Create a user") + @(Returns(200).Description("User created")) + async createUser(@Req() req: any, @BodyParams() userData: any) { + await this.checkAdmin(req); + return await this.adminService.createUser(userData); + } + + // Job Management + @Get("/jobs") + @Summary("List all jobs") + @(Returns(200).Description("Jobs returned")) + async getAllJobs(@Req() req: any) { + await this.checkAdmin(req); + return await this.adminService.getAllJobs(); + } + + @Get("/jobs/:id") + @Summary("Get job by ID") + @(Returns(200).Description("Job returned")) + async getJobById(@Req() req: any, @PathParams("id") id: string) { + await this.checkAdmin(req); + return await this.adminService.getJobById(id); + } + + @Patch("/jobs/:id/status") + @Summary("Update job status") + @(Returns(200).Description("Job status updated")) + async updateJobStatus( + @Req() req: any, + @PathParams("id") id: string, + @BodyParams() body: { status: string } + ) { + await this.checkAdmin(req); + return await this.adminService.updateJobStatus(id, body.status); + } + + @Put("/jobs/:id") + @Summary("Update job details") + @(Returns(200).Description("Job updated")) + async updateJob( + @Req() req: any, + @PathParams("id") id: string, + @BodyParams() jobData: any + ) { + await this.checkAdmin(req); + return await this.adminService.updateJob(id, jobData); + } + + // Token Management + @Get("/user-token-summaries") + @Summary("List user token summaries") + @(Returns(200).Description("Summaries returned")) + async getUserTokenSummaries(@Req() req: any) { + await this.checkAdmin(req); + return await this.adminService.getUserTokenSummaries(); + } + + @Post("/add-tokens") + @Summary("Add tokens to a user") + @(Returns(200).Description("Tokens added")) + async addTokensToUser(@Req() req: any, @BodyParams() tokenData: any) { + await this.checkAdmin(req); + return await this.adminService.addTokensToUser(tokenData); + } + + @Get("/token-packages") + @Summary("List token packages") + @(Returns(200).Description("Packages returned")) + async getTokenPackages(@Req() req: any) { + await this.checkAdmin(req); + return await this.adminService.getTokenPackages(); + } + + @Post("/token-packages") + @Summary("Create token package") + @(Returns(200).Description("Package created")) + async createTokenPackage(@Req() req: any, @BodyParams() packageData: any) { + await this.checkAdmin(req); + return await this.adminService.createTokenPackage(packageData); + } + + @Put("/token-packages/:id") + @Summary("Update token package") + @(Returns(200).Description("Package updated")) + async updateTokenPackage( + @Req() req: any, + @PathParams("id") id: string, + @BodyParams() packageData: any + ) { + await this.checkAdmin(req); + return await this.adminService.updateTokenPackage(id, packageData); + } + + @Patch("/token-packages/:id/toggle-status") + @Summary("Toggle token package active status") + @(Returns(200).Description("Package status toggled")) + async toggleTokenPackageStatus(@Req() req: any, @PathParams("id") id: string) { + await this.checkAdmin(req); + return await this.adminService.toggleTokenPackageStatus(id); + } + + @Delete("/token-packages/:id") + @Summary("Delete token package") + @(Returns(200).Description("Package deleted")) + async deleteTokenPackage(@Req() req: any, @PathParams("id") id: string) { + await this.checkAdmin(req); + return await this.adminService.deleteTokenPackage(id); + } + + // Interview Management + @Get("/interviews") + @Summary("List interviews") + @(Returns(200).Description("Interviews returned")) + async getAllInterviews(@Req() req: any) { + await this.checkAdmin(req); + return await this.adminService.getAllInterviews(); + } + + @Get("/interviews/:id") + @Summary("Get interview by ID") + @(Returns(200).Description("Interview returned")) + async getInterviewById(@Req() req: any, @PathParams("id") id: string) { + await this.checkAdmin(req); + return await this.adminService.getInterviewById(id); + } + + // Payment Records + @Get("/payments") + @Summary("List payment records") + @(Returns(200).Description("Payments returned")) + async getPaymentRecords(@Req() req: any) { + await this.checkAdmin(req); + return await this.adminService.getPaymentRecords(); + } + + @Get("/payments/:id") + @Summary("Get payment by ID") + @(Returns(200).Description("Payment returned")) + async getPaymentById(@Req() req: any, @PathParams("id") id: string) { + await this.checkAdmin(req); + return await this.adminService.getPaymentById(id); + } +} diff --git a/backend/src/controllers/rest/AuthController.ts b/backend/src/controllers/rest/AuthController.ts new file mode 100644 index 0000000..c40ad20 --- /dev/null +++ b/backend/src/controllers/rest/AuthController.ts @@ -0,0 +1,167 @@ +import { Controller } from "@tsed/di"; +import { Post, Get, Summary, Description, Returns, Tags, Security } from "@tsed/schema"; +import { BodyParams } from "@tsed/platform-params"; +import { Req } from "@tsed/platform-http"; +import { BadRequest, Unauthorized } from "@tsed/exceptions"; +import jwt from "jsonwebtoken"; +import { UserService } from "../../services/UserService.js"; +import { User, CreateUserRequest, UpdateUserRequest, UserResponse } from "../../models/User.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; + +@Controller("/auth") +@Tags("Auth") +export class AuthController { + private userService = new UserService(); + + @Post("/login") + @Summary("Authenticate and obtain a JWT") + @Description("Provide email and password to receive a signed JWT used for subsequent requests.") + @Returns(200).Description("Successful authentication") + @Returns(400).Description("Missing email or password") + @Returns(401).Description("Invalid credentials or deactivated account") + async login(@BodyParams() body: { email: string; password: string }) { + const { email, password } = body; + + if (!email || !password) { + throw new BadRequest("Email and password are required"); + } + + const user = await this.userService.getUserByEmail(email); + if (!user) { + throw new Unauthorized("Invalid credentials"); + } + + if (!user.is_active) { + throw new Unauthorized("Account is deactivated"); + } + + const isValidPassword = await this.userService.verifyPassword(user, password); + if (!isValidPassword) { + throw new Unauthorized("Invalid credentials"); + } + + // Update last login + await this.userService.updateLastLogin(user.id); + + const token = jwt.sign( + { userId: user.id, email: user.email, role: user.role }, + JWT_SECRET, + { expiresIn: "24h" } + ); + + return { + token, + user: { + id: user.id, + email: user.email, + first_name: user.first_name, + last_name: user.last_name, + role: user.role, + company_name: user.company_name, + avatar_url: user.avatar_url, + is_active: user.is_active, + last_login_at: user.last_login_at, + email_verified_at: user.email_verified_at, + created_at: user.created_at, + updated_at: user.updated_at + } + }; + } + + @Post("/register") + @Summary("Register a new recruiter user") + @Description("Creates a recruiter account and returns a JWT for immediate use.") + @Returns(200).Description("User created and token issued") + @Returns(400).Description("Validation failed or email already exists") + async register(@BodyParams() body: { email: string; password: string; first_name: string; last_name: string; company_name?: string }) { + const { email, password, first_name, last_name, company_name } = body; + + if (!email || !password || !first_name || !last_name) { + throw new BadRequest("Email, password, first name, and last name are required"); + } + + // Validate email format + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + throw new BadRequest("Invalid email format"); + } + + // Validate password strength + if (password.length < 8) { + throw new BadRequest("Password must be at least 8 characters long"); + } + + try { + const user = await this.userService.createUser({ + email, + password, + first_name, + last_name, + company_name, + role: 'recruiter' + }); + + // Generate token + const token = jwt.sign( + { userId: user.id, email: user.email, role: user.role }, + JWT_SECRET, + { expiresIn: "24h" } + ); + + return { + token, + user + }; + } catch (error: any) { + if (error.message.includes('already exists')) { + throw new BadRequest("User with this email already exists"); + } + throw new BadRequest("Failed to create user account"); + } + } + + @Get("/me") + @Security("bearerAuth") + @Summary("Get the current authenticated user") + @Description("Returns the profile of the user associated with the provided JWT.") + @Returns(200).Description("User profile returned") + @Returns(401).Description("Missing or invalid token") + async getCurrentUser(@Req() req: any) { + const token = req.headers.authorization?.replace("Bearer ", ""); + + if (!token) { + throw new Unauthorized("No token provided"); + } + + try { + const decoded = jwt.verify(token, JWT_SECRET) as any; + const user = await this.userService.getUserById(decoded.userId); + + if (!user) { + throw new Unauthorized("User not found"); + } + + if (!user.is_active) { + throw new Unauthorized("Account is deactivated"); + } + + return { + id: user.id, + email: user.email, + first_name: user.first_name, + last_name: user.last_name, + role: user.role, + company_name: user.company_name, + avatar_url: user.avatar_url, + is_active: user.is_active, + last_login_at: user.last_login_at, + email_verified_at: user.email_verified_at, + created_at: user.created_at, + updated_at: user.updated_at + }; + } catch (error) { + throw new Unauthorized("Invalid token"); + } + } +} diff --git a/backend/src/controllers/rest/HelloWorldController.ts b/backend/src/controllers/rest/HelloWorldController.ts new file mode 100644 index 0000000..2731bc8 --- /dev/null +++ b/backend/src/controllers/rest/HelloWorldController.ts @@ -0,0 +1,10 @@ +import {Controller} from "@tsed/di"; +import {Get} from "@tsed/schema"; + +@Controller("/hello-world") +export class HelloWorldController { + @Get("/") + get() { + return "hello"; + } +} diff --git a/backend/src/controllers/rest/JobController.ts b/backend/src/controllers/rest/JobController.ts new file mode 100644 index 0000000..08b44ad --- /dev/null +++ b/backend/src/controllers/rest/JobController.ts @@ -0,0 +1,501 @@ +import { Controller } from "@tsed/di"; +import { Post, Get, Delete, Put, Patch, Tags, Summary, Description, Returns, Security } from "@tsed/schema"; +import { BodyParams, PathParams } from "@tsed/platform-params"; +import { Req } from "@tsed/platform-http"; +import { Unauthorized, NotFound } from "@tsed/exceptions"; +import jwt from "jsonwebtoken"; +import { pool } from "../../config/database.js"; +import { UserService } from "../../services/UserService.js"; +import { JobService } from "../../services/JobService.js"; +import { TokenService } from "../../services/TokenService.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; + +@Controller("/jobs") +@Tags("Jobs") +export class JobController { + private userService = new UserService(); + private jobService = new JobService(); + private tokenService = new TokenService(); + + // Middleware to check if user is authenticated + private async checkAuth(req: any) { + const token = req.headers.authorization?.replace("Bearer ", ""); + + if (!token) { + throw new Unauthorized("No token provided"); + } + + try { + const decoded = jwt.verify(token, JWT_SECRET) as any; + const user = await this.userService.getUserById(decoded.userId); + + if (!user) { + throw new Unauthorized("User not found"); + } + + return user; + } catch (error) { + throw new Unauthorized("Invalid token"); + } + } + + // Create a new job + @Post("/") + @Security("bearerAuth") + @Summary("Create a new job") + @Description("Recruiters and admins can create a job posting.") + @(Returns(200).Description("Job created successfully")) + @(Returns(401).Description("Unauthorized or missing token")) + async createJob(@Req() req: any, @BodyParams() jobData: any) { + try { + console.log('=== JOB CREATION START ==='); + console.log('Job creation request received:', JSON.stringify(jobData, null, 2)); + console.log('Request headers:', req.headers); + + // Test database connection first + try { + const connection = await pool.getConnection(); + console.log('Database connection successful'); + connection.release(); + } catch (dbError) { + console.error('Database connection failed:', dbError); + throw new Error('Database connection failed: ' + dbError.message); + } + + const user = await this.checkAuth(req); + console.log('User authenticated:', user.email, user.role); + + // Check if user can create a job (basic validation) + if (user.role !== 'recruiter' && user.role !== 'admin') { + throw new Unauthorized("Only recruiters can create jobs"); + } + + // Validate required fields + if (!jobData.title || !jobData.description || !jobData.requirements) { + throw new Error("Missing required fields: title, description, or requirements"); + } + + console.log('All validations passed, creating job...'); + const createdJob = await this.jobService.createJob(user.id, jobData); + console.log('Job created successfully:', createdJob.id); + + return { + success: true, + job: createdJob, + message: "Job created successfully" + }; + } catch (error) { + console.error('=== JOB CREATION ERROR ==='); + console.error('Error type:', (error as any).constructor.name); + console.error('Error message:', (error as any).message); + console.error('Error stack:', (error as any).stack); + console.error('Full error object:', error as any); + throw error; + } + } + + // Test endpoint to check if the controller is working + @Get("/test") + @Summary("Test endpoint") + @Description("Returns a simple heartbeat for Job controller") + @(Returns(200).Description("Service reachable")) + async testEndpoint() { + return { + success: true, + message: "JobController is working!", + timestamp: new Date().toISOString() + }; + } + + // Get all jobs for a user + @Get("/") + @Security("bearerAuth") + @Summary("List jobs") + @Description("Recruiters see their jobs; admins see all jobs.") + @(Returns(200).Description("Array of jobs returned")) + @(Returns(401).Description("Unauthorized")) + async getJobs(@Req() req: any) { + try { + const user = await this.checkAuth(req); + console.log('Fetching jobs for user:', user.email, user.role); + + if (user.role === 'recruiter') { + // Recruiters can only see their own jobs + const jobs = await this.jobService.getJobsByUserId(user.id); + return { + success: true, + jobs: jobs + }; + } else if (user.role === 'admin') { + // Admins can see all jobs + const jobs = await this.jobService.getAllJobs(); + return { + success: true, + jobs: jobs + }; + } else { + throw new Unauthorized("Only recruiters and admins can access jobs"); + } + } catch (error: any) { + console.error('Error fetching jobs:', error); + throw error; + } + } + + // Get a single job by ID + @Get("/:id") + @Security("bearerAuth") + @Summary("Get a job by ID") + @(Returns(200).Description("Job found")) + @(Returns(401).Description("Unauthorized")) + @(Returns(404).Description("Job not found")) + async getJobById(@Req() req: any, @PathParams("id") id: string) { + try { + const user = await this.checkAuth(req); + console.log('Fetching job by ID:', id, 'for user:', user.email); + + const job = await this.jobService.getJobById(id); + + if (!job) { + throw new NotFound("Job not found"); + } + + // Check if user can access this job + if (user.role === 'recruiter' && job.user_id !== user.id) { + throw new Unauthorized("You can only view your own jobs"); + } + + // Get job links if any + const links = await this.jobService.getJobLinks(id); + + return { + success: true, + job: { + ...job, + links: links + } + }; + } catch (error: any) { + console.error('Error fetching job by ID:', error); + throw error; + } + } + + // Update a job (recruiter owns it or admin) + @Put("/:id") + @Security("bearerAuth") + @Summary("Update a job") + @(Returns(200).Description("Job updated")) + @(Returns(401).Description("Unauthorized")) + @(Returns(404).Description("Job not found")) + async updateJob(@Req() req: any, @PathParams("id") id: string, @BodyParams() body: any) { + const user = await this.checkAuth(req); + const job = await this.jobService.getJobById(id); + if (!job) { + throw new NotFound("Job not found"); + } + if (user.role === 'recruiter' && job.user_id !== user.id) { + throw new Unauthorized("You can only update your own jobs"); + } + const updated = await this.jobService.updateJob(id, body); + return { success: true, job: updated }; + } + + // Update job status + @Patch("/:id/status") + @Security("bearerAuth") + @Summary("Update job status") + @(Returns(200).Description("Job status updated")) + @(Returns(401).Description("Unauthorized")) + @(Returns(404).Description("Job not found")) + async updateJobStatus(@Req() req: any, @PathParams("id") id: string, @BodyParams() body: { status: string }) { + const user = await this.checkAuth(req); + const job = await this.jobService.getJobById(id); + if (!job) { + throw new NotFound("Job not found"); + } + if (user.role === 'recruiter' && job.user_id !== user.id) { + throw new Unauthorized("You can only update your own jobs"); + } + const updated = await this.jobService.updateJobStatus(id, body.status); + return { success: true, job: updated }; + } + + // Create a job link + @Post("/:id/links") + @Security("bearerAuth") + @Summary("Create interview link for a job") + @(Returns(200).Description("Link created")) + @(Returns(401).Description("Unauthorized")) + @(Returns(404).Description("Job not found")) + async createJobLink(@Req() req: any, @PathParams("id") id: string, @BodyParams() linkData: any) { + try { + const user = await this.checkAuth(req); + console.log('Creating job link for job:', id, 'by user:', user.email); + + // Verify job exists and user has access + const job = await this.jobService.getJobById(id); + if (!job) { + throw new NotFound("Job not found"); + } + + if (user.role === 'recruiter' && job.user_id !== user.id) { + throw new Unauthorized("You can only create links for your own jobs"); + } + + const link = await this.jobService.createJobLink(id, linkData.tokens_available || 0); + + return { + success: true, + link: link, + message: "Job link created successfully" + }; + } catch (error: any) { + console.error('Error creating job link:', error); + throw error; + } + } + + // Add tokens to a job link + @Post("/:id/links/:linkId/tokens") + @Security("bearerAuth") + @Summary("Add tokens to a job link") + @(Returns(200).Description("Tokens added")) + @(Returns(400).Description("Insufficient tokens or invalid amount")) + @(Returns(401).Description("Unauthorized")) + @(Returns(404).Description("Job not found")) + async addTokensToLink(@Req() req: any, @PathParams("id") id: string, @PathParams("linkId") linkId: string, @BodyParams() tokenData: any) { + try { + const user = await this.checkAuth(req); + console.log('Adding tokens to link:', linkId, 'for job:', id, 'by user:', user.email); + + // Verify job exists and user has access + const job = await this.jobService.getJobById(id); + if (!job) { + throw new NotFound("Job not found"); + } + + if (user.role === 'recruiter' && job.user_id !== user.id) { + throw new Unauthorized("You can only modify links for your own jobs"); + } + + // Check if user has enough tokens + const tokenSummary = await this.tokenService.getUserTokenSummary(user.id); + const tokensToAdd = tokenData.tokens || 0; + + if (tokenSummary.total_available < tokensToAdd) { + return { + success: false, + error: "INSUFFICIENT_TOKENS", + message: `You don't have enough tokens. You have ${tokenSummary.total_available} tokens available, but need ${tokensToAdd}.`, + available_tokens: tokenSummary.total_available, + requested_tokens: tokensToAdd + }; + } + + const updatedLink = await this.jobService.addTokensToLink(linkId, tokensToAdd, user.id); + + return { + success: true, + link: updatedLink, + message: "Tokens added successfully" + }; + } catch (error: any) { + console.error('Error adding tokens to link:', error); + throw error; + } + } + + // Remove tokens from a job link + @Delete("/:id/links/:linkId/tokens") + @Security("bearerAuth") + @Summary("Remove tokens from a job link") + @(Returns(200).Description("Tokens removed")) + @(Returns(400).Description("Invalid amount")) + @(Returns(401).Description("Unauthorized")) + @(Returns(404).Description("Job not found")) + async removeTokensFromLink(@Req() req: any, @PathParams("id") id: string, @PathParams("linkId") linkId: string, @BodyParams() tokenData: any) { + try { + const user = await this.checkAuth(req); + console.log('Removing tokens from link:', linkId, 'for job:', id, 'by user:', user.email); + + // Verify job exists and user has access + const job = await this.jobService.getJobById(id); + if (!job) { + throw new NotFound("Job not found"); + } + + if (user.role === 'recruiter' && job.user_id !== user.id) { + throw new Unauthorized("You can only modify links for your own jobs"); + } + + const tokensToRemove = tokenData.tokens || 0; + if (tokensToRemove <= 0) { + return { + success: false, + error: "INVALID_AMOUNT", + message: "Please specify a valid number of tokens to remove." + }; + } + + const updatedLink = await this.jobService.removeTokensFromLink(linkId, tokensToRemove, user.id); + + return { + success: true, + link: updatedLink, + message: "Tokens removed successfully" + }; + } catch (error: any) { + console.error('Error removing tokens from link:', error); + throw error; + } + } + + // Delete a job link + @Delete("/:id/links/:linkId") + @Security("bearerAuth") + @Summary("Delete a job link") + @(Returns(200).Description("Link deleted; tokens returned if applicable")) + @(Returns(401).Description("Unauthorized")) + @(Returns(404).Description("Job not found")) + async deleteJobLink(@Req() req: any, @PathParams("id") id: string, @PathParams("linkId") linkId: string) { + try { + const user = await this.checkAuth(req); + console.log('Deleting job link:', linkId, 'for job:', id, 'by user:', user.email); + + // Verify job exists and user has access + const job = await this.jobService.getJobById(id); + if (!job) { + throw new NotFound("Job not found"); + } + + if (user.role === 'recruiter' && job.user_id !== user.id) { + throw new Unauthorized("You can only modify links for your own jobs"); + } + + const result = await this.jobService.deleteJobLink(linkId, user.id); + + return { + success: true, + message: "Job link deleted successfully", + tokensReturned: result.tokensReturned + }; + } catch (error: any) { + console.error('Error deleting job link:', error); + throw error; + } + } + + // Get job by interview link (public endpoint) + @Get("/interview/:linkId") + @Summary("Get job by interview link") + @Description("Public endpoint used by candidates to load interview context.") + @(Returns(200).Description("Job returned")) + @(Returns(404).Description("Interview link not found or expired")) + async getJobByLink(@PathParams("linkId") linkId: string) { + try { + console.log('Getting job by link ID:', linkId); + + const job = await this.jobService.getJobByLinkId(linkId); + + if (!job) { + throw new NotFound("Interview link not found or expired"); + } + + return { + success: true, + job: job + }; + } catch (error: any) { + console.error('Error getting job by link:', error); + throw error; + } + } + + // Submit interview responses + @Post("/interview/:linkId/submit") + @Summary("Submit interview responses") + @Description("Submits candidate answers; if not a test, consumes one token.") + @(Returns(200).Description("Submission acknowledged")) + @(Returns(404).Description("Interview link not found or expired")) + async submitInterview(@PathParams("linkId") linkId: string, @BodyParams() submissionData: any) { + try { + console.log('Submitting interview for link:', linkId); + + const job = await this.jobService.getJobByLinkId(linkId); + + if (!job) { + throw new NotFound("Interview link not found or expired"); + } + + // If it's not a test, save the interview + if (!submissionData.isTest) { + await this.jobService.submitInterview(linkId, submissionData.answers); + } + + return { + success: true, + message: submissionData.isTest ? "Test interview completed" : "Interview submitted successfully" + }; + } catch (error: any) { + console.error('Error submitting interview:', error); + throw error; + } + } + + // Log failed interview attempt (consent declined) + @Post("/interview/:linkId/failed") + @Summary("Log a failed interview attempt") + @Description("Records consent decline or early exit without consuming tokens.") + @(Returns(200).Description("Event recorded")) + @(Returns(404).Description("Interview link not found or expired")) + async logFailedAttempt(@PathParams("linkId") linkId: string) { + try { + console.log('Logging failed attempt for link:', linkId); + + const job = await this.jobService.getJobByLinkId(linkId); + + if (!job) { + throw new NotFound("Interview link not found or expired"); + } + + // Log the failed attempt (no token deduction) + await this.jobService.logFailedAttempt(linkId); + + return { + success: true, + message: "Failed attempt logged successfully" + }; + } catch (error: any) { + console.error('Error logging failed attempt:', error); + throw error; + } + } + + // Health check endpoint + @Get("/health") + @Summary("Health check") + @Description("Reports DB connectivity and service health") + @(Returns(200).Description("Healthy or unhealthy status returned")) + async healthCheck() { + try { + // Test database connection + const connection = await pool.getConnection(); + connection.release(); + + return { + status: "healthy", + database: "connected", + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + status: "unhealthy", + database: "disconnected", + error: (error as any).message, + timestamp: new Date().toISOString() + }; + } + } +} diff --git a/backend/src/controllers/rest/UserController.ts b/backend/src/controllers/rest/UserController.ts new file mode 100644 index 0000000..8fc0a00 --- /dev/null +++ b/backend/src/controllers/rest/UserController.ts @@ -0,0 +1,75 @@ +import { Controller } from "@tsed/di"; +import { Get, Summary, Description, Returns, Tags, Security } from "@tsed/schema"; +import { Req } from "@tsed/platform-http"; +import { Unauthorized } from "@tsed/exceptions"; +import jwt from "jsonwebtoken"; +import { UserService } from "../../services/UserService.js"; +import { TokenService } from "../../services/TokenService.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; + +@Controller("/user") +@Tags("Users") +export class UserController { + private userService = new UserService(); + private tokenService = new TokenService(); + + // Middleware to check if user is authenticated + private async checkAuth(req: any) { + const token = req.headers.authorization?.replace("Bearer ", ""); + + if (!token) { + throw new Unauthorized("No token provided"); + } + + try { + const decoded = jwt.verify(token, JWT_SECRET) as any; + const user = await this.userService.getUserById(decoded.userId); + + if (!user) { + throw new Unauthorized("User not found"); + } + + return user; + } catch (error) { + throw new Unauthorized("Invalid token"); + } + } + + // Get user token summary + @Get("/token-summary") + @Security("bearerAuth") + @Summary("Get token summary for current user") + @Description("Returns total tokens purchased and used by the authenticated user.") + @Returns(200).Description("Token summary returned") + @Returns(401).Description("Unauthorized") + async getTokenSummary(@Req() req: any) { + const user = await this.checkAuth(req); + return await this.tokenService.getUserTokenSummary(user.id); + } + + // Get user profile + @Get("/profile") + @Security("bearerAuth") + @Summary("Get current user profile") + @Description("Returns profile details for the authenticated user") + @Returns(200).Description("User profile returned") + @Returns(401).Description("Unauthorized") + async getProfile(@Req() req: any) { + const user = await this.checkAuth(req); + return { + id: user.id, + email: user.email, + first_name: user.first_name, + last_name: user.last_name, + role: user.role, + company_name: user.company_name, + avatar_url: user.avatar_url, + is_active: user.is_active, + last_login_at: user.last_login_at, + email_verified_at: user.email_verified_at, + created_at: user.created_at, + updated_at: user.updated_at + }; + } +} diff --git a/backend/src/controllers/rest/index.ts b/backend/src/controllers/rest/index.ts new file mode 100644 index 0000000..f902d2f --- /dev/null +++ b/backend/src/controllers/rest/index.ts @@ -0,0 +1,9 @@ +/** + * @file Automatically generated by @tsed/barrels. + */ +export * from "./AIController.js"; +export * from "./AdminController.js"; +export * from "./AuthController.js"; +export * from "./HelloWorldController.js"; +export * from "./JobController.js"; +export * from "./UserController.js"; diff --git a/backend/src/index.ts b/backend/src/index.ts new file mode 100644 index 0000000..5615448 --- /dev/null +++ b/backend/src/index.ts @@ -0,0 +1,36 @@ +import {$log} from "@tsed/logger"; +import { PlatformExpress } from "@tsed/platform-express"; +import {Server} from "./Server.js"; + +const SIG_EVENTS = [ + "beforeExit", + "SIGHUP", + "SIGINT", + "SIGQUIT", + "SIGILL", + "SIGTRAP", + "SIGABRT", + "SIGBUS", + "SIGFPE", + "SIGUSR1", + "SIGSEGV", + "SIGUSR2", + "SIGTERM" +]; + +try { + const platform = await PlatformExpress.bootstrap(Server); + await platform.listen(); + + SIG_EVENTS.forEach((evt) => process.on(evt, () => platform.stop())); + + ["uncaughtException", "unhandledRejection"].forEach((evt) => + process.on(evt, async (error) => { + $log.error({event: "SERVER_" + evt.toUpperCase(), message: error.message, stack: error.stack}); + await platform.stop(); + }) + ); +} catch (error) { + $log.error({event: "SERVER_BOOTSTRAP_ERROR", message: error.message, stack: error.stack}); +} + diff --git a/backend/src/middleware/adminAuth.ts b/backend/src/middleware/adminAuth.ts new file mode 100644 index 0000000..92d98ba --- /dev/null +++ b/backend/src/middleware/adminAuth.ts @@ -0,0 +1,51 @@ +import { Request, Response, NextFunction } from "express"; +import jwt from "jsonwebtoken"; +import { UserService } from "../services/UserService.js"; +import { Unauthorized } from "@tsed/exceptions"; + +const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; + +export interface AuthenticatedRequest extends Request { + user?: { + id: string; + email: string; + role: string; + first_name: string; + last_name: string; + }; +} + +export async function adminAuth(req: AuthenticatedRequest, res: Response, next: NextFunction) { + try { + const token = req.headers.authorization?.replace("Bearer ", ""); + + if (!token) { + throw new Unauthorized("No token provided"); + } + + const decoded = jwt.verify(token, JWT_SECRET) as any; + const userService = new UserService(); + const user = await userService.getUserById(decoded.userId); + + if (!user) { + throw new Unauthorized("User not found"); + } + + if (user.role !== 'admin') { + throw new Unauthorized("Admin access required"); + } + + // Add user info to request + req.user = { + id: user.id, + email: user.email, + role: user.role, + first_name: user.first_name, + last_name: user.last_name + }; + + next(); + } catch (error) { + next(new Unauthorized("Invalid token or insufficient permissions")); + } +} diff --git a/backend/src/models/User.ts b/backend/src/models/User.ts new file mode 100644 index 0000000..043f7a6 --- /dev/null +++ b/backend/src/models/User.ts @@ -0,0 +1,66 @@ +export interface User { + id: string; + email: string; + password_hash: string; + first_name: string; + last_name: string; + role: 'admin' | 'recruiter'; + company_name?: string; + avatar_url?: string; + is_active: boolean; + last_login_at?: Date; + email_verified_at?: Date; + created_at: Date; + updated_at: Date; + deleted_at?: Date; +} + +export type LoginRequest = { + email: string; + password: string; +} + +export type RegisterRequest = { + email: string; + password: string; + first_name: string; + last_name: string; + company_name?: string; +} + +export type CreateUserRequest = { + email: string; + password: string; + first_name: string; + last_name: string; + company_name?: string; + role?: 'admin' | 'recruiter'; +} + +export type UpdateUserRequest = { + first_name?: string; + last_name?: string; + company_name?: string; + avatar_url?: string; + is_active?: boolean; +} + +export type UserResponse = { + id: string; + email: string; + first_name: string; + last_name: string; + role: 'admin' | 'recruiter'; + company_name?: string; + avatar_url?: string; + is_active: boolean; + last_login_at?: Date; + email_verified_at?: Date; + created_at: Date; + updated_at: Date; +} + +export type LoginResponse = { + token: string; + user: UserResponse; +} \ No newline at end of file diff --git a/backend/src/services/AIService.ts b/backend/src/services/AIService.ts new file mode 100644 index 0000000..e3d05b9 --- /dev/null +++ b/backend/src/services/AIService.ts @@ -0,0 +1,294 @@ +import axios from 'axios'; +import { ChatbotService } from './ChatbotService.js'; + +export interface ChatMessage { + role: 'system' | 'user' | 'assistant'; + content: string; +} + +export interface ChatRequest { + model: string; + messages: ChatMessage[]; + temperature: number; +} + +export interface ChatChoice { + message: ChatMessage; +} + +export interface ChatResponse { + choices: ChatChoice[]; +} + +export class AIService { + private apiKey: string; + private model: string; + private baseUrl: string; + private relPath: string; + private temperature: number; + private chatbotService: ChatbotService; + + // Predefined models from your C# code + private static readonly PREDEFINED_MODELS: Record = { + 'dobby': 'sentientagi/dobby-mini-unhinged-plus-llama-3.1-8b', + 'dolphin': 'cognitivecomputations/dolphin-mixtral-8x22b', + 'dolphin_free': 'cognitivecomputations/dolphin3.0-mistral-24b:free', + 'gemma': 'google/gemma-3-12b-it', + 'gpt-4o-mini': 'openai/gpt-4o-mini', + 'gpt-4.1-nano': 'openai/gpt-4.1-nano', + 'qwen': 'qwen/qwen3-30b-a3b', + 'unslop': 'thedrummer/unslopnemo-12b', + 'euryale': 'sao10k/l3.3-euryale-70b', + 'wizard': 'microsoft/wizardlm-2-8x22b', + 'deepseek': 'deepseek/deepseek-chat-v3-0324' + }; + + constructor() { + this.apiKey = process.env.OPENROUTER_API_KEY || 'sk-or-REPLACE_ME'; + this.model = process.env.OPENROUTER_MODEL || 'gemma'; + this.baseUrl = process.env.OPENROUTER_BASE_URL || 'openrouter.ai'; + this.relPath = process.env.OPENROUTER_REL_PATH || '/api'; + this.temperature = parseFloat(process.env.OPENROUTER_TEMPERATURE || '0.7'); + this.chatbotService = new ChatbotService(); + + // Map predefined model names to full model names + if (AIService.PREDEFINED_MODELS[this.model]) { + this.model = AIService.PREDEFINED_MODELS[this.model]; + } + + console.log(`[DEBUG] AIService initialized:`); + console.log(`[DEBUG] - API Key: ${this.apiKey.substring(0, 10)}...`); + console.log(`[DEBUG] - Model: ${this.model}`); + console.log(`[DEBUG] - Base URL: ${this.baseUrl}`); + console.log(`[DEBUG] - Rel Path: ${this.relPath}`); + console.log(`[DEBUG] - Temperature: ${this.temperature}`); + console.log(`[DEBUG] - Chatbot Service: ${this.chatbotService ? 'Enabled' : 'Disabled'}`); + } + + async generateResponse(prompt: string, systemMessage?: string): Promise { + try { + const messages: ChatMessage[] = []; + + if (systemMessage) { + messages.push({ role: 'system', content: systemMessage }); + } + + messages.push({ role: 'user', content: prompt }); + + const payload: ChatRequest = { + model: this.model, + messages: messages, + temperature: this.temperature + }; + + const url = `https://${this.baseUrl}${this.relPath}/v1/chat/completions`; + + console.log(`[DEBUG] Sending to OpenRouter - Model: ${this.model}, URL: ${url}`); + console.log(`[DEBUG] Prompt length: ${prompt.length} characters`); + + const response = await axios.post(url, payload, { + headers: { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json' + } + }); + + const data = response.data as ChatResponse; + const aiResponse = data.choices?.[0]?.message?.content || null; + + console.log(`[DEBUG] OpenRouter Response: ${aiResponse}`); + + return aiResponse; + } catch (error) { + console.error('Error calling OpenRouter:', error); + if (error.response) { + console.error('Response status:', error.response.status); + console.error('Response data:', error.response.data); + } + return null; + } + } + + async generateResponseWithHistory( + userMessage: string, + conversationHistory: any[], + systemMessage?: string + ): Promise { + try { + const messages: ChatMessage[] = []; + + if (systemMessage) { + messages.push({ role: 'system', content: systemMessage }); + } + + // Add conversation history + conversationHistory.forEach(msg => { + if (msg.sender === 'candidate' || msg.sender === 'user') { + messages.push({ role: 'user', content: msg.message }); + } else if (msg.sender === 'ai' || msg.sender === 'assistant') { + messages.push({ role: 'assistant', content: msg.message }); + } + }); + + // Add current user message + messages.push({ role: 'user', content: userMessage }); + + const payload: ChatRequest = { + model: this.model, + messages: messages, + temperature: this.temperature + }; + + const url = `https://${this.baseUrl}${this.relPath}/v1/chat/completions`; + + console.log(`[DEBUG] Sending to OpenRouter with history - Model: ${this.model}`); + console.log(`[DEBUG] Messages count: ${messages.length}`); + + const response = await axios.post(url, payload, { + headers: { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json' + } + }); + + const data = response.data as ChatResponse; + const aiResponse = data.choices?.[0]?.message?.content || null; + + console.log(`[DEBUG] OpenRouter Response: ${aiResponse}`); + + return aiResponse; + } catch (error) { + console.error('Error calling OpenRouter with history:', error); + if (error.response) { + console.error('Response status:', error.response.status); + console.error('Response data:', error.response.data); + } + return null; + } + } + + /** + * Generate response using chatbot service with fallback to direct OpenRouter + */ + async generateResponseWithChatbot( + userMessage: string, + conversationHistory: any[], + systemMessage?: string, + job?: any, + candidateName?: string, + linkId?: string + ): Promise { + // Try chatbot service first + try { + const isHealthy = await this.chatbotService.isHealthy(); + if (isHealthy) { + console.log(`[DEBUG] Using chatbot service for response generation`); + + const response = await this.chatbotService.sendMessage({ + message: userMessage, + conversationHistory, + systemMessage, + job, + candidateName, + linkId + }); + + if (response) { + return response; + } + } + } catch (error) { + console.error('[ERROR] Chatbot service failed, falling back to direct OpenRouter:', error); + } + + // Fallback to direct OpenRouter + if (this.chatbotService.shouldUseFallback()) { + console.log(`[DEBUG] Falling back to direct OpenRouter`); + return await this.generateResponseWithHistory(userMessage, conversationHistory, systemMessage); + } + + return null; + } + + /** + * Initialize interview using chatbot service + */ + async initializeInterviewWithChatbot( + job: any, + candidateName: string, + linkId: string, + conversationHistory: any[] = [] + ): Promise { + try { + const isHealthy = await this.chatbotService.isHealthy(); + if (isHealthy) { + console.log(`[DEBUG] Using chatbot service for interview initialization`); + return await this.chatbotService.initializeInterview(job, candidateName, linkId, conversationHistory); + } + } catch (error) { + console.error('[ERROR] Chatbot service failed for interview initialization:', error); + } + + // Fallback to direct OpenRouter + if (this.chatbotService.shouldUseFallback()) { + console.log(`[DEBUG] Falling back to direct OpenRouter for interview initialization`); + const systemMessage = this.buildInterviewSystemMessage(job, candidateName, conversationHistory); + return await this.generateResponse(`The candidate's name is ${candidateName}. Please start the interview.`, systemMessage); + } + + return null; + } + + /** + * End interview using chatbot service + */ + async endInterviewWithChatbot(linkId: string): Promise { + try { + const isHealthy = await this.chatbotService.isHealthy(); + if (isHealthy) { + console.log(`[DEBUG] Using chatbot service for interview end`); + return await this.chatbotService.endInterview(linkId); + } + } catch (error) { + console.error('[ERROR] Chatbot service failed for interview end:', error); + } + + return false; + } + + /** + * Build interview system message + */ + private buildInterviewSystemMessage(job: any, candidateName: string, conversationHistory: any[] = []): string { + const skills = job.skills_required ? job.skills_required.join(', ') : 'various technical skills'; + const experience = job.experience_level.replace('_', ' '); + + // Build context from conversation history (mandatory question answers) + const conversationContext = conversationHistory + .map(msg => `${msg.sender === 'candidate' ? 'Candidate' : 'Interviewer'}: ${msg.message}`) + .join('\n'); + + return `You are an AI interview agent conducting an interview for the position: ${job.title} + +Job Description: ${job.description} +Requirements: ${job.requirements} +Required Skills: ${skills} +Experience Level: ${experience} +Location: ${job.location || 'Remote'} + +${conversationContext ? `Previous conversation (mandatory questions answered): +${conversationContext} + +Based on the candidate's answers to the mandatory questions above, you should now conduct a deeper interview.` : ''} + +Your task is to: +1. Greet the candidate warmly and professionally +2. Introduce yourself as their evaluation agent +3. ${conversationContext ? 'Acknowledge their previous answers and build upon them' : 'Explain that you\'ll be conducting a comprehensive interview'} +4. Ask them to tell you about themselves and their interest in this role +5. Keep your response conversational and engaging +6. Don't ask multiple questions at once - start with one open-ended question + +Respond in a friendly, professional tone. Keep it concise but welcoming.`; + } +} diff --git a/backend/src/services/AdminService.ts b/backend/src/services/AdminService.ts new file mode 100644 index 0000000..2c108c4 --- /dev/null +++ b/backend/src/services/AdminService.ts @@ -0,0 +1,776 @@ +import { pool } from '../config/database.js'; +import { $log } from '@tsed/logger'; +import bcrypt from 'bcryptjs'; +import { randomUUID } from 'crypto'; + +export class AdminService { + // System Statistics + async getSystemStatistics() { + const connection = await pool.getConnection(); + + try { + // Get basic counts + const [userStats] = await connection.execute(` + SELECT + COUNT(*) as total_users, + SUM(CASE WHEN is_active = TRUE THEN 1 ELSE 0 END) as active_users + FROM users + WHERE deleted_at IS NULL + `); + + const [jobStats] = await connection.execute(` + SELECT COUNT(*) as total_jobs + FROM jobs + WHERE deleted_at IS NULL + `); + + const [interviewStats] = await connection.execute(` + SELECT COUNT(*) as total_interviews + FROM interviews + WHERE status = 'completed' + `); + + const [tokenStats] = await connection.execute(` + SELECT + COALESCE(SUM(quantity), 0) as total_tokens_purchased, + COALESCE(SUM(tokens_used), 0) as total_tokens_used + FROM interview_tokens + `); + + const [revenueStats] = await connection.execute(` + SELECT COALESCE(SUM(amount), 0) as total_revenue + FROM payment_records + WHERE status = 'paid' + `); + + const userStatsData = Array.isArray(userStats) ? userStats[0] : userStats; + const jobStatsData = Array.isArray(jobStats) ? jobStats[0] : jobStats; + const interviewStatsData = Array.isArray(interviewStats) ? interviewStats[0] : interviewStats; + const tokenStatsData = Array.isArray(tokenStats) ? tokenStats[0] : tokenStats; + const revenueStatsData = Array.isArray(revenueStats) ? revenueStats[0] : revenueStats; + + return { + total_users: userStatsData?.total_users || 0, + active_users: userStatsData?.active_users || 0, + total_jobs: jobStatsData?.total_jobs || 0, + total_interviews: interviewStatsData?.total_interviews || 0, + total_tokens_purchased: tokenStatsData?.total_tokens_purchased || 0, + total_tokens_used: tokenStatsData?.total_tokens_used || 0, + total_revenue: revenueStatsData?.total_revenue || 0, + generated_at: new Date().toISOString() + }; + } catch (error) { + $log.error('Error getting system statistics:', error); + throw error; + } finally { + connection.release(); + } + } + + // User Management + async getAllUsers() { + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.execute(` + SELECT + id, email, first_name, last_name, role, company_name, + avatar_url, is_active, last_login_at, email_verified_at, + created_at, updated_at + FROM users + WHERE deleted_at IS NULL + ORDER BY created_at DESC + `); + + return Array.isArray(rows) ? rows : []; + } catch (error) { + $log.error('Error getting all users:', error); + throw error; + } finally { + connection.release(); + } + } + + async getUserById(id: string) { + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.execute(` + SELECT + id, email, first_name, last_name, role, company_name, + avatar_url, is_active, last_login_at, email_verified_at, + created_at, updated_at + FROM users + WHERE id = ? AND deleted_at IS NULL + `, [id]); + + if (Array.isArray(rows) && rows.length > 0) { + return rows[0]; + } + + return null; + } catch (error) { + $log.error('Error getting user by ID:', error); + throw error; + } finally { + connection.release(); + } + } + + async updateUser(id: string, userData: any) { + const connection = await pool.getConnection(); + + try { + const updateFields = []; + const values = []; + + if (userData.first_name) { + updateFields.push('first_name = ?'); + values.push(userData.first_name); + } + if (userData.last_name) { + updateFields.push('last_name = ?'); + values.push(userData.last_name); + } + if (userData.email) { + updateFields.push('email = ?'); + values.push(userData.email); + } + if (userData.role) { + updateFields.push('role = ?'); + values.push(userData.role); + } + if (userData.company_name !== undefined) { + updateFields.push('company_name = ?'); + values.push(userData.company_name); + } + if (userData.avatar_url !== undefined) { + updateFields.push('avatar_url = ?'); + values.push(userData.avatar_url); + } + if (userData.is_active !== undefined) { + updateFields.push('is_active = ?'); + values.push(userData.is_active); + } + + if (updateFields.length === 0) { + throw new Error('No fields to update'); + } + + updateFields.push('updated_at = NOW()'); + values.push(id); + + await connection.execute( + `UPDATE users SET ${updateFields.join(', ')} WHERE id = ? AND deleted_at IS NULL`, + values + ); + + return await this.getUserById(id); + } catch (error) { + $log.error('Error updating user:', error); + throw error; + } finally { + connection.release(); + } + } + + async toggleUserStatus(id: string) { + const connection = await pool.getConnection(); + + try { + // Get current status + const [rows] = await connection.execute( + 'SELECT is_active FROM users WHERE id = ? AND deleted_at IS NULL', + [id] + ); + + if (Array.isArray(rows) && rows.length === 0) { + throw new Error('User not found'); + } + + const currentStatus = Array.isArray(rows) ? rows[0] : rows; + const newStatus = !currentStatus.is_active; + + await connection.execute( + 'UPDATE users SET is_active = ?, updated_at = NOW() WHERE id = ? AND deleted_at IS NULL', + [newStatus, id] + ); + + return { success: true, new_status: newStatus }; + } catch (error) { + $log.error('Error toggling user status:', error); + throw error; + } finally { + connection.release(); + } + } + + async changeUserPassword(id: string, newPassword: string) { + const connection = await pool.getConnection(); + + try { + const password_hash = await bcrypt.hash(newPassword, 10); + + await connection.execute( + 'UPDATE users SET password_hash = ?, updated_at = NOW() WHERE id = ? AND deleted_at IS NULL', + [password_hash, id] + ); + + return { success: true }; + } catch (error) { + $log.error('Error changing user password:', error); + throw error; + } finally { + connection.release(); + } + } + + async createUser(userData: any) { + const connection = await pool.getConnection(); + + try { + // Check if user already exists + const [existingUsers] = await connection.execute( + 'SELECT id FROM users WHERE email = ? AND deleted_at IS NULL', + [userData.email] + ); + + if (Array.isArray(existingUsers) && existingUsers.length > 0) { + throw new Error('User with this email already exists'); + } + + // Hash password + const password_hash = await bcrypt.hash(userData.password, 10); + + // Generate UUID for user ID + const userId = randomUUID(); + + // Insert user + await connection.execute( + `INSERT INTO users (id, email, password_hash, first_name, last_name, role, company_name, is_active, email_verified_at, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW(), NOW())`, + [ + userId, + userData.email, + password_hash, + userData.first_name, + userData.last_name, + userData.role || 'recruiter', + userData.company_name || null, + true + ] + ); + + // Initialize usage tracking + await connection.execute( + 'INSERT INTO user_usage (user_id) VALUES (?)', + [userId] + ); + + return await this.getUserById(userId); + } catch (error) { + $log.error('Error creating user:', error); + throw error; + } finally { + connection.release(); + } + } + + // Job Management + async getAllJobs() { + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.execute(` + SELECT + j.*, + u.first_name, + u.last_name, + u.email, + u.company_name + FROM jobs j + LEFT JOIN users u ON j.user_id = u.id + WHERE j.deleted_at IS NULL + ORDER BY j.created_at DESC + `); + + return Array.isArray(rows) ? rows : []; + } catch (error) { + $log.error('Error getting all jobs:', error); + throw error; + } finally { + connection.release(); + } + } + + async getJobById(id: string) { + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.execute(` + SELECT + j.*, + u.first_name, + u.last_name, + u.email, + u.company_name + FROM jobs j + LEFT JOIN users u ON j.user_id = u.id + WHERE j.id = ? AND j.deleted_at IS NULL + `, [id]); + + if (Array.isArray(rows) && rows.length > 0) { + return rows[0]; + } + + return null; + } catch (error) { + $log.error('Error getting job by ID:', error); + throw error; + } finally { + connection.release(); + } + } + + async updateJobStatus(id: string, status: string) { + const connection = await pool.getConnection(); + + try { + await connection.execute( + 'UPDATE jobs SET status = ?, updated_at = NOW() WHERE id = ? AND deleted_at IS NULL', + [status, id] + ); + + return { success: true, new_status: status }; + } catch (error) { + $log.error('Error updating job status:', error); + throw error; + } finally { + connection.release(); + } + } + + async updateJob(id: string, jobData: any) { + const connection = await pool.getConnection(); + + try { + const updateFields = []; + const values = []; + + if (jobData.title) { + updateFields.push('title = ?'); + values.push(jobData.title); + } + if (jobData.description) { + updateFields.push('description = ?'); + values.push(jobData.description); + } + if (jobData.requirements) { + updateFields.push('requirements = ?'); + values.push(jobData.requirements); + } + if (jobData.skills_required) { + updateFields.push('skills_required = ?'); + values.push(JSON.stringify(jobData.skills_required)); + } + if (jobData.location) { + updateFields.push('location = ?'); + values.push(jobData.location); + } + if (jobData.employment_type) { + updateFields.push('employment_type = ?'); + values.push(jobData.employment_type); + } + if (jobData.experience_level) { + updateFields.push('experience_level = ?'); + values.push(jobData.experience_level); + } + if (jobData.salary_min !== undefined) { + updateFields.push('salary_min = ?'); + values.push(jobData.salary_min); + } + if (jobData.salary_max !== undefined) { + updateFields.push('salary_max = ?'); + values.push(jobData.salary_max); + } + if (jobData.currency) { + updateFields.push('currency = ?'); + values.push(jobData.currency); + } + if (jobData.status) { + updateFields.push('status = ?'); + values.push(jobData.status); + } + + if (updateFields.length === 0) { + throw new Error('No fields to update'); + } + + updateFields.push('updated_at = NOW()'); + values.push(id); + + await connection.execute( + `UPDATE jobs SET ${updateFields.join(', ')} WHERE id = ? AND deleted_at IS NULL`, + values + ); + + return await this.getJobById(id); + } catch (error) { + $log.error('Error updating job:', error); + throw error; + } finally { + connection.release(); + } + } + + // Token Management + async getUserTokenSummaries() { + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.execute(` + SELECT + u.id as user_id, + u.first_name, + u.last_name, + u.email, + COALESCE(SUM(it.quantity), 0) as total_purchased, + COALESCE(SUM(it.tokens_used), 0) as total_used, + COALESCE(SUM(it.tokens_remaining), 0) as total_available, + CASE + WHEN SUM(it.quantity) > 0 THEN ROUND((SUM(it.tokens_used) / SUM(it.quantity)) * 100, 2) + ELSE 0 + END as utilization_percentage + FROM users u + LEFT JOIN interview_tokens it ON u.id = it.user_id AND it.status = 'active' + WHERE u.deleted_at IS NULL + GROUP BY u.id, u.first_name, u.last_name, u.email + ORDER BY u.created_at DESC + `); + + return Array.isArray(rows) ? rows : []; + } catch (error) { + $log.error('Error getting user token summaries:', error); + throw error; + } finally { + connection.release(); + } + } + + async addTokensToUser(tokenData: any) { + const connection = await pool.getConnection(); + + try { + const { user_id, quantity, price_per_token } = tokenData; + const total_price = quantity * price_per_token; + const tokenId = randomUUID(); + + // Create token record + await connection.execute(` + INSERT INTO interview_tokens ( + id, user_id, token_type, quantity, price_per_token, + total_price, status, purchased_at, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, 'active', NOW(), NOW(), NOW()) + `, [ + tokenId, + user_id, + quantity === 1 ? 'single' : 'bulk', + quantity, + price_per_token, + total_price + ]); + + // No payment record needed for admin-granted tokens + + // Update user usage + await connection.execute(` + INSERT INTO user_usage (user_id, tokens_purchased) + VALUES (?, ?) + ON DUPLICATE KEY UPDATE tokens_purchased = tokens_purchased + ? + `, [user_id, quantity, quantity]); + + return { success: true, token_id: tokenId }; + } catch (error) { + $log.error('Error adding tokens to user:', error); + throw error; + } finally { + connection.release(); + } + } + + // Token Packages + async getTokenPackages() { + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.execute(` + SELECT * FROM token_packages + ORDER BY created_at DESC + `); + + return Array.isArray(rows) ? rows : []; + } catch (error) { + $log.error('Error getting token packages:', error); + throw error; + } finally { + connection.release(); + } + } + + async createTokenPackage(packageData: any) { + const connection = await pool.getConnection(); + + try { + const packageId = randomUUID(); + + await connection.execute(` + INSERT INTO token_packages ( + id, name, description, quantity, price_per_token, + total_price, discount_percentage, is_popular, is_active, + created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) + `, [ + packageId, + packageData.name, + packageData.description, + packageData.quantity, + packageData.price_per_token, + packageData.total_price, + packageData.discount_percentage || 0, + packageData.is_popular || false, + packageData.is_active !== false + ]); + + return { success: true, package_id: packageId }; + } catch (error) { + $log.error('Error creating token package:', error); + throw error; + } finally { + connection.release(); + } + } + + async updateTokenPackage(id: string, packageData: any) { + const connection = await pool.getConnection(); + + try { + const updateFields = []; + const values = []; + + if (packageData.name) { + updateFields.push('name = ?'); + values.push(packageData.name); + } + if (packageData.description) { + updateFields.push('description = ?'); + values.push(packageData.description); + } + if (packageData.quantity) { + updateFields.push('quantity = ?'); + values.push(packageData.quantity); + } + if (packageData.price_per_token) { + updateFields.push('price_per_token = ?'); + values.push(packageData.price_per_token); + } + if (packageData.total_price) { + updateFields.push('total_price = ?'); + values.push(packageData.total_price); + } + if (packageData.discount_percentage !== undefined) { + updateFields.push('discount_percentage = ?'); + values.push(packageData.discount_percentage); + } + if (packageData.is_popular !== undefined) { + updateFields.push('is_popular = ?'); + values.push(packageData.is_popular); + } + if (packageData.is_active !== undefined) { + updateFields.push('is_active = ?'); + values.push(packageData.is_active); + } + + if (updateFields.length === 0) { + throw new Error('No fields to update'); + } + + updateFields.push('updated_at = NOW()'); + values.push(id); + + await connection.execute( + `UPDATE token_packages SET ${updateFields.join(', ')} WHERE id = ?`, + values + ); + + return { success: true }; + } catch (error) { + $log.error('Error updating token package:', error); + throw error; + } finally { + connection.release(); + } + } + + async toggleTokenPackageStatus(id: string) { + const connection = await pool.getConnection(); + + try { + // Get current status + const [rows] = await connection.execute( + 'SELECT is_active FROM token_packages WHERE id = ?', + [id] + ); + + if (Array.isArray(rows) && rows.length === 0) { + throw new Error('Token package not found'); + } + + const currentStatus = Array.isArray(rows) ? rows[0] : rows; + const newStatus = !currentStatus.is_active; + + await connection.execute( + 'UPDATE token_packages SET is_active = ?, updated_at = NOW() WHERE id = ?', + [newStatus, id] + ); + + return { success: true, new_status: newStatus }; + } catch (error) { + $log.error('Error toggling token package status:', error); + throw error; + } finally { + connection.release(); + } + } + + async deleteTokenPackage(id: string) { + const connection = await pool.getConnection(); + + try { + await connection.execute( + 'DELETE FROM token_packages WHERE id = ?', + [id] + ); + + return { success: true }; + } catch (error) { + $log.error('Error deleting token package:', error); + throw error; + } finally { + connection.release(); + } + } + + // Interview Management + async getAllInterviews() { + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.execute(` + SELECT + i.*, + u.first_name, + u.last_name, + u.email, + j.title as job_title + FROM interviews i + LEFT JOIN users u ON i.user_id = u.id + LEFT JOIN jobs j ON i.job_id = j.id + ORDER BY i.created_at DESC + `); + + return Array.isArray(rows) ? rows : []; + } catch (error) { + $log.error('Error getting all interviews:', error); + throw error; + } finally { + connection.release(); + } + } + + async getInterviewById(id: string) { + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.execute(` + SELECT + i.*, + u.first_name, + u.last_name, + u.email, + j.title as job_title + FROM interviews i + LEFT JOIN users u ON i.user_id = u.id + LEFT JOIN jobs j ON i.job_id = j.id + WHERE i.id = ? + `, [id]); + + if (Array.isArray(rows) && rows.length > 0) { + return rows[0]; + } + + return null; + } catch (error) { + $log.error('Error getting interview by ID:', error); + throw error; + } finally { + connection.release(); + } + } + + // Payment Records + async getPaymentRecords() { + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.execute(` + SELECT + pr.*, + u.first_name, + u.last_name, + u.email, + tp.name as package_name + FROM payment_records pr + LEFT JOIN users u ON pr.user_id = u.id + LEFT JOIN token_packages tp ON pr.token_package_id = tp.id + ORDER BY pr.created_at DESC + `); + + return Array.isArray(rows) ? rows : []; + } catch (error) { + $log.error('Error getting payment records:', error); + throw error; + } finally { + connection.release(); + } + } + + async getPaymentById(id: string) { + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.execute(` + SELECT + pr.*, + u.first_name, + u.last_name, + u.email, + tp.name as package_name + FROM payment_records pr + LEFT JOIN users u ON pr.user_id = u.id + LEFT JOIN token_packages tp ON pr.token_package_id = tp.id + WHERE pr.id = ? + `, [id]); + + if (Array.isArray(rows) && rows.length > 0) { + return rows[0]; + } + + return null; + } catch (error) { + $log.error('Error getting payment by ID:', error); + throw error; + } finally { + connection.release(); + } + } +} diff --git a/backend/src/services/ChatbotService.ts b/backend/src/services/ChatbotService.ts new file mode 100644 index 0000000..adb82f9 --- /dev/null +++ b/backend/src/services/ChatbotService.ts @@ -0,0 +1,170 @@ +import axios, { AxiosInstance, AxiosResponse } from 'axios'; + +export interface ChatbotRequest { + message: string; + conversationHistory?: any[]; + job?: any; + candidateName?: string; + linkId?: string; + systemMessage?: string; +} + +export interface ChatbotResponse { + ok: boolean; + reply?: string; + error?: string; +} + +export interface ChatbotHealthResponse { + status: string; + timestamp: string; +} + +export class ChatbotService { + private client: AxiosInstance; + private baseUrl: string; + private timeout: number; + private fallbackEnabled: boolean; + + constructor() { + this.baseUrl = process.env.CHATBOT_SERVICE_URL || 'http://chatbot:80'; + this.timeout = parseInt(process.env.CHATBOT_SERVICE_TIMEOUT || '30000'); + this.fallbackEnabled = process.env.CHATBOT_FALLBACK_ENABLED === 'true'; + + this.client = axios.create({ + baseURL: this.baseUrl, + timeout: this.timeout, + headers: { + 'Content-Type': 'application/json', + }, + }); + + console.log(`[DEBUG] ChatbotService initialized:`); + console.log(`[DEBUG] - Base URL: ${this.baseUrl}`); + console.log(`[DEBUG] - Timeout: ${this.timeout}ms`); + console.log(`[DEBUG] - Fallback Enabled: ${this.fallbackEnabled}`); + } + + /** + * Check if chatbot service is healthy + */ + async isHealthy(): Promise { + try { + const response = await this.client.get('/api/health'); + return response.status === 200; + } catch (error) { + console.error('[ERROR] Chatbot service health check failed:', error); + return false; + } + } + + /** + * Send a chat message to the chatbot service + */ + async sendMessage(request: ChatbotRequest): Promise { + try { + console.log(`[DEBUG] Sending message to chatbot service: ${request.message.substring(0, 100)}...`); + + const response: AxiosResponse = await this.client.post('/api/chat', { + message: request.message, + conversationHistory: request.conversationHistory, + job: request.job, + candidateName: request.candidateName, + linkId: request.linkId, + systemMessage: request.systemMessage + }); + + if (response.data.ok && response.data.reply) { + console.log(`[DEBUG] Chatbot service response received: ${response.data.reply.substring(0, 100)}...`); + return response.data.reply; + } else { + console.error('[ERROR] Chatbot service returned error:', response.data.error); + return null; + } + } catch (error) { + console.error('[ERROR] Chatbot service request failed:', error); + if (error.response) { + console.error('[ERROR] Response status:', error.response.status); + console.error('[ERROR] Response data:', error.response.data); + } + return null; + } + } + + /** + * Initialize an interview with the chatbot service + */ + async initializeInterview(job: any, candidateName: string, linkId: string, conversationHistory: any[] = []): Promise { + try { + console.log(`[DEBUG] Initializing interview with chatbot service for ${candidateName}`); + + const response: AxiosResponse = await this.client.post('/api/interview/start', { + job, + candidateName, + linkId, + conversationHistory + }); + + if (response.data.ok && response.data.reply) { + console.log(`[DEBUG] Interview initialized successfully`); + return response.data.reply; + } else { + console.error('[ERROR] Failed to initialize interview:', response.data.error); + return null; + } + } catch (error) { + console.error('[ERROR] Interview initialization failed:', error); + return null; + } + } + + /** + * End an interview with the chatbot service + */ + async endInterview(linkId: string): Promise { + try { + console.log(`[DEBUG] Ending interview with chatbot service for linkId: ${linkId}`); + + const response: AxiosResponse = await this.client.post('/api/interview/end', { + linkId + }); + + if (response.data.ok) { + console.log(`[DEBUG] Interview ended successfully`); + return true; + } else { + console.error('[ERROR] Failed to end interview:', response.data.error); + return false; + } + } catch (error) { + console.error('[ERROR] Interview end failed:', error); + return false; + } + } + + /** + * Get interview status from chatbot service + */ + async getInterviewStatus(linkId: string): Promise { + try { + const response: AxiosResponse = await this.client.get(`/api/interview/status/${linkId}`); + + if (response.data.ok) { + return response.data; + } else { + console.error('[ERROR] Failed to get interview status:', response.data.error); + return null; + } + } catch (error) { + console.error('[ERROR] Get interview status failed:', error); + return null; + } + } + + /** + * Check if fallback to direct AI service should be used + */ + shouldUseFallback(): boolean { + return this.fallbackEnabled; + } +} diff --git a/backend/src/services/JobService.ts b/backend/src/services/JobService.ts new file mode 100644 index 0000000..aa95ee2 --- /dev/null +++ b/backend/src/services/JobService.ts @@ -0,0 +1,1070 @@ +import { pool } from '../config/database.js'; +import { $log } from '@tsed/logger'; +import { randomUUID } from 'crypto'; + +export interface Job { + id: string; + user_id: string; + title: string; + description: string; + requirements: string; + skills_required: string[]; + location: string; + employment_type: string; + experience_level: string; + salary_min?: number; + salary_max?: number; + currency: string; + status: string; + evaluation_criteria: any; + interview_questions: string[]; + interview_style?: 'technical' | 'personal' | 'balanced'; + application_deadline?: string; + icon?: string; + created_at: string; + updated_at: string; + deleted_at?: string; +} + +export interface CreateJobRequest { + title: string; + description: string; + requirements: string; + skills_required: string[]; + location: string; + employment_type: string; + experience_level: string; + salary_min?: number; + salary_max?: number; + currency?: string; + evaluation_criteria?: any; + interview_questions?: any; + application_deadline?: string; + icon?: string; +} + +export interface UpdateJobRequest { + title?: string; + description?: string; + requirements?: string; + skills_required?: string[]; + location?: string; + employment_type?: string; + experience_level?: string; + salary_min?: number; + salary_max?: number; + currency?: string; + status?: string; + evaluation_criteria?: any; + interview_questions?: string[]; + interview_style?: 'technical' | 'personal' | 'balanced'; + application_deadline?: string; +} + +export interface JobLink { + id: string; + job_id: string; + url_slug: string; + tokens_available: number; + tokens_used?: number; + created_at: string; + updated_at: string; +} + +export class JobService { + // Helper function to safely parse JSON fields + private parseJsonField(field: any, defaultValue: any) { + try { + if (typeof field === 'string') { + return JSON.parse(field); + } + return field || defaultValue; + } catch (error) { + console.warn(`Failed to parse JSON field: ${field}, using default value`); + return defaultValue; + } + } + + async updateJob(jobId: string, updates: UpdateJobRequest): Promise { + const connection = await pool.getConnection(); + try { + console.log('JobService.updateJob called with:', { jobId, updates }); + + // Build dynamic SET clause + const fields: string[] = []; + const values: any[] = []; + + if (updates.title !== undefined) { fields.push('title = ?'); values.push(updates.title); } + if (updates.description !== undefined) { fields.push('description = ?'); values.push(updates.description); } + if (updates.requirements !== undefined) { fields.push('requirements = ?'); values.push(updates.requirements); } + if (updates.skills_required !== undefined) { + const skillsArray = Array.isArray(updates.skills_required) ? updates.skills_required : []; + fields.push('skills_required = ?'); values.push(JSON.stringify(skillsArray)); + } + if (updates.location !== undefined) { fields.push('location = ?'); values.push(updates.location); } + if (updates.employment_type !== undefined) { fields.push('employment_type = ?'); values.push(updates.employment_type); } + if (updates.experience_level !== undefined) { fields.push('experience_level = ?'); values.push(updates.experience_level); } + if (updates.salary_min !== undefined) { fields.push('salary_min = ?'); values.push(updates.salary_min); } + if (updates.salary_max !== undefined) { fields.push('salary_max = ?'); values.push(updates.salary_max); } + if (updates.currency !== undefined) { fields.push('currency = ?'); values.push(updates.currency); } + if (updates.status !== undefined) { fields.push('status = ?'); values.push(updates.status); } + if (updates.evaluation_criteria !== undefined) { fields.push('evaluation_criteria = ?'); values.push(JSON.stringify(updates.evaluation_criteria ?? null)); } + if (updates.interview_questions !== undefined) { + const questionsArray = Array.isArray(updates.interview_questions) ? updates.interview_questions : []; + fields.push('interview_questions = ?'); + values.push(JSON.stringify(questionsArray)); + } + if (updates.interview_style !== undefined) { + console.log('Adding interview_style field:', updates.interview_style); + fields.push('interview_style = ?'); + values.push(updates.interview_style); + } + if (updates.application_deadline !== undefined) { fields.push('application_deadline = ?'); values.push(updates.application_deadline || null); } + + console.log('Fields to update:', fields); + console.log('Values:', values); + + if (fields.length === 0) { + console.log('No fields to update, returning existing job'); + const job = await this.getJobById(jobId); + return job as Job | null; + } + + fields.push('updated_at = NOW()'); + + const sql = `UPDATE jobs SET ${fields.join(', ')} WHERE id = ?`; + values.push(jobId); + await connection.execute(sql, values); + + const updated = await this.getJobById(jobId); + return updated as Job | null; + } finally { + connection.release(); + } + } + + async updateJobStatus(jobId: string, status: string): Promise { + const connection = await pool.getConnection(); + try { + await connection.execute( + 'UPDATE jobs SET status = ?, updated_at = NOW() WHERE id = ?', + [status, jobId] + ); + const updated = await this.getJobById(jobId); + return updated as Job | null; + } finally { + connection.release(); + } + } + + // Helper function to safely get tokens_used (backward compatibility) + private getTokensUsed(link: any): number { + return link.tokens_used || 0; + } + + async createJob(userId: string, jobData: CreateJobRequest): Promise { + const connection = await pool.getConnection(); + + try { + console.log('JobService: Creating job for user:', userId); + console.log('JobService: Job data:', jobData); + + const jobId = randomUUID(); + + // Ensure skills_required is always an array + const skillsArray = Array.isArray(jobData.skills_required) ? jobData.skills_required : []; + + const result = await connection.execute(` + INSERT INTO jobs ( + id, user_id, title, description, requirements, skills_required, + location, employment_type, experience_level, salary_min, salary_max, + currency, status, evaluation_criteria, interview_questions, + application_deadline, icon, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) + `, [ + jobId, + userId, + jobData.title, + jobData.description, + jobData.requirements, + JSON.stringify(skillsArray), + jobData.location || null, + jobData.employment_type || 'full_time', + jobData.experience_level || 'mid', + jobData.salary_min || null, + jobData.salary_max || null, + jobData.currency || 'USD', + 'draft', + JSON.stringify(jobData.evaluation_criteria || {}), + JSON.stringify(jobData.interview_questions || {}), + jobData.application_deadline || null, + jobData.icon || 'briefcase' + ]); + + console.log('JobService: Job inserted with ID:', jobId); + + const createdJob = await this.getJobById(jobId); + if (!createdJob) { + throw new Error('Failed to retrieve created job'); + } + + console.log('JobService: Job created successfully:', createdJob.id); + return createdJob; + } catch (error) { + console.error('JobService: Error creating job:', error); + $log.error('Error creating job:', error); + throw error; + } finally { + connection.release(); + } + } + + async getJobById(id: string): Promise { + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.execute( + 'SELECT * FROM jobs WHERE id = ? AND deleted_at IS NULL', + [id] + ); + + if (Array.isArray(rows) && rows.length > 0) { + const job = rows[0] as any; + console.log('JobService: Raw job data from DB:', { + id: job.id, + skills_required: job.skills_required, + skills_type: typeof job.skills_required, + evaluation_criteria: job.evaluation_criteria, + interview_questions: job.interview_questions + }); + + return { + ...job, + skills_required: this.parseJsonField(job.skills_required, []), + evaluation_criteria: this.parseJsonField(job.evaluation_criteria, {}), + interview_questions: this.parseJsonField(job.interview_questions, []), + interview_style: job.interview_style || 'balanced' + }; + } + + return null; + } catch (error) { + $log.error('Error getting job by ID:', error); + throw error; + } finally { + connection.release(); + } + } + + async getJobsByUserId(userId: string): Promise { + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.execute( + `SELECT + j.*, + DATEDIFF(NOW(), j.created_at) AS running_days, + COUNT(DISTINCT i.id) AS total_interviews, + SUM(CASE WHEN i.status = 'completed' THEN 1 ELSE 0 END) AS interviews_completed, + COUNT(DISTINCT c.id) AS applications + FROM jobs j + LEFT JOIN interviews i ON i.job_id = j.id + LEFT JOIN candidates c ON c.job_id = j.id + WHERE j.user_id = ? AND j.deleted_at IS NULL + GROUP BY j.id + ORDER BY j.created_at DESC`, + [userId] + ); + + if (Array.isArray(rows)) { + return rows.map((job: any) => { + const parsed = { + ...job, + skills_required: this.parseJsonField(job.skills_required, []), + evaluation_criteria: this.parseJsonField(job.evaluation_criteria, {}), + interview_questions: this.parseJsonField(job.interview_questions, []), + interview_style: job.interview_style || 'balanced' + } as any; + // Derive available_interviews if not provided + parsed.total_interviews = Number(job.total_interviews || 0); + parsed.interviews_completed = Number(job.interviews_completed || 0); + parsed.available_interviews = Math.max( + 0, + parsed.total_interviews - parsed.interviews_completed + ); + parsed.applications = Number(job.applications || 0); + parsed.running_days = Number(job.running_days || 0); + return parsed; + }); + } + + return []; + } catch (error) { + $log.error('Error getting jobs by user ID (with metrics). Falling back to basic query:', error); + // Fallback: simple query without metrics so frontend still works + const [rows] = await connection.execute( + 'SELECT * FROM jobs WHERE user_id = ? AND deleted_at IS NULL ORDER BY created_at DESC', + [userId] + ); + const jobs = Array.isArray(rows) ? rows : []; + return jobs.map((job: any) => ({ + ...job, + skills_required: this.parseJsonField(job.skills_required, []), + evaluation_criteria: this.parseJsonField(job.evaluation_criteria, {}), + interview_questions: this.parseJsonField(job.interview_questions, []), + interview_style: job.interview_style || 'balanced', + total_interviews: 0, + interviews_completed: 0, + available_interviews: 0, + applications: 0, + running_days: 0 + })); + } finally { + connection.release(); + } + } + + async updateJob(id: string, jobData: UpdateJobRequest): Promise { + const connection = await pool.getConnection(); + + try { + const updateFields = []; + const values = []; + + if (jobData.title) { + updateFields.push('title = ?'); + values.push(jobData.title); + } + if (jobData.description) { + updateFields.push('description = ?'); + values.push(jobData.description); + } + if (jobData.requirements) { + updateFields.push('requirements = ?'); + values.push(jobData.requirements); + } + if (jobData.skills_required) { + updateFields.push('skills_required = ?'); + values.push(JSON.stringify(jobData.skills_required)); + } + if (jobData.location) { + updateFields.push('location = ?'); + values.push(jobData.location); + } + if (jobData.employment_type) { + updateFields.push('employment_type = ?'); + values.push(jobData.employment_type); + } + if (jobData.experience_level) { + updateFields.push('experience_level = ?'); + values.push(jobData.experience_level); + } + if (jobData.salary_min !== undefined) { + updateFields.push('salary_min = ?'); + values.push(jobData.salary_min); + } + if (jobData.salary_max !== undefined) { + updateFields.push('salary_max = ?'); + values.push(jobData.salary_max); + } + if (jobData.currency) { + updateFields.push('currency = ?'); + values.push(jobData.currency); + } + if (jobData.status !== undefined) { + updateFields.push('status = ?'); + values.push(jobData.status); + } + if (jobData.evaluation_criteria !== undefined) { + updateFields.push('evaluation_criteria = ?'); + values.push(JSON.stringify(jobData.evaluation_criteria ?? null)); + } + if (jobData.interview_questions !== undefined) { + updateFields.push('interview_questions = ?'); + const questionsArray = Array.isArray(jobData.interview_questions) ? jobData.interview_questions : []; + values.push(JSON.stringify(questionsArray)); + } + if ((jobData as any).interview_style !== undefined) { + updateFields.push('interview_style = ?'); + values.push((jobData as any).interview_style); + } + if (jobData.application_deadline !== undefined) { + updateFields.push('application_deadline = ?'); + values.push(jobData.application_deadline); + } + + if (updateFields.length === 0) { + throw new Error('No fields to update'); + } + + updateFields.push('updated_at = NOW()'); + values.push(id); + + await connection.execute( + `UPDATE jobs SET ${updateFields.join(', ')} WHERE id = ? AND deleted_at IS NULL`, + values + ); + + return await this.getJobById(id); + } catch (error) { + $log.error('Error updating job:', error); + throw error; + } finally { + connection.release(); + } + } + + async deleteJob(id: string): Promise { + const connection = await pool.getConnection(); + + try { + await connection.execute( + 'UPDATE jobs SET deleted_at = NOW(), updated_at = NOW() WHERE id = ? AND deleted_at IS NULL', + [id] + ); + } catch (error) { + $log.error('Error deleting job:', error); + throw error; + } finally { + connection.release(); + } + } + + async getAllJobs(): Promise { + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.execute( + `SELECT + j.*, + DATEDIFF(NOW(), j.created_at) AS running_days, + COUNT(DISTINCT i.id) AS total_interviews, + SUM(CASE WHEN i.status = 'completed' THEN 1 ELSE 0 END) AS interviews_completed, + COUNT(DISTINCT c.id) AS applications + FROM jobs j + LEFT JOIN interviews i ON i.job_id = j.id + LEFT JOIN candidates c ON c.job_id = j.id + WHERE j.deleted_at IS NULL + GROUP BY j.id + ORDER BY j.created_at DESC` + ); + + if (Array.isArray(rows)) { + return rows.map((job: any) => { + const parsed = { + ...job, + skills_required: this.parseJsonField(job.skills_required, []), + evaluation_criteria: this.parseJsonField(job.evaluation_criteria, {}), + interview_questions: this.parseJsonField(job.interview_questions, []), + interview_style: job.interview_style || 'balanced' + } as any; + parsed.total_interviews = Number(job.total_interviews || 0); + parsed.interviews_completed = Number(job.interviews_completed || 0); + parsed.available_interviews = Math.max( + 0, + parsed.total_interviews - parsed.interviews_completed + ); + parsed.applications = Number(job.applications || 0); + parsed.running_days = Number(job.running_days || 0); + return parsed; + }); + } + + return []; + } catch (error) { + $log.error('Error getting all jobs (with metrics). Falling back to basic query:', error); + // Fallback: simple query without metrics + const [rows] = await connection.execute( + 'SELECT * FROM jobs WHERE deleted_at IS NULL ORDER BY created_at DESC' + ); + const jobs = Array.isArray(rows) ? rows : []; + return jobs.map((job: any) => ({ + ...job, + skills_required: this.parseJsonField(job.skills_required, []), + evaluation_criteria: this.parseJsonField(job.evaluation_criteria, {}), + interview_questions: this.parseJsonField(job.interview_questions, []), + interview_style: job.interview_style || 'balanced', + total_interviews: 0, + interviews_completed: 0, + available_interviews: 0, + applications: 0, + running_days: 0 + })); + } finally { + connection.release(); + } + } + + async getJobLinks(jobId: string): Promise { + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.execute( + 'SELECT * FROM job_links WHERE job_id = ? ORDER BY created_at DESC', + [jobId] + ); + + if (Array.isArray(rows)) { + return rows.map(link => ({ + ...link, + tokens_used: this.getTokensUsed(link) + })) as JobLink[]; + } + return []; + } catch (error) { + $log.error('Error getting job links:', error); + return []; + } finally { + connection.release(); + } + } + + async createJobLink(jobId: string, tokensAvailable: number = 0): Promise { + const connection = await pool.getConnection(); + + try { + const linkId = randomUUID(); + const urlSlug = randomUUID().substring(0, 8); // Short URL slug + + await connection.execute( + 'INSERT INTO job_links (id, job_id, url_slug, tokens_available, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())', + [linkId, jobId, urlSlug, tokensAvailable] + ); + + const [rows] = await connection.execute( + 'SELECT * FROM job_links WHERE id = ?', + [linkId] + ); + + if (Array.isArray(rows) && rows.length > 0) { + const link = rows[0]; + return { + ...link, + tokens_used: this.getTokensUsed(link) + } as JobLink; + } + + throw new Error('Failed to create job link'); + } catch (error) { + $log.error('Error creating job link:', error); + throw error; + } finally { + connection.release(); + } + } + + async addTokensToLink(linkId: string, tokensToAdd: number, userId: string): Promise { + const connection = await pool.getConnection(); + + try { + // First get current tokens + const [rows] = await connection.execute( + 'SELECT * FROM job_links WHERE id = ?', + [linkId] + ); + + if (!Array.isArray(rows) || rows.length === 0) { + throw new Error('Job link not found'); + } + + const currentLink = rows[0] as JobLink; + const newTokenCount = currentLink.tokens_available + tokensToAdd; + + // Update tokens + await connection.execute( + 'UPDATE job_links SET tokens_available = ?, updated_at = NOW() WHERE id = ?', + [newTokenCount, linkId] + ); + + // Consume user's tokens from interview_tokens table + // Find active token packages and consume tokens from them + const [tokenRows] = await connection.execute( + 'SELECT id, tokens_remaining FROM interview_tokens WHERE user_id = ? AND status = "active" AND tokens_remaining > 0 ORDER BY created_at ASC', + [userId] + ); + + let remainingToConsume = tokensToAdd; + for (const tokenRow of Array.isArray(tokenRows) ? tokenRows : []) { + if (remainingToConsume <= 0) break; + + const availableInThisPackage = Math.min(tokenRow.tokens_remaining, remainingToConsume); + const newRemaining = tokenRow.tokens_remaining - availableInThisPackage; + const newUsed = tokenRow.tokens_remaining - newRemaining; + + await connection.execute( + 'UPDATE interview_tokens SET tokens_used = tokens_used + ?, updated_at = NOW() WHERE id = ?', + [availableInThisPackage, tokenRow.id] + ); + + remainingToConsume -= availableInThisPackage; + } + + if (remainingToConsume > 0) { + throw new Error(`Insufficient tokens available. Only ${tokensToAdd - remainingToConsume} tokens could be consumed.`); + } + + // Return updated link + const [updatedRows] = await connection.execute( + 'SELECT * FROM job_links WHERE id = ?', + [linkId] + ); + + if (Array.isArray(updatedRows) && updatedRows.length > 0) { + const link = updatedRows[0]; + return { + ...link, + tokens_used: this.getTokensUsed(link) + } as JobLink; + } + + throw new Error('Failed to update job link'); + } catch (error) { + $log.error('Error adding tokens to link:', error); + throw error; + } finally { + connection.release(); + } + } + + async removeTokensFromLink(linkId: string, tokensToRemove: number, userId: string): Promise { + const connection = await pool.getConnection(); + + try { + // First get current tokens + const [rows] = await connection.execute( + 'SELECT * FROM job_links WHERE id = ?', + [linkId] + ); + + if (!Array.isArray(rows) || rows.length === 0) { + throw new Error('Job link not found'); + } + + const currentLink = rows[0] as JobLink; + + // Check if we have enough tokens to remove + if (currentLink.tokens_available < tokensToRemove) { + throw new Error(`Cannot remove ${tokensToRemove} tokens. Link only has ${currentLink.tokens_available} tokens available.`); + } + + const newTokenCount = currentLink.tokens_available - tokensToRemove; + + // Update tokens + await connection.execute( + 'UPDATE job_links SET tokens_available = ?, updated_at = NOW() WHERE id = ?', + [newTokenCount, linkId] + ); + + // Return tokens to user by increasing tokens_remaining in interview_tokens + // Find the most recently used token package to return tokens to + const [tokenRows] = await connection.execute( + 'SELECT id, tokens_remaining, quantity, tokens_used FROM interview_tokens WHERE user_id = ? AND status = "active" ORDER BY updated_at DESC', + [userId] + ); + + console.log('Token removal - Found token packages:', tokenRows); + + if (Array.isArray(tokenRows) && tokenRows.length > 0) { + // Return tokens to the most recently used package + const tokenRow = tokenRows[0]; + console.log('Token removal - Returning to package:', tokenRow); + + const newRemaining = Math.min(tokenRow.tokens_remaining + tokensToRemove, tokenRow.quantity); + const tokensToReturn = newRemaining - tokenRow.tokens_remaining; + const newUsed = Math.max(0, tokenRow.tokens_used - tokensToReturn); + + console.log('Token removal - New values:', { newRemaining, tokensToReturn, newUsed }); + + await connection.execute( + 'UPDATE interview_tokens SET tokens_used = ?, updated_at = NOW() WHERE id = ?', + [newUsed, tokenRow.id] + ); + + console.log('Token removal - Updated package successfully'); + } else { + console.log('Token removal - No active token packages found for user'); + } + + // Return updated link + const [updatedRows] = await connection.execute( + 'SELECT * FROM job_links WHERE id = ?', + [linkId] + ); + + if (Array.isArray(updatedRows) && updatedRows.length > 0) { + const link = updatedRows[0]; + return { + ...link, + tokens_used: this.getTokensUsed(link) + } as JobLink; + } + + throw new Error('Failed to update job link'); + } catch (error) { + $log.error('Error removing tokens from link:', error); + throw error; + } finally { + connection.release(); + } + } + + async deleteJobLink(linkId: string, userId: string): Promise<{ tokensReturned: number }> { + const connection = await pool.getConnection(); + + try { + // First get the link to check how many tokens it has + const [rows] = await connection.execute( + 'SELECT * FROM job_links WHERE id = ?', + [linkId] + ); + + if (!Array.isArray(rows) || rows.length === 0) { + throw new Error('Job link not found'); + } + + const link = rows[0] as JobLink; + const tokensToReturn = link.tokens_available; + + console.log('Deleting job link with', tokensToReturn, 'tokens to return'); + + // Return all tokens to user if there are any + if (tokensToReturn > 0) { + // Find the most recently used token package to return tokens to + const [tokenRows] = await connection.execute( + 'SELECT id, tokens_remaining, quantity, tokens_used FROM interview_tokens WHERE user_id = ? AND status = "active" ORDER BY updated_at DESC', + [userId] + ); + + console.log('Link deletion - Found token packages:', tokenRows); + + if (Array.isArray(tokenRows) && tokenRows.length > 0) { + // Return tokens to the most recently used package + const tokenRow = tokenRows[0]; + console.log('Link deletion - Returning to package:', tokenRow); + + const newRemaining = Math.min(tokenRow.tokens_remaining + tokensToReturn, tokenRow.quantity); + const tokensToReturnToPackage = newRemaining - tokenRow.tokens_remaining; + const newUsed = Math.max(0, tokenRow.tokens_used - tokensToReturnToPackage); + + console.log('Link deletion - New values:', { newRemaining, tokensToReturnToPackage, newUsed }); + + await connection.execute( + 'UPDATE interview_tokens SET tokens_used = ?, updated_at = NOW() WHERE id = ?', + [newUsed, tokenRow.id] + ); + + console.log('Link deletion - Updated package successfully'); + } else { + console.log('Link deletion - No active token packages found for user'); + } + } + + // Delete the job link + await connection.execute( + 'DELETE FROM job_links WHERE id = ?', + [linkId] + ); + + console.log('Link deletion - Job link deleted successfully'); + + return { tokensReturned: tokensToReturn }; + } catch (error) { + $log.error('Error deleting job link:', error); + throw error; + } finally { + connection.release(); + } + } + + async getJobByLinkId(linkId: string): Promise { + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.execute( + `SELECT j.* FROM jobs j + INNER JOIN job_links jl ON j.id = jl.job_id + WHERE jl.url_slug = ? AND j.deleted_at IS NULL`, + [linkId] + ); + + if (Array.isArray(rows) && rows.length > 0) { + const job = rows[0] as any; + return { + ...job, + skills_required: this.parseJsonField(job.skills_required, []), + evaluation_criteria: this.parseJsonField(job.evaluation_criteria, {}), + interview_questions: this.parseJsonField(job.interview_questions, []), + interview_style: job.interview_style || 'balanced' + }; + } + + return null; + } catch (error) { + $log.error('Error getting job by link ID:', error); + throw error; + } finally { + connection.release(); + } + } + + async submitInterview(linkId: string, answers: any): Promise { + const connection = await pool.getConnection(); + + try { + // Get the job ID from the link + const [linkRows] = await connection.execute( + 'SELECT job_id FROM job_links WHERE url_slug = ?', + [linkId] + ); + + if (!Array.isArray(linkRows) || linkRows.length === 0) { + throw new Error('Interview link not found'); + } + + const jobId = (linkRows[0] as any).job_id; + + // Create a candidate record + const candidateId = randomUUID(); + await connection.execute( + 'INSERT INTO candidates (id, job_id, email, name, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())', + [candidateId, jobId, 'interview@candidate.com', 'Interview Candidate'] + ); + + // Create an interview record + const interviewId = randomUUID(); + await connection.execute( + 'INSERT INTO interviews (id, job_id, candidate_id, status, answers, created_at, updated_at) VALUES (?, ?, ?, ?, ?, NOW(), NOW())', + [interviewId, jobId, candidateId, 'completed', JSON.stringify(answers)] + ); + + // Decrement tokens from the link + await connection.execute( + 'UPDATE job_links SET tokens_available = tokens_available - 1, updated_at = NOW() WHERE url_slug = ?', + [linkId] + ); + + } catch (error) { + $log.error('Error submitting interview:', error); + throw error; + } finally { + connection.release(); + } + } + + async logInterviewEvent(linkId: string, eventType: string, data: any): Promise { + const connection = await pool.getConnection(); + + try { + // Get the job ID from the link + const [linkRows] = await connection.execute( + 'SELECT job_id FROM job_links WHERE url_slug = ?', + [linkId] + ); + + if (!Array.isArray(linkRows) || linkRows.length === 0) { + throw new Error('Interview link not found'); + } + + const jobId = (linkRows[0] as any).job_id; + + // Log the event + await connection.execute( + 'INSERT INTO interview_events (id, job_id, link_id, event_type, event_data, created_at) VALUES (?, ?, ?, ?, ?, NOW())', + [randomUUID(), jobId, linkId, eventType, JSON.stringify(data)] + ); + + } catch (error) { + $log.error('Error logging interview event:', error); + throw error; + } finally { + connection.release(); + } + } + + async logFailedAttempt(linkId: string): Promise { + const connection = await pool.getConnection(); + + try { + // Get the job ID from the link + const [linkRows] = await connection.execute( + 'SELECT job_id FROM job_links WHERE url_slug = ?', + [linkId] + ); + + if (!Array.isArray(linkRows) || linkRows.length === 0) { + throw new Error('Interview link not found'); + } + + const jobId = (linkRows[0] as any).job_id; + + // Log the failed attempt (no token deduction) + await connection.execute( + 'INSERT INTO interview_events (id, job_id, link_id, event_type, event_data, created_at) VALUES (?, ?, ?, ?, ?, NOW())', + [randomUUID(), jobId, linkId, 'consent_declined', JSON.stringify({ + timestamp: new Date().toISOString(), + reason: 'User declined consent' + })] + ); + + } catch (error) { + $log.error('Error logging failed attempt:', error); + throw error; + } finally { + connection.release(); + } + } + + async getOrCreateInterview(linkId: string, candidateName: string, isTestMode: boolean = false): Promise { + const connection = await pool.getConnection(); + + try { + // Get the job ID and user ID from the link and job + const [linkRows] = await connection.execute( + `SELECT jl.job_id, j.user_id + FROM job_links jl + INNER JOIN jobs j ON jl.job_id = j.id + WHERE jl.url_slug = ?`, + [linkId] + ); + + if (!Array.isArray(linkRows) || linkRows.length === 0) { + throw new Error('Interview link not found'); + } + + const jobId = (linkRows[0] as any).job_id; + const userId = (linkRows[0] as any).user_id; + + // For test mode, just return a mock interview ID without saving to database + if (isTestMode) { + return `test-interview-${linkId}-${Date.now()}`; + } + + // Check if interview already exists for this link + const [existingInterviews] = await connection.execute( + 'SELECT id FROM interviews WHERE job_id = ? AND token = ?', + [jobId, linkId] + ); + + if (Array.isArray(existingInterviews) && existingInterviews.length > 0) { + return (existingInterviews[0] as any).id; + } + + // Create new interview + const interviewId = randomUUID(); + const candidateId = randomUUID(); + + // Create candidate record with unique email + const candidateEmail = `interview-${candidateId}@candidate.com`; + await connection.execute( + 'INSERT INTO candidates (id, user_id, job_id, email, first_name, last_name, status, applied_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW(), NOW())', + [candidateId, userId, jobId, candidateEmail, candidateName.split(' ')[0] || candidateName, candidateName.split(' ').slice(1).join(' ') || '', 'interviewing'] + ); + + // Create interview record + await connection.execute( + 'INSERT INTO interviews (id, user_id, candidate_id, job_id, token, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, NOW(), NOW())', + [interviewId, userId, candidateId, jobId, linkId, 'in_progress'] + ); + + return interviewId; + } catch (error) { + $log.error('Error getting or creating interview:', error); + throw error; + } finally { + connection.release(); + } + } + + async saveConversationMessage(interviewId: string, linkId: string, sender: 'candidate' | 'ai', message: string, isTestMode: boolean = false): Promise { + // Skip database writes in test mode + if (isTestMode) { + console.log(`[TEST MODE] Would save message: ${sender}: ${message.substring(0, 50)}...`); + return; + } + + const connection = await pool.getConnection(); + + try { + await connection.execute( + 'INSERT INTO conversation_messages (id, interview_id, link_id, sender, message, created_at) VALUES (?, ?, ?, ?, ?, NOW())', + [randomUUID(), interviewId, linkId, sender, message] + ); + } catch (error) { + $log.error('Error saving conversation message:', error); + throw error; + } finally { + connection.release(); + } + } + + async getConversationHistory(interviewId: string): Promise { + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.execute( + 'SELECT sender, message, created_at FROM conversation_messages WHERE interview_id = ? ORDER BY created_at ASC', + [interviewId] + ); + + return Array.isArray(rows) ? rows.map((row: any) => ({ + sender: row.sender, + message: row.message, + timestamp: row.created_at + })) : []; + } catch (error) { + $log.error('Error getting conversation history:', error); + throw error; + } finally { + connection.release(); + } + } + + async getInterviewIdByLink(linkId: string): Promise { + const connection = await pool.getConnection(); + + try { + const [linkRows] = await connection.execute( + 'SELECT job_id FROM job_links WHERE url_slug = ?', + [linkId] + ); + + if (!Array.isArray(linkRows) || linkRows.length === 0) { + return null; + } + + const jobId = (linkRows[0] as any).job_id; + + const [interviewRows] = await connection.execute( + 'SELECT id FROM interviews WHERE job_id = ? ORDER BY created_at DESC LIMIT 1', + [jobId] + ); + + if (Array.isArray(interviewRows) && interviewRows.length > 0) { + return (interviewRows[0] as any).id; + } + + return null; + } catch (error) { + $log.error('Error getting interview ID by link:', error); + throw error; + } finally { + connection.release(); + } + } + + async completeInterview(interviewId: string): Promise { + const connection = await pool.getConnection(); + + try { + await connection.execute( + 'UPDATE interviews SET status = ?, completed_at = NOW(), updated_at = NOW() WHERE id = ?', + ['completed', interviewId] + ); + } catch (error) { + $log.error('Error completing interview:', error); + throw error; + } finally { + connection.release(); + } + } +} diff --git a/backend/src/services/TokenService.ts b/backend/src/services/TokenService.ts new file mode 100644 index 0000000..f08ae10 --- /dev/null +++ b/backend/src/services/TokenService.ts @@ -0,0 +1,443 @@ +import { pool } from '../config/database.js'; +import { $log } from '@tsed/logger'; +import { randomUUID } from 'crypto'; + +export interface InterviewToken { + id: string; + user_id: string; + token_type: 'single' | 'bulk'; + quantity: number; + price_per_token: number; + total_price: number; + tokens_used: number; + tokens_remaining: number; + status: 'active' | 'exhausted' | 'expired'; + expires_at?: string; + purchased_at: string; + created_at: string; + updated_at: string; +} + +export interface TokenPackage { + id: string; + name: string; + description: string; + quantity: number; + price_per_token: number; + total_price: number; + discount_percentage: number; + is_popular: boolean; + is_active: boolean; + created_at: string; + updated_at: string; +} + +export interface CreateTokenPackageRequest { + name: string; + description: string; + quantity: number; + price_per_token: number; + total_price: number; + discount_percentage?: number; + is_popular?: boolean; + is_active?: boolean; +} + +export interface UpdateTokenPackageRequest { + name?: string; + description?: string; + quantity?: number; + price_per_token?: number; + total_price?: number; + discount_percentage?: number; + is_popular?: boolean; + is_active?: boolean; +} + +export class TokenService { + // Token Packages + async getTokenPackages(): Promise { + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.execute( + 'SELECT * FROM token_packages ORDER BY created_at DESC' + ); + + return Array.isArray(rows) ? rows as TokenPackage[] : []; + } catch (error) { + $log.error('Error getting token packages:', error); + throw error; + } finally { + connection.release(); + } + } + + async getTokenPackageById(id: string): Promise { + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.execute( + 'SELECT * FROM token_packages WHERE id = ?', + [id] + ); + + if (Array.isArray(rows) && rows.length > 0) { + return rows[0] as TokenPackage; + } + + return null; + } catch (error) { + $log.error('Error getting token package by ID:', error); + throw error; + } finally { + connection.release(); + } + } + + async createTokenPackage(packageData: CreateTokenPackageRequest): Promise { + const connection = await pool.getConnection(); + + try { + const packageId = randomUUID(); + + await connection.execute(` + INSERT INTO token_packages ( + id, name, description, quantity, price_per_token, + total_price, discount_percentage, is_popular, is_active, + created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) + `, [ + packageId, + packageData.name, + packageData.description, + packageData.quantity, + packageData.price_per_token, + packageData.total_price, + packageData.discount_percentage || 0, + packageData.is_popular || false, + packageData.is_active !== false + ]); + + return await this.getTokenPackageById(packageId) as TokenPackage; + } catch (error) { + $log.error('Error creating token package:', error); + throw error; + } finally { + connection.release(); + } + } + + async updateTokenPackage(id: string, packageData: UpdateTokenPackageRequest): Promise { + const connection = await pool.getConnection(); + + try { + const updateFields = []; + const values = []; + + if (packageData.name) { + updateFields.push('name = ?'); + values.push(packageData.name); + } + if (packageData.description) { + updateFields.push('description = ?'); + values.push(packageData.description); + } + if (packageData.quantity) { + updateFields.push('quantity = ?'); + values.push(packageData.quantity); + } + if (packageData.price_per_token) { + updateFields.push('price_per_token = ?'); + values.push(packageData.price_per_token); + } + if (packageData.total_price) { + updateFields.push('total_price = ?'); + values.push(packageData.total_price); + } + if (packageData.discount_percentage !== undefined) { + updateFields.push('discount_percentage = ?'); + values.push(packageData.discount_percentage); + } + if (packageData.is_popular !== undefined) { + updateFields.push('is_popular = ?'); + values.push(packageData.is_popular); + } + if (packageData.is_active !== undefined) { + updateFields.push('is_active = ?'); + values.push(packageData.is_active); + } + + if (updateFields.length === 0) { + throw new Error('No fields to update'); + } + + updateFields.push('updated_at = NOW()'); + values.push(id); + + await connection.execute( + `UPDATE token_packages SET ${updateFields.join(', ')} WHERE id = ?`, + values + ); + + return await this.getTokenPackageById(id); + } catch (error) { + $log.error('Error updating token package:', error); + throw error; + } finally { + connection.release(); + } + } + + async deleteTokenPackage(id: string): Promise { + const connection = await pool.getConnection(); + + try { + await connection.execute( + 'DELETE FROM token_packages WHERE id = ?', + [id] + ); + } catch (error) { + $log.error('Error deleting token package:', error); + throw error; + } finally { + connection.release(); + } + } + + async toggleTokenPackageStatus(id: string): Promise<{ success: boolean; new_status: boolean }> { + const connection = await pool.getConnection(); + + try { + // Get current status + const [rows] = await connection.execute( + 'SELECT is_active FROM token_packages WHERE id = ?', + [id] + ); + + if (Array.isArray(rows) && rows.length === 0) { + throw new Error('Token package not found'); + } + + const currentStatus = Array.isArray(rows) ? rows[0] : rows; + const newStatus = !currentStatus.is_active; + + await connection.execute( + 'UPDATE token_packages SET is_active = ?, updated_at = NOW() WHERE id = ?', + [newStatus, id] + ); + + return { success: true, new_status: newStatus }; + } catch (error) { + $log.error('Error toggling token package status:', error); + throw error; + } finally { + connection.release(); + } + } + + // Interview Tokens + async getTokensByUserId(userId: string): Promise { + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.execute( + 'SELECT * FROM interview_tokens WHERE user_id = ? ORDER BY created_at DESC', + [userId] + ); + + return Array.isArray(rows) ? rows as InterviewToken[] : []; + } catch (error) { + $log.error('Error getting tokens by user ID:', error); + throw error; + } finally { + connection.release(); + } + } + + async getTokenById(id: string): Promise { + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.execute( + 'SELECT * FROM interview_tokens WHERE id = ?', + [id] + ); + + if (Array.isArray(rows) && rows.length > 0) { + return rows[0] as InterviewToken; + } + + return null; + } catch (error) { + $log.error('Error getting token by ID:', error); + throw error; + } finally { + connection.release(); + } + } + + async addTokensToUser(userId: string, quantity: number, pricePerToken: number): Promise { + const connection = await pool.getConnection(); + + try { + const totalPrice = quantity * pricePerToken; + const tokenId = randomUUID(); + + // Create token record + await connection.execute(` + INSERT INTO interview_tokens ( + id, user_id, token_type, quantity, price_per_token, + total_price, status, purchased_at, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, 'active', NOW(), NOW(), NOW()) + `, [ + tokenId, + userId, + quantity === 1 ? 'single' : 'bulk', + quantity, + pricePerToken, + totalPrice + ]); + + // No payment record needed for admin-granted tokens + + // Update user usage + await connection.execute(` + INSERT INTO user_usage (user_id, tokens_purchased) + VALUES (?, ?) + ON DUPLICATE KEY UPDATE tokens_purchased = tokens_purchased + ? + `, [userId, quantity, quantity]); + + return await this.getTokenById(tokenId) as InterviewToken; + } catch (error) { + $log.error('Error adding tokens to user:', error); + throw error; + } finally { + connection.release(); + } + } + + async useToken(tokenId: string): Promise { + const connection = await pool.getConnection(); + + try { + // Get current token status + const token = await this.getTokenById(tokenId); + if (!token || token.status !== 'active') { + return false; + } + + // Check if token has remaining uses + if (token.tokens_remaining <= 0) { + return false; + } + + // Update token usage + const newUsedCount = token.tokens_used + 1; + const newStatus = newUsedCount >= token.quantity ? 'exhausted' : 'active'; + + await connection.execute(` + UPDATE interview_tokens + SET tokens_used = ?, status = ?, updated_at = NOW() + WHERE id = ? + `, [newUsedCount, newStatus, tokenId]); + + // Update user usage + await connection.execute(` + INSERT INTO user_usage (user_id, tokens_used) + VALUES (?, 1) + ON DUPLICATE KEY UPDATE tokens_used = tokens_used + 1 + `, [token.user_id]); + + return true; + } catch (error) { + $log.error('Error using token:', error); + throw error; + } finally { + connection.release(); + } + } + + async getUserTokenSummary(userId: string): Promise<{ + total_purchased: number; + total_used: number; + total_available: number; + utilization_percentage: number; + }> { + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.execute(` + SELECT + COALESCE(SUM(quantity), 0) as total_purchased, + COALESCE(SUM(tokens_used), 0) as total_used, + COALESCE(SUM(tokens_remaining), 0) as total_available + FROM interview_tokens + WHERE user_id = ? AND status = 'active' + `, [userId]); + + const data = Array.isArray(rows) ? rows[0] : rows; + const totalPurchased = data?.total_purchased || 0; + const totalUsed = data?.total_used || 0; + const totalAvailable = data?.total_available || 0; + + const utilizationPercentage = totalPurchased > 0 + ? Math.round((totalUsed / totalPurchased) * 100) + : 0; + + return { + total_purchased: totalPurchased, + total_used: totalUsed, + total_available: totalAvailable, + utilization_percentage: utilizationPercentage + }; + } catch (error) { + $log.error('Error getting user token summary:', error); + throw error; + } finally { + connection.release(); + } + } + + async getAllUserTokenSummaries(): Promise> { + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.execute(` + SELECT + u.id as user_id, + u.first_name, + u.last_name, + u.email, + COALESCE(SUM(it.quantity), 0) as total_purchased, + COALESCE(SUM(it.tokens_used), 0) as total_used, + COALESCE(SUM(it.tokens_remaining), 0) as total_available, + CASE + WHEN SUM(it.quantity) > 0 THEN ROUND((SUM(it.tokens_used) / SUM(it.quantity)) * 100, 2) + ELSE 0 + END as utilization_percentage + FROM users u + LEFT JOIN interview_tokens it ON u.id = it.user_id AND it.status = 'active' + WHERE u.deleted_at IS NULL + GROUP BY u.id, u.first_name, u.last_name, u.email + ORDER BY u.created_at DESC + `); + + return Array.isArray(rows) ? rows : []; + } catch (error) { + $log.error('Error getting all user token summaries:', error); + throw error; + } finally { + connection.release(); + } + } +} diff --git a/backend/src/services/UserService.ts b/backend/src/services/UserService.ts new file mode 100644 index 0000000..bd48f6c --- /dev/null +++ b/backend/src/services/UserService.ts @@ -0,0 +1,223 @@ +import { pool } from '../config/database.js'; +import { User, CreateUserRequest, UpdateUserRequest, UserResponse } from '../models/User.js'; +import { $log } from '@tsed/logger'; +import bcrypt from 'bcryptjs'; +import { randomUUID } from 'crypto'; + +export class UserService { + async createUser(userData: CreateUserRequest): Promise { + const connection = await pool.getConnection(); + + try { + // Check if user already exists + const [existingUsers] = await connection.execute( + 'SELECT id FROM users WHERE email = ? AND deleted_at IS NULL', + [userData.email] + ); + + if (Array.isArray(existingUsers) && existingUsers.length > 0) { + throw new Error('User with this email already exists'); + } + + // Hash password + const password_hash = await bcrypt.hash(userData.password, 10); + + // Generate UUID for user ID + const userId = randomUUID(); + + // Insert user + const [result] = await connection.execute( + `INSERT INTO users (id, email, password_hash, first_name, last_name, role, company_name, is_active, email_verified_at, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW(), NOW())`, + [ + userId, + userData.email, + password_hash, + userData.first_name, + userData.last_name, + userData.role || 'recruiter', + userData.company_name || null, + true + ] + ); + + // Get the created user + const user = await this.getUserById(userId); + if (!user) { + throw new Error('Failed to create user'); + } + + return this.mapUserToResponse(user); + } catch (error) { + $log.error('Error creating user:', error); + throw error; + } finally { + connection.release(); + } + } + + async getUserByEmail(email: string): Promise { + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.execute( + 'SELECT * FROM users WHERE email = ? AND deleted_at IS NULL', + [email] + ); + + if (Array.isArray(rows) && rows.length > 0) { + return rows[0] as User; + } + + return null; + } catch (error) { + $log.error('Error getting user by email:', error); + throw error; + } finally { + connection.release(); + } + } + + async getUserById(id: string): Promise { + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.execute( + 'SELECT * FROM users WHERE id = ? AND deleted_at IS NULL', + [id] + ); + + if (Array.isArray(rows) && rows.length > 0) { + return rows[0] as User; + } + + return null; + } catch (error) { + $log.error('Error getting user by ID:', error); + throw error; + } finally { + connection.release(); + } + } + + async updateUser(id: string, userData: UpdateUserRequest): Promise { + const connection = await pool.getConnection(); + + try { + const updateFields = []; + const values = []; + + if (userData.first_name) { + updateFields.push('first_name = ?'); + values.push(userData.first_name); + } + if (userData.last_name) { + updateFields.push('last_name = ?'); + values.push(userData.last_name); + } + if (userData.company_name !== undefined) { + updateFields.push('company_name = ?'); + values.push(userData.company_name); + } + if (userData.avatar_url !== undefined) { + updateFields.push('avatar_url = ?'); + values.push(userData.avatar_url); + } + if (userData.is_active !== undefined) { + updateFields.push('is_active = ?'); + values.push(userData.is_active); + } + + if (updateFields.length === 0) { + throw new Error('No fields to update'); + } + + updateFields.push('updated_at = NOW()'); + values.push(id); + + await connection.execute( + `UPDATE users SET ${updateFields.join(', ')} WHERE id = ? AND deleted_at IS NULL`, + values + ); + + const user = await this.getUserById(id); + return user ? this.mapUserToResponse(user) : null; + } catch (error) { + $log.error('Error updating user:', error); + throw error; + } finally { + connection.release(); + } + } + + async updateLastLogin(id: string): Promise { + const connection = await pool.getConnection(); + + try { + await connection.execute( + 'UPDATE users SET last_login_at = NOW(), updated_at = NOW() WHERE id = ? AND deleted_at IS NULL', + [id] + ); + } catch (error) { + $log.error('Error updating last login:', error); + throw error; + } finally { + connection.release(); + } + } + + async verifyPassword(user: User, password: string): Promise { + return await bcrypt.compare(password, user.password_hash); + } + + async changePassword(id: string, newPassword: string): Promise { + const connection = await pool.getConnection(); + + try { + const password_hash = await bcrypt.hash(newPassword, 10); + + await connection.execute( + 'UPDATE users SET password_hash = ?, updated_at = NOW() WHERE id = ? AND deleted_at IS NULL', + [password_hash, id] + ); + } catch (error) { + $log.error('Error changing password:', error); + throw error; + } finally { + connection.release(); + } + } + + async softDeleteUser(id: string): Promise { + const connection = await pool.getConnection(); + + try { + await connection.execute( + 'UPDATE users SET deleted_at = NOW(), is_active = FALSE, updated_at = NOW() WHERE id = ? AND deleted_at IS NULL', + [id] + ); + } catch (error) { + $log.error('Error soft deleting user:', error); + throw error; + } finally { + connection.release(); + } + } + + private mapUserToResponse(user: User): UserResponse { + return { + id: user.id, + email: user.email, + first_name: user.first_name, + last_name: user.last_name, + role: user.role, + company_name: user.company_name, + avatar_url: user.avatar_url, + is_active: user.is_active, + last_login_at: user.last_login_at, + email_verified_at: user.email_verified_at, + created_at: user.created_at, + updated_at: user.updated_at + }; + } +} diff --git a/backend/src/types/admin.ts b/backend/src/types/admin.ts new file mode 100644 index 0000000..f4b615d --- /dev/null +++ b/backend/src/types/admin.ts @@ -0,0 +1,171 @@ +// Admin-specific types + +export interface SystemStatistics { + total_users: number; + active_users: number; + total_jobs: number; + total_interviews: number; + total_tokens_purchased: number; + total_tokens_used: number; + total_revenue: number; + generated_at: string; +} + +export interface UserWithStats { + id: string; + email: string; + first_name: string; + last_name: string; + role: 'admin' | 'recruiter'; + company_name?: string; + avatar_url?: string; + is_active: boolean; + last_login_at?: string; + email_verified_at?: string; + created_at: string; + updated_at: string; +} + +export interface JobWithUser { + id: string; + user_id: string; + title: string; + description: string; + requirements: string; + skills_required: string[]; + location: string; + employment_type: string; + experience_level: string; + salary_min?: number; + salary_max?: number; + currency: string; + status: string; + evaluation_criteria: any; + interview_questions: any; + application_deadline?: string; + created_at: string; + updated_at: string; + first_name?: string; + last_name?: string; + email?: string; + company_name?: string; +} + +export interface UserTokenSummary { + user_id: string; + first_name: string; + last_name: string; + email: string; + total_purchased: number; + total_used: number; + total_available: number; + utilization_percentage: number; +} + +export interface TokenPackage { + id: string; + name: string; + description: string; + quantity: number; + price_per_token: number; + total_price: number; + discount_percentage: number; + is_popular: boolean; + is_active: boolean; + created_at: string; + updated_at: string; +} + +export interface AddTokensRequest { + user_id: string; + quantity: number; + price_per_token: number; + total_price: number; +} + +export interface CreateUserRequest { + email: string; + password: string; + first_name: string; + last_name: string; + role: 'admin' | 'recruiter'; + company_name?: string; +} + +export interface UpdateUserRequest { + first_name?: string; + last_name?: string; + email?: string; + role?: 'admin' | 'recruiter'; + company_name?: string; + avatar_url?: string; + is_active?: boolean; +} + +export interface CreateTokenPackageRequest { + name: string; + description: string; + quantity: number; + price_per_token: number; + total_price: number; + discount_percentage?: number; + is_popular?: boolean; + is_active?: boolean; +} + +export interface UpdateTokenPackageRequest { + name?: string; + description?: string; + quantity?: number; + price_per_token?: number; + total_price?: number; + discount_percentage?: number; + is_popular?: boolean; + is_active?: boolean; +} + +export interface InterviewWithDetails { + id: string; + user_id: string; + candidate_id: string; + job_id: string; + token: string; + status: string; + started_at?: string; + completed_at?: string; + duration_minutes: number; + ai_questions: any; + candidate_responses: any; + ai_evaluation: any; + overall_score?: number; + technical_score?: number; + communication_score?: number; + culture_fit_score?: number; + ai_feedback?: string; + created_at: string; + updated_at: string; + first_name?: string; + last_name?: string; + email?: string; + job_title?: string; +} + +export interface PaymentRecord { + id: string; + user_id: string; + interview_token_id?: string; + token_package_id?: string; + amount: number; + currency: string; + status: string; + payment_method?: string; + payment_reference?: string; + invoice_url?: string; + paid_at?: string; + created_at: string; + updated_at: string; + first_name?: string; + last_name?: string; + email?: string; + package_name?: string; +} diff --git a/backend/src/types/auth.ts b/backend/src/types/auth.ts new file mode 100644 index 0000000..6b70549 --- /dev/null +++ b/backend/src/types/auth.ts @@ -0,0 +1,51 @@ +// Authentication types that will be preserved in JavaScript compilation + +export const LoginRequestSchema = { + email: String, + password: String +}; + +export const RegisterRequestSchema = { + email: String, + password: String, + first_name: String, + last_name: String, + company_name: String +}; + +export const CreateUserRequestSchema = { + email: String, + password: String, + first_name: String, + last_name: String, + company_name: String, + role: String +}; + +export const UpdateUserRequestSchema = { + first_name: String, + last_name: String, + company_name: String, + avatar_url: String, + is_active: Boolean +}; + +export const UserResponseSchema = { + id: String, + email: String, + first_name: String, + last_name: String, + role: String, + company_name: String, + avatar_url: String, + is_active: Boolean, + last_login_at: Date, + email_verified_at: Date, + created_at: Date, + updated_at: Date +}; + +export const LoginResponseSchema = { + token: String, + user: UserResponseSchema +}; diff --git a/backend/tsconfig.base.json b/backend/tsconfig.base.json new file mode 100644 index 0000000..17a1ccf --- /dev/null +++ b/backend/tsconfig.base.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "target": "ESNext", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "moduleResolution": "NodeNext", + "downlevelIteration": false, + "isolatedModules": false, + "suppressImplicitAnyIndexErrors": false, + "noImplicitAny": true, + "strictNullChecks": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "useDefineForClassFields": false, + "importHelpers": true, + "resolveJsonModule": true, + "newLine": "LF", + "skipLibCheck": true, + "lib": ["ESNext", "esnext.asynciterable"], + "declaration": false, + "noResolve": false, + "preserveConstEnums": true, + "sourceMap": true, + "noEmit": true + } +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..61a3f4f --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "noEmit": true + }, + "include": [], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/backend/tsconfig.node.json b/backend/tsconfig.node.json new file mode 100644 index 0000000..78a2b7f --- /dev/null +++ b/backend/tsconfig.node.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "baseUrl": "." + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "src/**/*.spec.ts", + "dist", + "node_modules", + "**/helpers/*Fixture.ts", + "**/__mock__/**", + "coverage" + ], + "linterOptions": { + "exclude": [] + } +} diff --git a/backend/update-admin-password.sql b/backend/update-admin-password.sql new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/backend/update-admin-password.sql @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/backend/views/index.ejs b/backend/views/index.ejs new file mode 100644 index 0000000..b1b8efa --- /dev/null +++ b/backend/views/index.ejs @@ -0,0 +1,95 @@ + + + + + Swagger UI + + + + + + <% if (cssPath) { %> + + <% } %> + + + +
+ + + + + +<% if (jsPath) { %> + +<% } %> + + + + diff --git a/backend/views/swagger.ejs b/backend/views/swagger.ejs new file mode 100644 index 0000000..2274db6 --- /dev/null +++ b/backend/views/swagger.ejs @@ -0,0 +1,100 @@ + + + + + + + + client + + + + +
+
+ + + +
+
+ + + diff --git a/database/Dockerfile b/database/Dockerfile new file mode 100644 index 0000000..46df165 --- /dev/null +++ b/database/Dockerfile @@ -0,0 +1,15 @@ +# Use MySQL 8.0 as base image +FROM mysql:8.0 + +# Rely on runtime environment variables provided by docker-compose +# (MYSQL_ROOT_PASSWORD, MYSQL_DATABASE, MYSQL_USER, MYSQL_PASSWORD) + +# Copy only the deploy_dump.sql file with deterministic name for init order +COPY deploy_dump.sql /docker-entrypoint-initdb.d/00_deploy_dump.sql + +# Expose MySQL port +EXPOSE 3306 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD mysqladmin ping -h localhost || exit 1 diff --git a/database/candidb_dump1/candidb_main_audit_logs.sql b/database/candidb_dump1/candidb_main_audit_logs.sql new file mode 100644 index 0000000..65be4cc --- /dev/null +++ b/database/candidb_dump1/candidb_main_audit_logs.sql @@ -0,0 +1,55 @@ +CREATE DATABASE IF NOT EXISTS `candidb_main` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */ /*!80016 DEFAULT ENCRYPTION='N' */; +USE `candidb_main`; +-- MySQL dump 10.13 Distrib 8.0.38, for Win64 (x86_64) +-- +-- Host: localhost Database: candidb_main +-- ------------------------------------------------------ +-- Server version 8.0.39 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `audit_logs` +-- + +DROP TABLE IF EXISTS `audit_logs`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `audit_logs` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT (uuid()), + `user_id` varchar(36) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `action` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `resource_type` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `resource_id` varchar(36) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `old_values` json DEFAULT NULL, + `new_values` json DEFAULT NULL, + `ip_address` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `user_agent` text COLLATE utf8mb4_unicode_ci, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_user_action` (`user_id`,`action`), + KEY `idx_resource` (`resource_type`,`resource_id`), + KEY `idx_created_at` (`created_at`), + CONSTRAINT `audit_logs_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2025-09-16 20:22:32 diff --git a/database/candidb_dump1/candidb_main_candidate_responses.sql b/database/candidb_dump1/candidb_main_candidate_responses.sql new file mode 100644 index 0000000..a45ca66 --- /dev/null +++ b/database/candidb_dump1/candidb_main_candidate_responses.sql @@ -0,0 +1,53 @@ +CREATE DATABASE IF NOT EXISTS `candidb_main` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */ /*!80016 DEFAULT ENCRYPTION='N' */; +USE `candidb_main`; +-- MySQL dump 10.13 Distrib 8.0.38, for Win64 (x86_64) +-- +-- Host: localhost Database: candidb_main +-- ------------------------------------------------------ +-- Server version 8.0.39 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `candidate_responses` +-- + +DROP TABLE IF EXISTS `candidate_responses`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `candidate_responses` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT (uuid()), + `interview_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `question_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `response_text` text COLLATE utf8mb4_unicode_ci NOT NULL, + `response_audio_url` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `ai_score` decimal(5,2) DEFAULT NULL, + `ai_feedback` text COLLATE utf8mb4_unicode_ci, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `unique_response_per_question` (`interview_id`,`question_id`), + KEY `question_id` (`question_id`), + CONSTRAINT `candidate_responses_ibfk_1` FOREIGN KEY (`interview_id`) REFERENCES `interviews` (`id`) ON DELETE CASCADE, + CONSTRAINT `candidate_responses_ibfk_2` FOREIGN KEY (`question_id`) REFERENCES `interview_questions` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2025-09-16 20:22:32 diff --git a/database/candidb_dump1/candidb_main_candidates.sql b/database/candidb_dump1/candidb_main_candidates.sql new file mode 100644 index 0000000..5fd7670 --- /dev/null +++ b/database/candidb_dump1/candidb_main_candidates.sql @@ -0,0 +1,63 @@ +CREATE DATABASE IF NOT EXISTS `candidb_main` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */ /*!80016 DEFAULT ENCRYPTION='N' */; +USE `candidb_main`; +-- MySQL dump 10.13 Distrib 8.0.38, for Win64 (x86_64) +-- +-- Host: localhost Database: candidb_main +-- ------------------------------------------------------ +-- Server version 8.0.39 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `candidates` +-- + +DROP TABLE IF EXISTS `candidates`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `candidates` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT (uuid()), + `user_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `job_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `first_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `last_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `phone` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `resume_url` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `cover_letter` text COLLATE utf8mb4_unicode_ci, + `source` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `status` enum('applied','interviewing','evaluated','hired','rejected') COLLATE utf8mb4_unicode_ci DEFAULT 'applied', + `applied_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `last_activity_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `unique_candidate_per_job` (`job_id`,`email`), + KEY `idx_user_job` (`user_id`,`job_id`), + KEY `idx_status` (`status`), + KEY `idx_candidates_user_job_status` (`user_id`,`job_id`,`status`), + CONSTRAINT `candidates_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + CONSTRAINT `candidates_ibfk_2` FOREIGN KEY (`job_id`) REFERENCES `jobs` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2025-09-16 20:22:32 diff --git a/database/candidb_dump1/candidb_main_conversation_messages.sql b/database/candidb_dump1/candidb_main_conversation_messages.sql new file mode 100644 index 0000000..3058d62 --- /dev/null +++ b/database/candidb_dump1/candidb_main_conversation_messages.sql @@ -0,0 +1,52 @@ +CREATE DATABASE IF NOT EXISTS `candidb_main` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */ /*!80016 DEFAULT ENCRYPTION='N' */; +USE `candidb_main`; +-- MySQL dump 10.13 Distrib 8.0.38, for Win64 (x86_64) +-- +-- Host: localhost Database: candidb_main +-- ------------------------------------------------------ +-- Server version 8.0.39 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `conversation_messages` +-- + +DROP TABLE IF EXISTS `conversation_messages`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `conversation_messages` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT (uuid()), + `interview_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `link_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `sender` enum('candidate','ai') COLLATE utf8mb4_unicode_ci NOT NULL, + `message` text COLLATE utf8mb4_unicode_ci NOT NULL, + `message_data` json DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_interview_id` (`interview_id`), + KEY `idx_link_id` (`link_id`), + KEY `idx_created_at` (`created_at`), + CONSTRAINT `conversation_messages_ibfk_1` FOREIGN KEY (`interview_id`) REFERENCES `interviews` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2025-09-16 20:22:31 diff --git a/database/candidb_dump1/candidb_main_interview_events.sql b/database/candidb_dump1/candidb_main_interview_events.sql new file mode 100644 index 0000000..4fbe051 --- /dev/null +++ b/database/candidb_dump1/candidb_main_interview_events.sql @@ -0,0 +1,52 @@ +CREATE DATABASE IF NOT EXISTS `candidb_main` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */ /*!80016 DEFAULT ENCRYPTION='N' */; +USE `candidb_main`; +-- MySQL dump 10.13 Distrib 8.0.38, for Win64 (x86_64) +-- +-- Host: localhost Database: candidb_main +-- ------------------------------------------------------ +-- Server version 8.0.39 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `interview_events` +-- + +DROP TABLE IF EXISTS `interview_events`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `interview_events` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `job_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `link_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `event_type` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `event_data` json DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_job_id` (`job_id`), + KEY `idx_link_id` (`link_id`), + KEY `idx_event_type` (`event_type`), + KEY `idx_created_at` (`created_at`), + CONSTRAINT `interview_events_ibfk_1` FOREIGN KEY (`job_id`) REFERENCES `jobs` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2025-09-16 20:22:32 diff --git a/database/candidb_dump1/candidb_main_interview_questions.sql b/database/candidb_dump1/candidb_main_interview_questions.sql new file mode 100644 index 0000000..0aa11a8 --- /dev/null +++ b/database/candidb_dump1/candidb_main_interview_questions.sql @@ -0,0 +1,52 @@ +CREATE DATABASE IF NOT EXISTS `candidb_main` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */ /*!80016 DEFAULT ENCRYPTION='N' */; +USE `candidb_main`; +-- MySQL dump 10.13 Distrib 8.0.38, for Win64 (x86_64) +-- +-- Host: localhost Database: candidb_main +-- ------------------------------------------------------ +-- Server version 8.0.39 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `interview_questions` +-- + +DROP TABLE IF EXISTS `interview_questions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `interview_questions` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT (uuid()), + `interview_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `question_text` text COLLATE utf8mb4_unicode_ci NOT NULL, + `question_type` enum('technical','behavioral','situational','culture_fit') COLLATE utf8mb4_unicode_ci NOT NULL, + `difficulty_level` enum('easy','medium','hard') COLLATE utf8mb4_unicode_ci DEFAULT 'medium', + `expected_answer` text COLLATE utf8mb4_unicode_ci, + `evaluation_criteria` json DEFAULT NULL, + `order_index` int NOT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_interview_order` (`interview_id`,`order_index`), + CONSTRAINT `interview_questions_ibfk_1` FOREIGN KEY (`interview_id`) REFERENCES `interviews` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2025-09-16 20:22:32 diff --git a/database/candidb_dump1/candidb_main_interview_tokens.sql b/database/candidb_dump1/candidb_main_interview_tokens.sql new file mode 100644 index 0000000..849f034 --- /dev/null +++ b/database/candidb_dump1/candidb_main_interview_tokens.sql @@ -0,0 +1,79 @@ +CREATE DATABASE IF NOT EXISTS `candidb_main` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */ /*!80016 DEFAULT ENCRYPTION='N' */; +USE `candidb_main`; +-- MySQL dump 10.13 Distrib 8.0.38, for Win64 (x86_64) +-- +-- Host: localhost Database: candidb_main +-- ------------------------------------------------------ +-- Server version 8.0.39 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `interview_tokens` +-- + +DROP TABLE IF EXISTS `interview_tokens`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `interview_tokens` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT (uuid()), + `user_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `token_type` enum('single','bulk') COLLATE utf8mb4_unicode_ci NOT NULL, + `quantity` int NOT NULL DEFAULT '1', + `price_per_token` decimal(10,2) NOT NULL, + `total_price` decimal(10,2) NOT NULL, + `tokens_used` int DEFAULT '0', + `tokens_remaining` int GENERATED ALWAYS AS ((`quantity` - `tokens_used`)) STORED, + `status` enum('active','exhausted','expired') COLLATE utf8mb4_unicode_ci DEFAULT 'active', + `expires_at` timestamp NULL DEFAULT NULL, + `purchased_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_user_status` (`user_id`,`status`), + KEY `idx_expires_at` (`expires_at`), + KEY `idx_interview_tokens_user_active` (`user_id`,`status`), + CONSTRAINT `interview_tokens_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + CONSTRAINT `chk_interview_tokens_quantity_positive` CHECK ((`quantity` > 0)), + CONSTRAINT `chk_interview_tokens_used_valid` CHECK (((`tokens_used` >= 0) and (`tokens_used` <= `quantity`))) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!50003 SET @saved_cs_client = @@character_set_client */ ; +/*!50003 SET @saved_cs_results = @@character_set_results */ ; +/*!50003 SET @saved_col_connection = @@collation_connection */ ; +/*!50003 SET character_set_client = utf8mb4 */ ; +/*!50003 SET character_set_results = utf8mb4 */ ; +/*!50003 SET collation_connection = utf8mb4_0900_ai_ci */ ; +/*!50003 SET @saved_sql_mode = @@sql_mode */ ; +/*!50003 SET sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION' */ ; +DELIMITER ;; +/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`localhost`*/ /*!50003 TRIGGER `update_token_usage_after_purchase` AFTER INSERT ON `interview_tokens` FOR EACH ROW BEGIN + INSERT INTO user_usage (user_id, tokens_purchased) + VALUES (NEW.user_id, NEW.quantity) + ON DUPLICATE KEY UPDATE tokens_purchased = tokens_purchased + NEW.quantity; +END */;; +DELIMITER ; +/*!50003 SET sql_mode = @saved_sql_mode */ ; +/*!50003 SET character_set_client = @saved_cs_client */ ; +/*!50003 SET character_set_results = @saved_cs_results */ ; +/*!50003 SET collation_connection = @saved_col_connection */ ; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2025-09-16 20:22:32 diff --git a/database/candidb_dump1/candidb_main_interviews.sql b/database/candidb_dump1/candidb_main_interviews.sql new file mode 100644 index 0000000..89c7d77 --- /dev/null +++ b/database/candidb_dump1/candidb_main_interviews.sql @@ -0,0 +1,96 @@ +CREATE DATABASE IF NOT EXISTS `candidb_main` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */ /*!80016 DEFAULT ENCRYPTION='N' */; +USE `candidb_main`; +-- MySQL dump 10.13 Distrib 8.0.38, for Win64 (x86_64) +-- +-- Host: localhost Database: candidb_main +-- ------------------------------------------------------ +-- Server version 8.0.39 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `interviews` +-- + +DROP TABLE IF EXISTS `interviews`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `interviews` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT (uuid()), + `user_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `candidate_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `job_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `token` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `status` enum('scheduled','in_progress','completed','abandoned') COLLATE utf8mb4_unicode_ci DEFAULT 'scheduled', + `started_at` timestamp NULL DEFAULT NULL, + `completed_at` timestamp NULL DEFAULT NULL, + `duration_minutes` int DEFAULT '0', + `ai_questions` json DEFAULT NULL, + `candidate_responses` json DEFAULT NULL, + `ai_evaluation` json DEFAULT NULL, + `overall_score` decimal(5,2) DEFAULT NULL, + `technical_score` decimal(5,2) DEFAULT NULL, + `communication_score` decimal(5,2) DEFAULT NULL, + `culture_fit_score` decimal(5,2) DEFAULT NULL, + `ai_feedback` text COLLATE utf8mb4_unicode_ci, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `token` (`token`), + KEY `candidate_id` (`candidate_id`), + KEY `job_id` (`job_id`), + KEY `idx_token` (`token`), + KEY `idx_user_candidate` (`user_id`,`candidate_id`), + KEY `idx_status` (`status`), + KEY `idx_interviews_user_status` (`user_id`,`status`), + KEY `idx_interviews_token_status` (`token`,`status`), + CONSTRAINT `interviews_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + CONSTRAINT `interviews_ibfk_2` FOREIGN KEY (`candidate_id`) REFERENCES `candidates` (`id`) ON DELETE CASCADE, + CONSTRAINT `interviews_ibfk_3` FOREIGN KEY (`job_id`) REFERENCES `jobs` (`id`) ON DELETE CASCADE, + CONSTRAINT `chk_scores_valid` CHECK ((((`overall_score` is null) or ((`overall_score` >= 0) and (`overall_score` <= 100))) and ((`technical_score` is null) or ((`technical_score` >= 0) and (`technical_score` <= 100))) and ((`communication_score` is null) or ((`communication_score` >= 0) and (`communication_score` <= 100))) and ((`culture_fit_score` is null) or ((`culture_fit_score` >= 0) and (`culture_fit_score` <= 100))))) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!50003 SET @saved_cs_client = @@character_set_client */ ; +/*!50003 SET @saved_cs_results = @@character_set_results */ ; +/*!50003 SET @saved_col_connection = @@collation_connection */ ; +/*!50003 SET character_set_client = utf8mb4 */ ; +/*!50003 SET character_set_results = utf8mb4 */ ; +/*!50003 SET collation_connection = utf8mb4_0900_ai_ci */ ; +/*!50003 SET @saved_sql_mode = @@sql_mode */ ; +/*!50003 SET sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION' */ ; +DELIMITER ;; +/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`localhost`*/ /*!50003 TRIGGER `update_interview_usage_after_complete` AFTER UPDATE ON `interviews` FOR EACH ROW BEGIN + IF OLD.status != 'completed' AND NEW.status = 'completed' THEN + -- Update user usage + INSERT INTO user_usage (user_id, interviews_completed, tokens_used) + VALUES (NEW.user_id, 1, 1) + ON DUPLICATE KEY UPDATE + interviews_completed = interviews_completed + 1, + tokens_used = tokens_used + 1; + END IF; +END */;; +DELIMITER ; +/*!50003 SET sql_mode = @saved_sql_mode */ ; +/*!50003 SET character_set_client = @saved_cs_client */ ; +/*!50003 SET character_set_results = @saved_cs_results */ ; +/*!50003 SET collation_connection = @saved_col_connection */ ; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2025-09-16 20:22:31 diff --git a/database/candidb_dump1/candidb_main_job_links.sql b/database/candidb_dump1/candidb_main_job_links.sql new file mode 100644 index 0000000..9bcb8e9 --- /dev/null +++ b/database/candidb_dump1/candidb_main_job_links.sql @@ -0,0 +1,52 @@ +CREATE DATABASE IF NOT EXISTS `candidb_main` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */ /*!80016 DEFAULT ENCRYPTION='N' */; +USE `candidb_main`; +-- MySQL dump 10.13 Distrib 8.0.38, for Win64 (x86_64) +-- +-- Host: localhost Database: candidb_main +-- ------------------------------------------------------ +-- Server version 8.0.39 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `job_links` +-- + +DROP TABLE IF EXISTS `job_links`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `job_links` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `job_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `url_slug` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `tokens_available` int DEFAULT '0', + `tokens_used` int DEFAULT '0', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `url_slug` (`url_slug`), + KEY `idx_job_id` (`job_id`), + KEY `idx_url_slug` (`url_slug`), + CONSTRAINT `job_links_ibfk_1` FOREIGN KEY (`job_id`) REFERENCES `jobs` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2025-09-16 20:22:32 diff --git a/database/candidb_dump1/candidb_main_jobs.sql b/database/candidb_dump1/candidb_main_jobs.sql new file mode 100644 index 0000000..86a3376 --- /dev/null +++ b/database/candidb_dump1/candidb_main_jobs.sql @@ -0,0 +1,85 @@ +CREATE DATABASE IF NOT EXISTS `candidb_main` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */ /*!80016 DEFAULT ENCRYPTION='N' */; +USE `candidb_main`; +-- MySQL dump 10.13 Distrib 8.0.38, for Win64 (x86_64) +-- +-- Host: localhost Database: candidb_main +-- ------------------------------------------------------ +-- Server version 8.0.39 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `jobs` +-- + +DROP TABLE IF EXISTS `jobs`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `jobs` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT (uuid()), + `user_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `description` text COLLATE utf8mb4_unicode_ci NOT NULL, + `requirements` text COLLATE utf8mb4_unicode_ci NOT NULL, + `skills_required` json DEFAULT NULL, + `location` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `employment_type` enum('full_time','part_time','contract','internship') COLLATE utf8mb4_unicode_ci DEFAULT 'full_time', + `experience_level` enum('entry','mid','senior','lead','executive') COLLATE utf8mb4_unicode_ci DEFAULT 'mid', + `salary_min` decimal(10,2) DEFAULT NULL, + `salary_max` decimal(10,2) DEFAULT NULL, + `currency` varchar(3) COLLATE utf8mb4_unicode_ci DEFAULT 'USD', + `status` enum('draft','active','paused','closed') COLLATE utf8mb4_unicode_ci DEFAULT 'draft', + `evaluation_criteria` json DEFAULT NULL, + `interview_questions` json DEFAULT NULL, + `interview_style` enum('personal','balanced','technical') COLLATE utf8mb4_unicode_ci DEFAULT 'balanced', + `application_deadline` timestamp NULL DEFAULT NULL, + `icon` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT 'briefcase', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_user_status` (`user_id`,`status`), + KEY `idx_created_at` (`created_at`), + KEY `idx_jobs_user_status_created` (`user_id`,`status`,`created_at` DESC), + CONSTRAINT `jobs_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!50003 SET @saved_cs_client = @@character_set_client */ ; +/*!50003 SET @saved_cs_results = @@character_set_results */ ; +/*!50003 SET @saved_col_connection = @@collation_connection */ ; +/*!50003 SET character_set_client = utf8mb4 */ ; +/*!50003 SET character_set_results = utf8mb4 */ ; +/*!50003 SET collation_connection = utf8mb4_0900_ai_ci */ ; +/*!50003 SET @saved_sql_mode = @@sql_mode */ ; +/*!50003 SET sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION' */ ; +DELIMITER ;; +/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`localhost`*/ /*!50003 TRIGGER `update_job_usage_after_insert` AFTER INSERT ON `jobs` FOR EACH ROW BEGIN + INSERT INTO user_usage (user_id, jobs_created) + VALUES (NEW.user_id, 1) + ON DUPLICATE KEY UPDATE jobs_created = jobs_created + 1; +END */;; +DELIMITER ; +/*!50003 SET sql_mode = @saved_sql_mode */ ; +/*!50003 SET character_set_client = @saved_cs_client */ ; +/*!50003 SET character_set_results = @saved_cs_results */ ; +/*!50003 SET collation_connection = @saved_col_connection */ ; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2025-09-16 20:22:31 diff --git a/database/candidb_dump1/candidb_main_payment_records.sql b/database/candidb_dump1/candidb_main_payment_records.sql new file mode 100644 index 0000000..d7b649f --- /dev/null +++ b/database/candidb_dump1/candidb_main_payment_records.sql @@ -0,0 +1,59 @@ +CREATE DATABASE IF NOT EXISTS `candidb_main` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */ /*!80016 DEFAULT ENCRYPTION='N' */; +USE `candidb_main`; +-- MySQL dump 10.13 Distrib 8.0.38, for Win64 (x86_64) +-- +-- Host: localhost Database: candidb_main +-- ------------------------------------------------------ +-- Server version 8.0.39 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `payment_records` +-- + +DROP TABLE IF EXISTS `payment_records`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `payment_records` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT (uuid()), + `user_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `token_package_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `amount` decimal(10,2) NOT NULL, + `currency` varchar(3) COLLATE utf8mb4_unicode_ci DEFAULT 'USD', + `status` enum('pending','paid','failed','refunded','cancelled') COLLATE utf8mb4_unicode_ci DEFAULT 'pending', + `payment_method` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `payment_reference` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `invoice_url` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `paid_at` timestamp NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `token_package_id` (`token_package_id`), + KEY `idx_user_status` (`user_id`,`status`), + KEY `idx_payment_reference` (`payment_reference`), + KEY `idx_payment_records_user_created` (`user_id`,`created_at` DESC), + CONSTRAINT `payment_records_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + CONSTRAINT `payment_records_ibfk_2` FOREIGN KEY (`token_package_id`) REFERENCES `token_packages` (`id`) ON DELETE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2025-09-16 20:22:31 diff --git a/database/candidb_dump1/candidb_main_routines.sql b/database/candidb_dump1/candidb_main_routines.sql new file mode 100644 index 0000000..d3c730b --- /dev/null +++ b/database/candidb_dump1/candidb_main_routines.sql @@ -0,0 +1,631 @@ +CREATE DATABASE IF NOT EXISTS `candidb_main` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */ /*!80016 DEFAULT ENCRYPTION='N' */; +USE `candidb_main`; +-- MySQL dump 10.13 Distrib 8.0.38, for Win64 (x86_64) +-- +-- Host: localhost Database: candidb_main +-- ------------------------------------------------------ +-- Server version 8.0.39 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Dumping events for database 'candidb_main' +-- + +-- +-- Dumping routines for database 'candidb_main' +-- +/*!50003 DROP FUNCTION IF EXISTS `can_create_job` */; +/*!50003 SET @saved_cs_client = @@character_set_client */ ; +/*!50003 SET @saved_cs_results = @@character_set_results */ ; +/*!50003 SET @saved_col_connection = @@collation_connection */ ; +/*!50003 SET character_set_client = utf8mb4 */ ; +/*!50003 SET character_set_results = utf8mb4 */ ; +/*!50003 SET collation_connection = utf8mb4_0900_ai_ci */ ; +/*!50003 SET @saved_sql_mode = @@sql_mode */ ; +/*!50003 SET sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION' */ ; +DELIMITER ;; +CREATE DEFINER=`root`@`localhost` FUNCTION `can_create_job`(user_uuid VARCHAR(36)) RETURNS tinyint(1) + READS SQL DATA + DETERMINISTIC +BEGIN + DECLARE current_jobs INT DEFAULT 0; + DECLARE max_jobs INT DEFAULT 100; -- Hard limit of 100 jobs + + -- Get current job count + SELECT COALESCE(jobs_created, 0) INTO current_jobs + FROM user_usage + WHERE user_id = user_uuid; + + -- Return TRUE if under limit + RETURN current_jobs < max_jobs; +END ;; +DELIMITER ; +/*!50003 SET sql_mode = @saved_sql_mode */ ; +/*!50003 SET character_set_client = @saved_cs_client */ ; +/*!50003 SET character_set_results = @saved_cs_results */ ; +/*!50003 SET collation_connection = @saved_col_connection */ ; +/*!50003 DROP FUNCTION IF EXISTS `get_all_users` */; +/*!50003 SET @saved_cs_client = @@character_set_client */ ; +/*!50003 SET @saved_cs_results = @@character_set_results */ ; +/*!50003 SET @saved_col_connection = @@collation_connection */ ; +/*!50003 SET character_set_client = utf8mb4 */ ; +/*!50003 SET character_set_results = utf8mb4 */ ; +/*!50003 SET collation_connection = utf8mb4_0900_ai_ci */ ; +/*!50003 SET @saved_sql_mode = @@sql_mode */ ; +/*!50003 SET sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION' */ ; +DELIMITER ;; +CREATE DEFINER=`root`@`localhost` FUNCTION `get_all_users`() RETURNS json + READS SQL DATA + DETERMINISTIC +BEGIN + DECLARE result JSON; + + SELECT JSON_ARRAYAGG( + JSON_OBJECT( + 'id', id, + 'email', email, + 'first_name', first_name, + 'last_name', last_name, + 'role', role, + 'company_name', company_name, + 'is_active', is_active, + 'last_login_at', last_login_at, + 'email_verified_at', email_verified_at, + 'created_at', created_at + ) + ) INTO result + FROM users + WHERE deleted_at IS NULL + ORDER BY created_at DESC; + + RETURN result; +END ;; +DELIMITER ; +/*!50003 SET sql_mode = @saved_sql_mode */ ; +/*!50003 SET character_set_client = @saved_cs_client */ ; +/*!50003 SET character_set_results = @saved_cs_results */ ; +/*!50003 SET collation_connection = @saved_col_connection */ ; +/*!50003 DROP FUNCTION IF EXISTS `get_token_usage_summary` */; +/*!50003 SET @saved_cs_client = @@character_set_client */ ; +/*!50003 SET @saved_cs_results = @@character_set_results */ ; +/*!50003 SET @saved_col_connection = @@collation_connection */ ; +/*!50003 SET character_set_client = utf8mb4 */ ; +/*!50003 SET character_set_results = utf8mb4 */ ; +/*!50003 SET collation_connection = utf8mb4_0900_ai_ci */ ; +/*!50003 SET @saved_sql_mode = @@sql_mode */ ; +/*!50003 SET sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION' */ ; +DELIMITER ;; +CREATE DEFINER=`root`@`localhost` FUNCTION `get_token_usage_summary`(user_uuid VARCHAR(36)) RETURNS json + READS SQL DATA + DETERMINISTIC +BEGIN + DECLARE total_purchased INT DEFAULT 0; + DECLARE total_used INT DEFAULT 0; + DECLARE total_available INT DEFAULT 0; + DECLARE result JSON; + + -- Get total purchased tokens + SELECT COALESCE(SUM(quantity), 0) INTO total_purchased + FROM interview_tokens + WHERE user_id = user_uuid; + + -- Get total used tokens + SELECT COALESCE(SUM(tokens_used), 0) INTO total_used + FROM interview_tokens + WHERE user_id = user_uuid; + + -- Get total available tokens + SELECT COALESCE(SUM(tokens_remaining), 0) INTO total_available + FROM interview_tokens + WHERE user_id = user_uuid + AND status = 'active' + AND (expires_at IS NULL OR expires_at > NOW()); + + -- Build JSON result + SET result = JSON_OBJECT( + 'total_purchased', total_purchased, + 'total_used', total_used, + 'total_available', total_available, + 'utilization_percentage', CASE + WHEN total_purchased > 0 THEN ROUND((total_used / total_purchased) * 100, 2) + ELSE 0 + END + ); + + RETURN result; +END ;; +DELIMITER ; +/*!50003 SET sql_mode = @saved_sql_mode */ ; +/*!50003 SET character_set_client = @saved_cs_client */ ; +/*!50003 SET character_set_results = @saved_cs_results */ ; +/*!50003 SET collation_connection = @saved_col_connection */ ; +/*!50003 DROP FUNCTION IF EXISTS `get_user_statistics` */; +/*!50003 SET @saved_cs_client = @@character_set_client */ ; +/*!50003 SET @saved_cs_results = @@character_set_results */ ; +/*!50003 SET @saved_col_connection = @@collation_connection */ ; +/*!50003 SET character_set_client = utf8mb4 */ ; +/*!50003 SET character_set_results = utf8mb4 */ ; +/*!50003 SET collation_connection = utf8mb4_0900_ai_ci */ ; +/*!50003 SET @saved_sql_mode = @@sql_mode */ ; +/*!50003 SET sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION' */ ; +DELIMITER ;; +CREATE DEFINER=`root`@`localhost` FUNCTION `get_user_statistics`(user_uuid VARCHAR(36)) RETURNS json + READS SQL DATA + DETERMINISTIC +BEGIN + DECLARE result JSON; + DECLARE user_usage_data JSON; + DECLARE token_summary JSON; + + -- Get usage data + SELECT JSON_OBJECT( + 'jobs_created', COALESCE(jobs_created, 0), + 'interviews_completed', COALESCE(interviews_completed, 0), + 'tokens_purchased', COALESCE(tokens_purchased, 0), + 'tokens_used', COALESCE(tokens_used, 0) + ) INTO user_usage_data + FROM user_usage + WHERE user_id = user_uuid; + + -- Get token summary + SELECT get_token_usage_summary(user_uuid) INTO token_summary; + + -- Build result + SET result = JSON_OBJECT( + 'usage', user_usage_data, + 'tokens', token_summary + ); + + RETURN result; +END ;; +DELIMITER ; +/*!50003 SET sql_mode = @saved_sql_mode */ ; +/*!50003 SET character_set_client = @saved_cs_client */ ; +/*!50003 SET character_set_results = @saved_cs_results */ ; +/*!50003 SET collation_connection = @saved_col_connection */ ; +/*!50003 DROP FUNCTION IF EXISTS `has_available_tokens` */; +/*!50003 SET @saved_cs_client = @@character_set_client */ ; +/*!50003 SET @saved_cs_results = @@character_set_results */ ; +/*!50003 SET @saved_col_connection = @@collation_connection */ ; +/*!50003 SET character_set_client = utf8mb4 */ ; +/*!50003 SET character_set_results = utf8mb4 */ ; +/*!50003 SET collation_connection = utf8mb4_0900_ai_ci */ ; +/*!50003 SET @saved_sql_mode = @@sql_mode */ ; +/*!50003 SET sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION' */ ; +DELIMITER ;; +CREATE DEFINER=`root`@`localhost` FUNCTION `has_available_tokens`(user_uuid VARCHAR(36)) RETURNS tinyint(1) + READS SQL DATA + DETERMINISTIC +BEGIN + DECLARE available_tokens INT DEFAULT 0; + + -- Get available tokens (active and not expired) + SELECT COALESCE(SUM(tokens_remaining), 0) INTO available_tokens + FROM interview_tokens + WHERE user_id = user_uuid + AND status = 'active' + AND (expires_at IS NULL OR expires_at > NOW()); + + -- Return TRUE if has available tokens + RETURN available_tokens > 0; +END ;; +DELIMITER ; +/*!50003 SET sql_mode = @saved_sql_mode */ ; +/*!50003 SET character_set_client = @saved_cs_client */ ; +/*!50003 SET character_set_results = @saved_cs_results */ ; +/*!50003 SET collation_connection = @saved_col_connection */ ; +/*!50003 DROP FUNCTION IF EXISTS `is_admin` */; +/*!50003 SET @saved_cs_client = @@character_set_client */ ; +/*!50003 SET @saved_cs_results = @@character_set_results */ ; +/*!50003 SET @saved_col_connection = @@collation_connection */ ; +/*!50003 SET character_set_client = utf8mb4 */ ; +/*!50003 SET character_set_results = utf8mb4 */ ; +/*!50003 SET collation_connection = utf8mb4_0900_ai_ci */ ; +/*!50003 SET @saved_sql_mode = @@sql_mode */ ; +/*!50003 SET sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION' */ ; +DELIMITER ;; +CREATE DEFINER=`root`@`localhost` FUNCTION `is_admin`(user_uuid VARCHAR(36)) RETURNS tinyint(1) + READS SQL DATA + DETERMINISTIC +BEGIN + DECLARE user_role VARCHAR(20) DEFAULT NULL; + + SELECT role INTO user_role + FROM users + WHERE id = user_uuid AND is_active = TRUE; + + RETURN user_role = 'admin'; +END ;; +DELIMITER ; +/*!50003 SET sql_mode = @saved_sql_mode */ ; +/*!50003 SET character_set_client = @saved_cs_client */ ; +/*!50003 SET character_set_results = @saved_cs_results */ ; +/*!50003 SET collation_connection = @saved_col_connection */ ; +/*!50003 DROP PROCEDURE IF EXISTS `add_tokens_to_user` */; +/*!50003 SET @saved_cs_client = @@character_set_client */ ; +/*!50003 SET @saved_cs_results = @@character_set_results */ ; +/*!50003 SET @saved_col_connection = @@collation_connection */ ; +/*!50003 SET character_set_client = utf8mb4 */ ; +/*!50003 SET character_set_results = utf8mb4 */ ; +/*!50003 SET collation_connection = utf8mb4_0900_ai_ci */ ; +/*!50003 SET @saved_sql_mode = @@sql_mode */ ; +/*!50003 SET sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION' */ ; +DELIMITER ;; +CREATE DEFINER=`root`@`localhost` PROCEDURE `add_tokens_to_user`( + IN p_user_id VARCHAR(36), + IN p_quantity INT, + IN p_price_per_token DECIMAL(10,2), + IN p_admin_id VARCHAR(36), + OUT p_success BOOLEAN, + OUT p_message VARCHAR(255) +) +BEGIN + DECLARE v_total_price DECIMAL(10,2); + DECLARE v_token_id VARCHAR(36); + + DECLARE EXIT HANDLER FOR SQLEXCEPTION + BEGIN + ROLLBACK; + SET p_success = FALSE; + SET p_message = 'An error occurred while adding tokens'; + END; + + -- Check if admin + IF NOT is_admin(p_admin_id) THEN + SET p_success = FALSE; + SET p_message = 'Access denied: Admin privileges required'; + ELSE + -- Check if user exists + IF NOT EXISTS (SELECT 1 FROM users WHERE id = p_user_id AND deleted_at IS NULL) THEN + SET p_success = FALSE; + SET p_message = 'User not found'; + ELSE + -- Calculate total price + SET v_total_price = p_quantity * p_price_per_token; + + -- Create token record + SET v_token_id = UUID(); + + INSERT INTO interview_tokens ( + id, user_id, token_type, quantity, price_per_token, + total_price, status, purchased_at + ) VALUES ( + v_token_id, p_user_id, + CASE WHEN p_quantity = 1 THEN 'single' ELSE 'bulk' END, + p_quantity, p_price_per_token, v_total_price, + 'active', NOW() + ); + + -- Create payment record (admin-granted) + INSERT INTO payment_records ( + user_id, interview_token_id, token_package_id, + amount, status, payment_method, payment_reference + ) VALUES ( + p_user_id, v_token_id, NULL, v_total_price, + 'paid', 'admin_granted', CONCAT('ADMIN_', p_admin_id, '_', NOW()) + ); + + SET p_success = TRUE; + SET p_message = CONCAT('Successfully added ', p_quantity, ' tokens to user'); + END IF; + END IF; +END ;; +DELIMITER ; +/*!50003 SET sql_mode = @saved_sql_mode */ ; +/*!50003 SET character_set_client = @saved_cs_client */ ; +/*!50003 SET character_set_results = @saved_cs_results */ ; +/*!50003 SET collation_connection = @saved_col_connection */ ; +/*!50003 DROP PROCEDURE IF EXISTS `change_user_password` */; +/*!50003 SET @saved_cs_client = @@character_set_client */ ; +/*!50003 SET @saved_cs_results = @@character_set_results */ ; +/*!50003 SET @saved_col_connection = @@collation_connection */ ; +/*!50003 SET character_set_client = utf8mb4 */ ; +/*!50003 SET character_set_results = utf8mb4 */ ; +/*!50003 SET collation_connection = utf8mb4_0900_ai_ci */ ; +/*!50003 SET @saved_sql_mode = @@sql_mode */ ; +/*!50003 SET sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION' */ ; +DELIMITER ;; +CREATE DEFINER=`root`@`localhost` PROCEDURE `change_user_password`( + IN p_user_id VARCHAR(36), + IN p_new_password_hash VARCHAR(255), + IN p_admin_id VARCHAR(36), + OUT p_success BOOLEAN, + OUT p_message VARCHAR(255) +) +BEGIN + DECLARE EXIT HANDLER FOR SQLEXCEPTION + BEGIN + ROLLBACK; + SET p_success = FALSE; + SET p_message = 'An error occurred while changing password'; + END; + + -- Check if admin + IF NOT is_admin(p_admin_id) THEN + SET p_success = FALSE; + SET p_message = 'Access denied: Admin privileges required'; + ELSE + -- Check if user exists + IF NOT EXISTS (SELECT 1 FROM users WHERE id = p_user_id AND deleted_at IS NULL) THEN + SET p_success = FALSE; + SET p_message = 'User not found'; + ELSE + -- Update password + UPDATE users SET + password_hash = p_new_password_hash, + updated_at = NOW() + WHERE id = p_user_id; + + SET p_success = TRUE; + SET p_message = 'Password changed successfully'; + END IF; + END IF; +END ;; +DELIMITER ; +/*!50003 SET sql_mode = @saved_sql_mode */ ; +/*!50003 SET character_set_client = @saved_cs_client */ ; +/*!50003 SET character_set_results = @saved_cs_results */ ; +/*!50003 SET collation_connection = @saved_col_connection */ ; +/*!50003 DROP PROCEDURE IF EXISTS `create_user` */; +/*!50003 SET @saved_cs_client = @@character_set_client */ ; +/*!50003 SET @saved_cs_results = @@character_set_results */ ; +/*!50003 SET @saved_col_connection = @@collation_connection */ ; +/*!50003 SET character_set_client = utf8mb4 */ ; +/*!50003 SET character_set_results = utf8mb4 */ ; +/*!50003 SET collation_connection = utf8mb4_0900_ai_ci */ ; +/*!50003 SET @saved_sql_mode = @@sql_mode */ ; +/*!50003 SET sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION' */ ; +DELIMITER ;; +CREATE DEFINER=`root`@`localhost` PROCEDURE `create_user`( + IN p_email VARCHAR(255), + IN p_password_hash VARCHAR(255), + IN p_first_name VARCHAR(100), + IN p_last_name VARCHAR(100), + IN p_role ENUM('admin', 'recruiter'), + IN p_company_name VARCHAR(255), + IN p_admin_id VARCHAR(36), + OUT p_user_id VARCHAR(36), + OUT p_success BOOLEAN, + OUT p_message VARCHAR(255) +) +BEGIN + DECLARE EXIT HANDLER FOR SQLEXCEPTION + BEGIN + ROLLBACK; + SET p_success = FALSE; + SET p_message = 'An error occurred while creating user'; + END; + + -- Check if admin + IF NOT is_admin(p_admin_id) THEN + SET p_success = FALSE; + SET p_message = 'Access denied: Admin privileges required'; + ELSE + -- Check if email already exists + IF EXISTS (SELECT 1 FROM users WHERE email = p_email AND deleted_at IS NULL) THEN + SET p_success = FALSE; + SET p_message = 'Email already exists'; + ELSE + -- Create user + SET p_user_id = UUID(); + + INSERT INTO users ( + id, email, password_hash, first_name, last_name, + role, company_name, is_active, email_verified_at + ) VALUES ( + p_user_id, p_email, p_password_hash, p_first_name, p_last_name, + p_role, p_company_name, TRUE, NOW() + ); + + -- Initialize usage tracking + INSERT INTO user_usage (user_id) VALUES (p_user_id); + + SET p_success = TRUE; + SET p_message = 'User created successfully'; + END IF; + END IF; +END ;; +DELIMITER ; +/*!50003 SET sql_mode = @saved_sql_mode */ ; +/*!50003 SET character_set_client = @saved_cs_client */ ; +/*!50003 SET character_set_results = @saved_cs_results */ ; +/*!50003 SET collation_connection = @saved_col_connection */ ; +/*!50003 DROP PROCEDURE IF EXISTS `deactivate_user` */; +/*!50003 SET @saved_cs_client = @@character_set_client */ ; +/*!50003 SET @saved_cs_results = @@character_set_results */ ; +/*!50003 SET @saved_col_connection = @@collation_connection */ ; +/*!50003 SET character_set_client = utf8mb4 */ ; +/*!50003 SET character_set_results = utf8mb4 */ ; +/*!50003 SET collation_connection = utf8mb4_0900_ai_ci */ ; +/*!50003 SET @saved_sql_mode = @@sql_mode */ ; +/*!50003 SET sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION' */ ; +DELIMITER ;; +CREATE DEFINER=`root`@`localhost` PROCEDURE `deactivate_user`( + IN p_user_id VARCHAR(36), + IN p_admin_id VARCHAR(36), + OUT p_success BOOLEAN, + OUT p_message VARCHAR(255) +) +BEGIN + DECLARE EXIT HANDLER FOR SQLEXCEPTION + BEGIN + ROLLBACK; + SET p_success = FALSE; + SET p_message = 'An error occurred while deactivating user'; + END; + + -- Check if admin + IF NOT is_admin(p_admin_id) THEN + SET p_success = FALSE; + SET p_message = 'Access denied: Admin privileges required'; + ELSE + -- Check if user exists + IF NOT EXISTS (SELECT 1 FROM users WHERE id = p_user_id AND deleted_at IS NULL) THEN + SET p_success = FALSE; + SET p_message = 'User not found'; + ELSE + -- Deactivate user + UPDATE users SET + is_active = FALSE, + updated_at = NOW() + WHERE id = p_user_id; + + SET p_success = TRUE; + SET p_message = 'User deactivated successfully'; + END IF; + END IF; +END ;; +DELIMITER ; +/*!50003 SET sql_mode = @saved_sql_mode */ ; +/*!50003 SET character_set_client = @saved_cs_client */ ; +/*!50003 SET character_set_results = @saved_cs_results */ ; +/*!50003 SET collation_connection = @saved_col_connection */ ; +/*!50003 DROP PROCEDURE IF EXISTS `get_system_statistics` */; +/*!50003 SET @saved_cs_client = @@character_set_client */ ; +/*!50003 SET @saved_cs_results = @@character_set_results */ ; +/*!50003 SET @saved_col_connection = @@collation_connection */ ; +/*!50003 SET character_set_client = utf8mb4 */ ; +/*!50003 SET character_set_results = utf8mb4 */ ; +/*!50003 SET collation_connection = utf8mb4_0900_ai_ci */ ; +/*!50003 SET @saved_sql_mode = @@sql_mode */ ; +/*!50003 SET sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION' */ ; +DELIMITER ;; +CREATE DEFINER=`root`@`localhost` PROCEDURE `get_system_statistics`( + IN p_admin_id VARCHAR(36), + OUT p_success BOOLEAN, + OUT p_message VARCHAR(255), + OUT p_statistics JSON +) +BEGIN + DECLARE v_total_users INT DEFAULT 0; + DECLARE v_active_users INT DEFAULT 0; + DECLARE v_total_jobs INT DEFAULT 0; + DECLARE v_total_interviews INT DEFAULT 0; + DECLARE v_total_tokens_purchased INT DEFAULT 0; + DECLARE v_total_tokens_used INT DEFAULT 0; + DECLARE v_total_revenue DECIMAL(10,2) DEFAULT 0; + + DECLARE EXIT HANDLER FOR SQLEXCEPTION + BEGIN + ROLLBACK; + SET p_success = FALSE; + SET p_message = 'An error occurred while getting statistics'; + END; + + -- Check if admin + IF NOT is_admin(p_admin_id) THEN + SET p_success = FALSE; + SET p_message = 'Access denied: Admin privileges required'; + ELSE + -- Get statistics + SELECT COUNT(*) INTO v_total_users FROM users WHERE deleted_at IS NULL; + SELECT COUNT(*) INTO v_active_users FROM users WHERE is_active = TRUE AND deleted_at IS NULL; + SELECT COALESCE(SUM(jobs_created), 0) INTO v_total_jobs FROM user_usage; + SELECT COALESCE(SUM(interviews_completed), 0) INTO v_total_interviews FROM user_usage; + SELECT COALESCE(SUM(tokens_purchased), 0) INTO v_total_tokens_purchased FROM user_usage; + SELECT COALESCE(SUM(tokens_used), 0) INTO v_total_tokens_used FROM user_usage; + SELECT COALESCE(SUM(amount), 0) INTO v_total_revenue FROM payment_records WHERE status = 'paid'; + + -- Build statistics JSON + SET p_statistics = JSON_OBJECT( + 'total_users', v_total_users, + 'active_users', v_active_users, + 'total_jobs', v_total_jobs, + 'total_interviews', v_total_interviews, + 'total_tokens_purchased', v_total_tokens_purchased, + 'total_tokens_used', v_total_tokens_used, + 'total_revenue', v_total_revenue, + 'generated_at', NOW() + ); + + SET p_success = TRUE; + SET p_message = 'Statistics retrieved successfully'; + END IF; +END ;; +DELIMITER ; +/*!50003 SET sql_mode = @saved_sql_mode */ ; +/*!50003 SET character_set_client = @saved_cs_client */ ; +/*!50003 SET character_set_results = @saved_cs_results */ ; +/*!50003 SET collation_connection = @saved_col_connection */ ; +/*!50003 DROP PROCEDURE IF EXISTS `update_user` */; +/*!50003 SET @saved_cs_client = @@character_set_client */ ; +/*!50003 SET @saved_cs_results = @@character_set_results */ ; +/*!50003 SET @saved_col_connection = @@collation_connection */ ; +/*!50003 SET character_set_client = utf8mb4 */ ; +/*!50003 SET character_set_results = utf8mb4 */ ; +/*!50003 SET collation_connection = utf8mb4_0900_ai_ci */ ; +/*!50003 SET @saved_sql_mode = @@sql_mode */ ; +/*!50003 SET sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION' */ ; +DELIMITER ;; +CREATE DEFINER=`root`@`localhost` PROCEDURE `update_user`( + IN p_user_id VARCHAR(36), + IN p_email VARCHAR(255), + IN p_first_name VARCHAR(100), + IN p_last_name VARCHAR(100), + IN p_role ENUM('admin', 'recruiter'), + IN p_company_name VARCHAR(255), + IN p_is_active BOOLEAN, + IN p_admin_id VARCHAR(36), + OUT p_success BOOLEAN, + OUT p_message VARCHAR(255) +) +BEGIN + DECLARE EXIT HANDLER FOR SQLEXCEPTION + BEGIN + ROLLBACK; + SET p_success = FALSE; + SET p_message = 'An error occurred while updating user'; + END; + + -- Check if admin + IF NOT is_admin(p_admin_id) THEN + SET p_success = FALSE; + SET p_message = 'Access denied: Admin privileges required'; + ELSE + -- Check if user exists + IF NOT EXISTS (SELECT 1 FROM users WHERE id = p_user_id AND deleted_at IS NULL) THEN + SET p_success = FALSE; + SET p_message = 'User not found'; + ELSE + -- Update user + UPDATE users SET + email = p_email, + first_name = p_first_name, + last_name = p_last_name, + role = p_role, + company_name = p_company_name, + is_active = p_is_active, + updated_at = NOW() + WHERE id = p_user_id; + + SET p_success = TRUE; + SET p_message = 'User updated successfully'; + END IF; + END IF; +END ;; +DELIMITER ; +/*!50003 SET sql_mode = @saved_sql_mode */ ; +/*!50003 SET character_set_client = @saved_cs_client */ ; +/*!50003 SET character_set_results = @saved_cs_results */ ; +/*!50003 SET collation_connection = @saved_col_connection */ ; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2025-09-16 20:22:32 diff --git a/database/candidb_dump1/candidb_main_token_packages.sql b/database/candidb_dump1/candidb_main_token_packages.sql new file mode 100644 index 0000000..2f87e24 --- /dev/null +++ b/database/candidb_dump1/candidb_main_token_packages.sql @@ -0,0 +1,55 @@ +CREATE DATABASE IF NOT EXISTS `candidb_main` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */ /*!80016 DEFAULT ENCRYPTION='N' */; +USE `candidb_main`; +-- MySQL dump 10.13 Distrib 8.0.38, for Win64 (x86_64) +-- +-- Host: localhost Database: candidb_main +-- ------------------------------------------------------ +-- Server version 8.0.39 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `token_packages` +-- + +DROP TABLE IF EXISTS `token_packages`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `token_packages` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT (uuid()), + `name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `description` text COLLATE utf8mb4_unicode_ci, + `quantity` int NOT NULL, + `price_per_token` decimal(10,2) NOT NULL, + `total_price` decimal(10,2) NOT NULL, + `discount_percentage` decimal(5,2) DEFAULT '0.00', + `is_popular` tinyint(1) DEFAULT '0', + `is_active` tinyint(1) DEFAULT '1', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + CONSTRAINT `chk_token_packages_discount_valid` CHECK (((`discount_percentage` >= 0) and (`discount_percentage` <= 100))), + CONSTRAINT `chk_token_packages_price_positive` CHECK ((`price_per_token` > 0)), + CONSTRAINT `chk_token_packages_quantity_positive` CHECK ((`quantity` > 0)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2025-09-16 20:22:32 diff --git a/database/candidb_dump1/candidb_main_user_usage.sql b/database/candidb_dump1/candidb_main_user_usage.sql new file mode 100644 index 0000000..fb94297 --- /dev/null +++ b/database/candidb_dump1/candidb_main_user_usage.sql @@ -0,0 +1,53 @@ +CREATE DATABASE IF NOT EXISTS `candidb_main` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */ /*!80016 DEFAULT ENCRYPTION='N' */; +USE `candidb_main`; +-- MySQL dump 10.13 Distrib 8.0.38, for Win64 (x86_64) +-- +-- Host: localhost Database: candidb_main +-- ------------------------------------------------------ +-- Server version 8.0.39 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `user_usage` +-- + +DROP TABLE IF EXISTS `user_usage`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `user_usage` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT (uuid()), + `user_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `jobs_created` int DEFAULT '0', + `interviews_completed` int DEFAULT '0', + `tokens_purchased` int DEFAULT '0', + `tokens_used` int DEFAULT '0', + `last_reset_date` date DEFAULT (curdate()), + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `unique_user_usage` (`user_id`), + CONSTRAINT `user_usage_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + CONSTRAINT `chk_usage_positive` CHECK (((`jobs_created` >= 0) and (`interviews_completed` >= 0) and (`tokens_purchased` >= 0) and (`tokens_used` >= 0))) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2025-09-16 20:22:31 diff --git a/database/candidb_dump1/candidb_main_users.sql b/database/candidb_dump1/candidb_main_users.sql new file mode 100644 index 0000000..1a9e2a4 --- /dev/null +++ b/database/candidb_dump1/candidb_main_users.sql @@ -0,0 +1,60 @@ +CREATE DATABASE IF NOT EXISTS `candidb_main` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */ /*!80016 DEFAULT ENCRYPTION='N' */; +USE `candidb_main`; +-- MySQL dump 10.13 Distrib 8.0.38, for Win64 (x86_64) +-- +-- Host: localhost Database: candidb_main +-- ------------------------------------------------------ +-- Server version 8.0.39 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `users` +-- + +DROP TABLE IF EXISTS `users`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `users` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT (uuid()), + `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `password_hash` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `first_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `last_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `role` enum('admin','recruiter') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'recruiter', + `company_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `avatar_url` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `is_active` tinyint(1) DEFAULT '1', + `last_login_at` timestamp NULL DEFAULT NULL, + `email_verified_at` timestamp NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `email` (`email`), + KEY `idx_email` (`email`), + KEY `idx_role` (`role`), + KEY `idx_active` (`is_active`), + KEY `idx_role_active` (`role`,`is_active`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2025-09-16 20:22:32 diff --git a/database/deploy_dump.sql b/database/deploy_dump.sql new file mode 100644 index 0000000..f2de20d --- /dev/null +++ b/database/deploy_dump.sql @@ -0,0 +1,703 @@ +-- Auto-generated consolidated deployment SQL based on candidb_dump1 + +DROP DATABASE IF EXISTS candidb_main; +CREATE DATABASE IF NOT EXISTS `candidb_main` + CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; +USE `candidb_main`; + +-- Core tables (ordered by dependencies) + +-- users +CREATE TABLE `users` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT (uuid()), + `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `password_hash` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `first_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `last_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `role` enum('admin','recruiter') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'recruiter', + `company_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `avatar_url` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `is_active` tinyint(1) DEFAULT '1', + `last_login_at` timestamp NULL DEFAULT NULL, + `email_verified_at` timestamp NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `email` (`email`), + KEY `idx_email` (`email`), + KEY `idx_role` (`role`), + KEY `idx_active` (`is_active`), + KEY `idx_role_active` (`role`,`is_active`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- token_packages +CREATE TABLE `token_packages` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT (uuid()), + `name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `description` text COLLATE utf8mb4_unicode_ci, + `quantity` int NOT NULL, + `price_per_token` decimal(10,2) NOT NULL, + `total_price` decimal(10,2) NOT NULL, + `discount_percentage` decimal(5,2) DEFAULT '0.00', + `is_popular` tinyint(1) DEFAULT '0', + `is_active` tinyint(1) DEFAULT '1', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + CONSTRAINT `chk_token_packages_discount_valid` CHECK (((`discount_percentage` >= 0) and (`discount_percentage` <= 100))), + CONSTRAINT `chk_token_packages_price_positive` CHECK ((`price_per_token` > 0)), + CONSTRAINT `chk_token_packages_quantity_positive` CHECK ((`quantity` > 0)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- jobs +CREATE TABLE `jobs` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT (uuid()), + `user_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `description` text COLLATE utf8mb4_unicode_ci NOT NULL, + `requirements` text COLLATE utf8mb4_unicode_ci NOT NULL, + `skills_required` json DEFAULT NULL, + `location` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `employment_type` enum('full_time','part_time','contract','internship') COLLATE utf8mb4_unicode_ci DEFAULT 'full_time', + `experience_level` enum('entry','mid','senior','lead','executive') COLLATE utf8mb4_unicode_ci DEFAULT 'mid', + `salary_min` decimal(10,2) DEFAULT NULL, + `salary_max` decimal(10,2) DEFAULT NULL, + `currency` varchar(3) COLLATE utf8mb4_unicode_ci DEFAULT 'USD', + `status` enum('draft','active','paused','closed') COLLATE utf8mb4_unicode_ci DEFAULT 'draft', + `evaluation_criteria` json DEFAULT NULL, + `interview_questions` json DEFAULT NULL, + `interview_style` enum('personal','balanced','technical') COLLATE utf8mb4_unicode_ci DEFAULT 'balanced', + `application_deadline` timestamp NULL DEFAULT NULL, + `icon` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT 'briefcase', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_user_status` (`user_id`,`status`), + KEY `idx_created_at` (`created_at`), + KEY `idx_jobs_user_status_created` (`user_id`,`status`,`created_at` DESC), + CONSTRAINT `jobs_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- candidates +CREATE TABLE `candidates` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT (uuid()), + `user_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `job_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `first_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `last_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `phone` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `resume_url` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `cover_letter` text COLLATE utf8mb4_unicode_ci, + `source` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `status` enum('applied','interviewing','evaluated','hired','rejected') COLLATE utf8mb4_unicode_ci DEFAULT 'applied', + `applied_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `last_activity_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `unique_candidate_per_job` (`job_id`,`email`), + KEY `idx_user_job` (`user_id`,`job_id`), + KEY `idx_status` (`status`), + KEY `idx_candidates_user_job_status` (`user_id`,`job_id`,`status`), + CONSTRAINT `candidates_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + CONSTRAINT `candidates_ibfk_2` FOREIGN KEY (`job_id`) REFERENCES `jobs` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- interviews +CREATE TABLE `interviews` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT (uuid()), + `user_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `candidate_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `job_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `token` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `status` enum('scheduled','in_progress','completed','abandoned') COLLATE utf8mb4_unicode_ci DEFAULT 'scheduled', + `started_at` timestamp NULL DEFAULT NULL, + `completed_at` timestamp NULL DEFAULT NULL, + `duration_minutes` int DEFAULT '0', + `ai_questions` json DEFAULT NULL, + `candidate_responses` json DEFAULT NULL, + `ai_evaluation` json DEFAULT NULL, + `overall_score` decimal(5,2) DEFAULT NULL, + `technical_score` decimal(5,2) DEFAULT NULL, + `communication_score` decimal(5,2) DEFAULT NULL, + `culture_fit_score` decimal(5,2) DEFAULT NULL, + `ai_feedback` text COLLATE utf8mb4_unicode_ci, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `token` (`token`), + KEY `candidate_id` (`candidate_id`), + KEY `job_id` (`job_id`), + KEY `idx_token` (`token`), + KEY `idx_user_candidate` (`user_id`,`candidate_id`), + KEY `idx_status` (`status`), + KEY `idx_interviews_user_status` (`user_id`,`status`), + KEY `idx_interviews_token_status` (`token`,`status`), + CONSTRAINT `interviews_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + CONSTRAINT `interviews_ibfk_2` FOREIGN KEY (`candidate_id`) REFERENCES `candidates` (`id`) ON DELETE CASCADE, + CONSTRAINT `interviews_ibfk_3` FOREIGN KEY (`job_id`) REFERENCES `jobs` (`id`) ON DELETE CASCADE, + CONSTRAINT `chk_scores_valid` CHECK ((((`overall_score` is null) or ((`overall_score` >= 0) and (`overall_score` <= 100))) and ((`technical_score` is null) or ((`technical_score` >= 0) and (`technical_score` <= 100))) and ((`communication_score` is null) or ((`communication_score` >= 0) and (`communication_score` <= 100))) and ((`culture_fit_score` is null) or ((`culture_fit_score` >= 0) and (`culture_fit_score` <= 100))))) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- interview_questions +CREATE TABLE `interview_questions` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT (uuid()), + `interview_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `question_text` text COLLATE utf8mb4_unicode_ci NOT NULL, + `question_type` enum('technical','behavioral','situational','culture_fit') COLLATE utf8mb4_unicode_ci NOT NULL, + `difficulty_level` enum('easy','medium','hard') COLLATE utf8mb4_unicode_ci DEFAULT 'medium', + `expected_answer` text COLLATE utf8mb4_unicode_ci, + `evaluation_criteria` json DEFAULT NULL, + `order_index` int NOT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_interview_order` (`interview_id`,`order_index`), + CONSTRAINT `interview_questions_ibfk_1` FOREIGN KEY (`interview_id`) REFERENCES `interviews` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- candidate_responses +CREATE TABLE `candidate_responses` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT (uuid()), + `interview_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `question_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `response_text` text COLLATE utf8mb4_unicode_ci NOT NULL, + `response_audio_url` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `ai_score` decimal(5,2) DEFAULT NULL, + `ai_feedback` text COLLATE utf8mb4_unicode_ci, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `unique_response_per_question` (`interview_id`,`question_id`), + KEY `question_id` (`question_id`), + CONSTRAINT `candidate_responses_ibfk_1` FOREIGN KEY (`interview_id`) REFERENCES `interviews` (`id`) ON DELETE CASCADE, + CONSTRAINT `candidate_responses_ibfk_2` FOREIGN KEY (`question_id`) REFERENCES `interview_questions` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- interview_tokens +CREATE TABLE `interview_tokens` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT (uuid()), + `user_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `token_type` enum('single','bulk') COLLATE utf8mb4_unicode_ci NOT NULL, + `quantity` int NOT NULL DEFAULT '1', + `price_per_token` decimal(10,2) NOT NULL, + `total_price` decimal(10,2) NOT NULL, + `tokens_used` int DEFAULT '0', + `tokens_remaining` int GENERATED ALWAYS AS ((`quantity` - `tokens_used`)) STORED, + `status` enum('active','exhausted','expired') COLLATE utf8mb4_unicode_ci DEFAULT 'active', + `expires_at` timestamp NULL DEFAULT NULL, + `purchased_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_user_status` (`user_id`,`status`), + KEY `idx_expires_at` (`expires_at`), + KEY `idx_interview_tokens_user_active` (`user_id`,`status`), + CONSTRAINT `interview_tokens_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + CONSTRAINT `chk_interview_tokens_quantity_positive` CHECK ((`quantity` > 0)), + CONSTRAINT `chk_interview_tokens_used_valid` CHECK (((`tokens_used` >= 0) and (`tokens_used` <= `quantity`))) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- payment_records +CREATE TABLE `payment_records` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT (uuid()), + `user_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `token_package_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `amount` decimal(10,2) NOT NULL, + `currency` varchar(3) COLLATE utf8mb4_unicode_ci DEFAULT 'USD', + `status` enum('pending','paid','failed','refunded','cancelled') COLLATE utf8mb4_unicode_ci DEFAULT 'pending', + `payment_method` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `payment_reference` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `invoice_url` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `paid_at` timestamp NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `token_package_id` (`token_package_id`), + KEY `idx_user_status` (`user_id`,`status`), + KEY `idx_payment_reference` (`payment_reference`), + KEY `idx_payment_records_user_created` (`user_id`,`created_at` DESC), + CONSTRAINT `payment_records_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + CONSTRAINT `payment_records_ibfk_2` FOREIGN KEY (`token_package_id`) REFERENCES `token_packages` (`id`) ON DELETE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- user_usage +CREATE TABLE `user_usage` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT (uuid()), + `user_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `jobs_created` int DEFAULT '0', + `interviews_completed` int DEFAULT '0', + `tokens_purchased` int DEFAULT '0', + `tokens_used` int DEFAULT '0', + `last_reset_date` date DEFAULT (curdate()), + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `unique_user_usage` (`user_id`), + CONSTRAINT `user_usage_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + CONSTRAINT `chk_usage_positive` CHECK (((`jobs_created` >= 0) and (`interviews_completed` >= 0) and (`tokens_purchased` >= 0) and (`tokens_used` >= 0))) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- audit_logs +CREATE TABLE `audit_logs` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT (uuid()), + `user_id` varchar(36) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `action` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `resource_type` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `resource_id` varchar(36) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `old_values` json DEFAULT NULL, + `new_values` json DEFAULT NULL, + `ip_address` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `user_agent` text COLLATE utf8mb4_unicode_ci, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_user_action` (`user_id`,`action`), + KEY `idx_resource` (`resource_type`,`resource_id`), + KEY `idx_created_at` (`created_at`), + CONSTRAINT `audit_logs_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- job_links +CREATE TABLE `job_links` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `job_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `url_slug` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `tokens_available` int DEFAULT '0', + `tokens_used` int DEFAULT '0', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `url_slug` (`url_slug`), + KEY `idx_job_id` (`job_id`), + KEY `idx_url_slug` (`url_slug`), + CONSTRAINT `job_links_ibfk_1` FOREIGN KEY (`job_id`) REFERENCES `jobs` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- conversation_messages +CREATE TABLE `conversation_messages` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT (uuid()), + `interview_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `link_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `sender` enum('candidate','ai') COLLATE utf8mb4_unicode_ci NOT NULL, + `message` text COLLATE utf8mb4_unicode_ci NOT NULL, + `message_data` json DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_interview_id` (`interview_id`), + KEY `idx_link_id` (`link_id`), + KEY `idx_created_at` (`created_at`), + CONSTRAINT `conversation_messages_ibfk_1` FOREIGN KEY (`interview_id`) REFERENCES `interviews` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- interview_events +CREATE TABLE `interview_events` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `job_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `link_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `event_type` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `event_data` json DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_job_id` (`job_id`), + KEY `idx_link_id` (`link_id`), + KEY `idx_event_type` (`event_type`), + KEY `idx_created_at` (`created_at`), + CONSTRAINT `interview_events_ibfk_1` FOREIGN KEY (`job_id`) REFERENCES `jobs` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Triggers +DELIMITER $$ +CREATE TRIGGER `update_job_usage_after_insert` AFTER INSERT ON `jobs` FOR EACH ROW BEGIN + INSERT INTO user_usage (user_id, jobs_created) + VALUES (NEW.user_id, 1) + ON DUPLICATE KEY UPDATE jobs_created = jobs_created + 1; +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE TRIGGER `update_token_usage_after_purchase` AFTER INSERT ON `interview_tokens` FOR EACH ROW BEGIN + INSERT INTO user_usage (user_id, tokens_purchased) + VALUES (NEW.user_id, NEW.quantity) + ON DUPLICATE KEY UPDATE tokens_purchased = tokens_purchased + NEW.quantity; +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE TRIGGER `update_interview_usage_after_complete` AFTER UPDATE ON `interviews` FOR EACH ROW BEGIN + IF OLD.status != 'completed' AND NEW.status = 'completed' THEN + INSERT INTO user_usage (user_id, interviews_completed, tokens_used) + VALUES (NEW.user_id, 1, 1) + ON DUPLICATE KEY UPDATE + interviews_completed = interviews_completed + 1, + tokens_used = tokens_used + 1; + END IF; +END$$ +DELIMITER ; + +-- Functions +DELIMITER $$ +CREATE FUNCTION `can_create_job`(user_uuid VARCHAR(36)) RETURNS tinyint(1) + READS SQL DATA + DETERMINISTIC +BEGIN + DECLARE current_jobs INT DEFAULT 0; + DECLARE max_jobs INT DEFAULT 100; + SELECT COALESCE(jobs_created, 0) INTO current_jobs FROM user_usage WHERE user_id = user_uuid; + RETURN current_jobs < max_jobs; +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE FUNCTION `get_all_users`() RETURNS json + READS SQL DATA + DETERMINISTIC +BEGIN + DECLARE result JSON; + SELECT JSON_ARRAYAGG( + JSON_OBJECT( + 'id', id, + 'email', email, + 'first_name', first_name, + 'last_name', last_name, + 'role', role, + 'company_name', company_name, + 'is_active', is_active, + 'last_login_at', last_login_at, + 'email_verified_at', email_verified_at, + 'created_at', created_at + ) + ) INTO result + FROM users + WHERE deleted_at IS NULL + ORDER BY created_at DESC; + RETURN result; +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE FUNCTION `get_token_usage_summary`(user_uuid VARCHAR(36)) RETURNS json + READS SQL DATA + DETERMINISTIC +BEGIN + DECLARE total_purchased INT DEFAULT 0; + DECLARE total_used INT DEFAULT 0; + DECLARE total_available INT DEFAULT 0; + DECLARE result JSON; + SELECT COALESCE(SUM(quantity), 0) INTO total_purchased FROM interview_tokens WHERE user_id = user_uuid; + SELECT COALESCE(SUM(tokens_used), 0) INTO total_used FROM interview_tokens WHERE user_id = user_uuid; + SELECT COALESCE(SUM(tokens_remaining), 0) INTO total_available FROM interview_tokens WHERE user_id = user_uuid AND status = 'active' AND (expires_at IS NULL OR expires_at > NOW()); + SET result = JSON_OBJECT( + 'total_purchased', total_purchased, + 'total_used', total_used, + 'total_available', total_available, + 'utilization_percentage', CASE WHEN total_purchased > 0 THEN ROUND((total_used / total_purchased) * 100, 2) ELSE 0 END + ); + RETURN result; +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE FUNCTION `get_user_statistics`(user_uuid VARCHAR(36)) RETURNS json + READS SQL DATA + DETERMINISTIC +BEGIN + DECLARE result JSON; + DECLARE user_usage_data JSON; + DECLARE token_summary JSON; + SELECT JSON_OBJECT( + 'jobs_created', COALESCE(jobs_created, 0), + 'interviews_completed', COALESCE(interviews_completed, 0), + 'tokens_purchased', COALESCE(tokens_purchased, 0), + 'tokens_used', COALESCE(tokens_used, 0) + ) INTO user_usage_data FROM user_usage WHERE user_id = user_uuid; + SELECT get_token_usage_summary(user_uuid) INTO token_summary; + SET result = JSON_OBJECT('usage', user_usage_data, 'tokens', token_summary); + RETURN result; +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE FUNCTION `has_available_tokens`(user_uuid VARCHAR(36)) RETURNS tinyint(1) + READS SQL DATA + DETERMINISTIC +BEGIN + DECLARE available_tokens INT DEFAULT 0; + SELECT COALESCE(SUM(tokens_remaining), 0) INTO available_tokens FROM interview_tokens WHERE user_id = user_uuid AND status = 'active' AND (expires_at IS NULL OR expires_at > NOW()); + RETURN available_tokens > 0; +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE FUNCTION `is_admin`(user_uuid VARCHAR(36)) RETURNS tinyint(1) + READS SQL DATA + DETERMINISTIC +BEGIN + DECLARE user_role VARCHAR(20) DEFAULT NULL; + SELECT role INTO user_role FROM users WHERE id = user_uuid AND is_active = TRUE; + RETURN user_role = 'admin'; +END$$ +DELIMITER ; + +-- Procedures (from dump routines) +DELIMITER $$ +CREATE PROCEDURE `add_tokens_to_user`( + IN p_user_id VARCHAR(36), + IN p_quantity INT, + IN p_price_per_token DECIMAL(10,2), + IN p_admin_id VARCHAR(36), + OUT p_success BOOLEAN, + OUT p_message VARCHAR(255) +) +BEGIN + DECLARE v_total_price DECIMAL(10,2); + DECLARE v_token_id VARCHAR(36); + DECLARE EXIT HANDLER FOR SQLEXCEPTION + BEGIN + ROLLBACK; + SET p_success = FALSE; + SET p_message = 'An error occurred while adding tokens'; + END; + IF NOT is_admin(p_admin_id) THEN + SET p_success = FALSE; + SET p_message = 'Access denied: Admin privileges required'; + ELSE + IF NOT EXISTS (SELECT 1 FROM users WHERE id = p_user_id AND deleted_at IS NULL) THEN + SET p_success = FALSE; + SET p_message = 'User not found'; + ELSE + SET v_total_price = p_quantity * p_price_per_token; + SET v_token_id = UUID(); + INSERT INTO interview_tokens ( + id, user_id, token_type, quantity, price_per_token, + total_price, status, purchased_at + ) VALUES ( + v_token_id, p_user_id, + CASE WHEN p_quantity = 1 THEN 'single' ELSE 'bulk' END, + p_quantity, p_price_per_token, v_total_price, + 'active', NOW() + ); + -- NOTE: routines in dump referenced interview_token_id; schema doesn't have it. Keeping minimal insert + INSERT INTO payment_records ( + user_id, token_package_id, amount, status, payment_method, payment_reference + ) VALUES ( + p_user_id, NULL, v_total_price, + 'paid', 'admin_granted', CONCAT('ADMIN_', p_admin_id, '_', NOW()) + ); + SET p_success = TRUE; + SET p_message = CONCAT('Successfully added ', p_quantity, ' tokens to user'); + END IF; + END IF; +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE PROCEDURE `change_user_password`( + IN p_user_id VARCHAR(36), + IN p_new_password_hash VARCHAR(255), + IN p_admin_id VARCHAR(36), + OUT p_success BOOLEAN, + OUT p_message VARCHAR(255) +) +BEGIN + DECLARE EXIT HANDLER FOR SQLEXCEPTION + BEGIN + ROLLBACK; + SET p_success = FALSE; + SET p_message = 'An error occurred while changing password'; + END; + IF NOT is_admin(p_admin_id) THEN + SET p_success = FALSE; + SET p_message = 'Access denied: Admin privileges required'; + ELSE + IF NOT EXISTS (SELECT 1 FROM users WHERE id = p_user_id AND deleted_at IS NULL) THEN + SET p_success = FALSE; + SET p_message = 'User not found'; + ELSE + UPDATE users SET + password_hash = p_new_password_hash, + updated_at = NOW() + WHERE id = p_user_id; + SET p_success = TRUE; + SET p_message = 'Password changed successfully'; + END IF; + END IF; +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE PROCEDURE `create_user`( + IN p_email VARCHAR(255), + IN p_password_hash VARCHAR(255), + IN p_first_name VARCHAR(100), + IN p_last_name VARCHAR(100), + IN p_role ENUM('admin', 'recruiter'), + IN p_company_name VARCHAR(255), + IN p_admin_id VARCHAR(36), + OUT p_user_id VARCHAR(36), + OUT p_success BOOLEAN, + OUT p_message VARCHAR(255) +) +BEGIN + DECLARE EXIT HANDLER FOR SQLEXCEPTION + BEGIN + ROLLBACK; + SET p_success = FALSE; + SET p_message = 'An error occurred while creating user'; + END; + IF NOT is_admin(p_admin_id) THEN + SET p_success = FALSE; + SET p_message = 'Access denied: Admin privileges required'; + ELSE + IF EXISTS (SELECT 1 FROM users WHERE email = p_email AND deleted_at IS NULL) THEN + SET p_success = FALSE; + SET p_message = 'Email already exists'; + ELSE + SET p_user_id = UUID(); + INSERT INTO users ( + id, email, password_hash, first_name, last_name, + role, company_name, is_active, email_verified_at + ) VALUES ( + p_user_id, p_email, p_password_hash, p_first_name, p_last_name, + p_role, p_company_name, TRUE, NOW() + ); + INSERT INTO user_usage (user_id) VALUES (p_user_id); + SET p_success = TRUE; + SET p_message = 'User created successfully'; + END IF; + END IF; +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE PROCEDURE `deactivate_user`( + IN p_user_id VARCHAR(36), + IN p_admin_id VARCHAR(36), + OUT p_success BOOLEAN, + OUT p_message VARCHAR(255) +) +BEGIN + DECLARE EXIT HANDLER FOR SQLEXCEPTION + BEGIN + ROLLBACK; + SET p_success = FALSE; + SET p_message = 'An error occurred while deactivating user'; + END; + IF NOT is_admin(p_admin_id) THEN + SET p_success = FALSE; + SET p_message = 'Access denied: Admin privileges required'; + ELSE + IF NOT EXISTS (SELECT 1 FROM users WHERE id = p_user_id AND deleted_at IS NULL) THEN + SET p_success = FALSE; + SET p_message = 'User not found'; + ELSE + UPDATE users SET + is_active = FALSE, + updated_at = NOW() + WHERE id = p_user_id; + SET p_success = TRUE; + SET p_message = 'User deactivated successfully'; + END IF; + END IF; +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE PROCEDURE `get_system_statistics`( + IN p_admin_id VARCHAR(36), + OUT p_success BOOLEAN, + OUT p_message VARCHAR(255), + OUT p_statistics JSON +) +BEGIN + DECLARE v_total_users INT DEFAULT 0; + DECLARE v_active_users INT DEFAULT 0; + DECLARE v_total_jobs INT DEFAULT 0; + DECLARE v_total_interviews INT DEFAULT 0; + DECLARE v_total_tokens_purchased INT DEFAULT 0; + DECLARE v_total_tokens_used INT DEFAULT 0; + DECLARE v_total_revenue DECIMAL(10,2) DEFAULT 0; + DECLARE EXIT HANDLER FOR SQLEXCEPTION + BEGIN + ROLLBACK; + SET p_success = FALSE; + SET p_message = 'An error occurred while getting statistics'; + END; + IF NOT is_admin(p_admin_id) THEN + SET p_success = FALSE; + SET p_message = 'Access denied: Admin privileges required'; + ELSE + SELECT COUNT(*) INTO v_total_users FROM users WHERE deleted_at IS NULL; + SELECT COUNT(*) INTO v_active_users FROM users WHERE is_active = TRUE AND deleted_at IS NULL; + SELECT COALESCE(SUM(jobs_created), 0) INTO v_total_jobs FROM user_usage; + SELECT COALESCE(SUM(interviews_completed), 0) INTO v_total_interviews FROM user_usage; + SELECT COALESCE(SUM(tokens_purchased), 0) INTO v_total_tokens_purchased FROM user_usage; + SELECT COALESCE(SUM(tokens_used), 0) INTO v_total_tokens_used FROM user_usage; + SELECT COALESCE(SUM(amount), 0) INTO v_total_revenue FROM payment_records WHERE status = 'paid'; + SET p_statistics = JSON_OBJECT( + 'total_users', v_total_users, + 'active_users', v_active_users, + 'total_jobs', v_total_jobs, + 'total_interviews', v_total_interviews, + 'total_tokens_purchased', v_total_tokens_purchased, + 'total_tokens_used', v_total_tokens_used, + 'total_revenue', v_total_revenue, + 'generated_at', NOW() + ); + SET p_success = TRUE; + SET p_message = 'Statistics retrieved successfully'; + END IF; +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE PROCEDURE `update_user`( + IN p_user_id VARCHAR(36), + IN p_email VARCHAR(255), + IN p_first_name VARCHAR(100), + IN p_last_name VARCHAR(100), + IN p_role ENUM('admin', 'recruiter'), + IN p_company_name VARCHAR(255), + IN p_is_active BOOLEAN, + IN p_admin_id VARCHAR(36), + OUT p_success BOOLEAN, + OUT p_message VARCHAR(255) +) +BEGIN + DECLARE EXIT HANDLER FOR SQLEXCEPTION + BEGIN + ROLLBACK; + SET p_success = FALSE; + SET p_message = 'An error occurred while updating user'; + END; + IF NOT is_admin(p_admin_id) THEN + SET p_success = FALSE; + SET p_message = 'Access denied: Admin privileges required'; + ELSE + IF NOT EXISTS (SELECT 1 FROM users WHERE id = p_user_id AND deleted_at IS NULL) THEN + SET p_success = FALSE; + SET p_message = 'User not found'; + ELSE + UPDATE users SET + email = p_email, + first_name = p_first_name, + last_name = p_last_name, + role = p_role, + company_name = p_company_name, + is_active = p_is_active, + updated_at = NOW() + WHERE id = p_user_id; + SET p_success = TRUE; + SET p_message = 'User updated successfully'; + END IF; + END IF; +END$$ +DELIMITER ; + +-- Insert default admin user (password: admin123 - CHANGE THIS!) +INSERT INTO users (id, email, password_hash, first_name, last_name, role, is_active, email_verified_at) VALUES +(UUID(), 'admin@candivista.com', '$2b$10$rcKrXbkDjjjT3vA3kMH78OkyUFNTn6nuCsqK90JEA2.S2p0dVjFUi', 'Admin', 'User', 'admin', TRUE, NOW()); + + diff --git a/docker-compose.yml b/docker-compose.yml index 1e776b9..e273d6d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,191 @@ version: '3.8' + services: - aisapp2: - build: . - ports: - - "5000:80" - working_dir: /app - command: ["dotnet", "run", "--project", "AISApp", "--urls", "http://0.0.0.0:80"] - volumes: - - .:/app + # Database Service + database: + build: + context: ./database + dockerfile: Dockerfile + image: candidat/database:${APP_VERSION:-latest} + container_name: candidat-database environment: - - ASPNETCORE_ENVIRONMENT=Development - - OPENROUTER_API_KEY=${OPENROUTER_API_KEY} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + ports: + - "${DB_PORT:-3307}:3306" + - "${DB_X_PORT:-33061}:33060" # MySQL X Protocol for development + volumes: + - db_data:/var/lib/mysql + networks: + - candidat-network + restart: unless-stopped + command: --default-authentication-plugin=mysql_native_password + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + timeout: 20s + retries: 10 + deploy: + resources: + limits: + memory: 1G + reservations: + memory: 512M + + # Backend Service + backend: + build: + context: ./backend + dockerfile: Dockerfile + image: candidat/backend:${APP_VERSION:-latest} + container_name: candidat-backend + environment: + NODE_ENV: ${NODE_ENV:-production} + DB_HOST: database + DB_PORT: 3306 + DB_NAME: ${MYSQL_DATABASE} + DB_USER: ${MYSQL_USER} + DB_PASSWORD: ${MYSQL_PASSWORD} + AI_PROVIDER: ${AI_PROVIDER} + OPENROUTER_API_KEY: ${OPENROUTER_API_KEY} + OPENROUTER_MODEL: ${OPENROUTER_MODEL} + OPENROUTER_BASE_URL: ${OPENROUTER_BASE_URL} + OPENROUTER_REL_PATH: ${OPENROUTER_REL_PATH} + OPENROUTER_TEMPERATURE: ${OPENROUTER_TEMPERATURE} + AI_PORT: ${AI_PORT} + AI_MODEL: ${AI_MODEL} + ports: + - "${BACKEND_PORT:-8083}:8083" + volumes: + # Development hot reloading (only if NODE_ENV=development) + - ./backend/src:/app/src:ro + depends_on: + database: + condition: service_healthy + networks: + - candidat-network + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8083/rest/ai/test-ai"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + deploy: + resources: + limits: + memory: 512M + reservations: + memory: 256M + + # Frontend Service + frontend: + build: + context: ./frontend/candidat-frontend + dockerfile: Dockerfile + image: candidat/frontend:${APP_VERSION:-latest} + container_name: candidat-frontend + environment: + NODE_ENV: ${NODE_ENV:-production} + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL} + ports: + - "${FRONTEND_PORT:-3000}:3000" + volumes: + # Development hot reloading (only if NODE_ENV=development) + - ./frontend/candidat-frontend/src:/app/src:ro + depends_on: + backend: + condition: service_healthy + networks: + - candidat-network + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + deploy: + resources: + limits: + memory: 256M + reservations: + memory: 128M + + # Chatbot Service + chatbot: + build: + context: ./tuna/tuna + dockerfile: Dockerfile + image: candidat/chatbot:${APP_VERSION:-latest} + container_name: candidat-chatbot + environment: + ASPNETCORE_ENVIRONMENT: ${NODE_ENV:-production} + OPENROUTER_API_KEY: ${OPENROUTER_API_KEY} + CHATBOT_DB_HOST: database + CHATBOT_DB_NAME: ${MYSQL_DATABASE} + CHATBOT_DB_USER: ${MYSQL_USER} + CHATBOT_DB_PASSWORD: ${MYSQL_PASSWORD} + CHATBOT_DB_PORT: 3306 + ports: + - "${CHATBOT_PORT:-5000}:80" + depends_on: + database: + condition: service_healthy + networks: + - candidat-network + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/api/chat"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + deploy: + resources: + limits: + memory: 512M + reservations: + memory: 256M + + # Nginx Reverse Proxy + nginx: + image: nginx:alpine + container_name: candidat-nginx + ports: + - "${NGINX_PORT:-80}:80" + - "${NGINX_SSL_PORT:-443}:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/ssl:/etc/nginx/ssl:ro + - nginx_logs:/var/log/nginx + depends_on: + - frontend + - backend + - chatbot + networks: + - candidat-network + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + deploy: + resources: + limits: + memory: 128M + reservations: + memory: 64M + +volumes: + db_data: + driver: local + nginx_logs: + driver: local + +networks: + candidat-network: + driver: bridge diff --git a/env.cloudflare b/env.cloudflare new file mode 100644 index 0000000..f3816a1 --- /dev/null +++ b/env.cloudflare @@ -0,0 +1,38 @@ +# Cloudflare Environment Configuration for VPS +APP_VERSION=1.0.0 +NODE_ENV=production + +# Database (Docker) +MYSQL_ROOT_PASSWORD=your_secure_root_password_here +MYSQL_DATABASE=candidb_main +MYSQL_USER=candidat +MYSQL_PASSWORD=your_secure_db_password_here + +# Database ports (internal only, not exposed externally) +DB_PORT=3306 +DB_X_PORT=33060 + +# Application URLs and Ports (Cloudflare handles SSL termination) +NEXT_PUBLIC_API_URL=https://candivista.com +BACKEND_PORT=8083 +FRONTEND_PORT=3000 +NGINX_PORT=80 +NGINX_SSL_PORT=443 + +# AI Configuration +AI_PROVIDER=openrouter +OPENROUTER_API_KEY=your_openrouter_api_key_here +OPENROUTER_MODEL=gemma +OPENROUTER_BASE_URL=openrouter.ai +OPENROUTER_REL_PATH=/api +OPENROUTER_TEMPERATURE=0.7 + +# Fallback AI (if needed) +AI_PORT=11434 +AI_MODEL=gpt-oss:20b + +# Chatbot Service Configuration +CHATBOT_SERVICE_URL=http://chatbot:80 +CHATBOT_SERVICE_TIMEOUT=30000 +CHATBOT_FALLBACK_ENABLED=true +CHATBOT_PORT=5000 diff --git a/env.example b/env.example new file mode 100644 index 0000000..70d73c5 --- /dev/null +++ b/env.example @@ -0,0 +1,38 @@ +# Environment +APP_VERSION=1.0.0 +NODE_ENV=development + +# Database (Docker) +MYSQL_ROOT_PASSWORD=musicisoverrated +MYSQL_DATABASE=candidb_main +MYSQL_USER=candidat +MYSQL_PASSWORD=StrongLocalDevPass123 + +DB_PORT=3306 +DB_X_PORT=33060 + +# Application URLs and Ports +NEXT_PUBLIC_API_URL=https://candivista.com +BACKEND_PORT=8083 +FRONTEND_PORT=3000 +NGINX_PORT=80 +NGINX_SSL_PORT=443 + +# AI Configuration +AI_PROVIDER=openrouter +OPENROUTER_API_KEY="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" +OPENROUTER_MODEL=gemma # or any model from your predefined list +OPENROUTER_BASE_URL=openrouter.ai +OPENROUTER_REL_PATH=/api +OPENROUTER_TEMPERATURE=0.7 + +# Fallback AI (if needed) +AI_PORT=11434 +AI_MODEL=gpt-oss:20b + +# Chatbot Service Configuration +CHATBOT_SERVICE_URL=http://chatbot:80 +CHATBOT_SERVICE_TIMEOUT=30000 +CHATBOT_FALLBACK_ENABLED=true +CHATBOT_PORT=5000 + diff --git a/env.production b/env.production new file mode 100644 index 0000000..c7612db --- /dev/null +++ b/env.production @@ -0,0 +1,38 @@ +# Production Environment Configuration for VPS +APP_VERSION=1.0.0 +NODE_ENV=production + +# Database (Docker) +MYSQL_ROOT_PASSWORD=your_secure_root_password_here +MYSQL_DATABASE=candidb_main +MYSQL_USER=candidat +MYSQL_PASSWORD=your_secure_db_password_here + +# Database ports (internal only, not exposed externally) +DB_PORT=3306 +DB_X_PORT=33060 + +# Application URLs and Ports +NEXT_PUBLIC_API_URL=https://candivista.com +BACKEND_PORT=8083 +FRONTEND_PORT=3000 +NGINX_PORT=80 +NGINX_SSL_PORT=443 + +# AI Configuration +AI_PROVIDER=openrouter +OPENROUTER_API_KEY=your_openrouter_api_key_here +OPENROUTER_MODEL=gemma +OPENROUTER_BASE_URL=openrouter.ai +OPENROUTER_REL_PATH=/api +OPENROUTER_TEMPERATURE=0.7 + +# Fallback AI (if needed) +AI_PORT=11434 +AI_MODEL=gpt-oss:20b + +# Chatbot Service Configuration +CHATBOT_SERVICE_URL=http://chatbot:80 +CHATBOT_SERVICE_TIMEOUT=30000 +CHATBOT_FALLBACK_ENABLED=true +CHATBOT_PORT=5000 diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..4bc54a9 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,13 @@ +node_modules +.next +.git +.gitignore +README.md +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +.DS_Store +*.log + diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..7f6bbb4 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,55 @@ +# Multi-stage build for Next.js +FROM node:18-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Copy package files +COPY package*.json ./ +RUN npm ci + +# Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Build the application +RUN npm run build + +# Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production + +# Install curl for health checks +RUN apk add --no-cache curl + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public + +# Set the correct permission for prerender cache +RUN mkdir .next +RUN chown nextjs:nodejs .next + +# Automatically leverage output traces to reduce image size +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:3000 || exit 1 + +CMD ["node", "server.js"] \ No newline at end of file diff --git a/frontend/FRONTEND_README.md b/frontend/FRONTEND_README.md new file mode 100644 index 0000000..552d542 --- /dev/null +++ b/frontend/FRONTEND_README.md @@ -0,0 +1,258 @@ +# Candivista Frontend - Modern AI Recruitment Platform + +## 🎨 **Beautiful, Modern Frontend** + +A stunning, responsive frontend built with Next.js 15, TypeScript, and Tailwind CSS that showcases the complete Candivista AI-powered recruitment platform. + +## ✨ **Key Features** + +### 🎯 **Modern Design** +- **Gradient animations** and smooth transitions +- **Glass morphism effects** with backdrop blur +- **Interactive hover animations** and micro-interactions +- **Responsive design** for all devices +- **Custom CSS animations** for enhanced UX + +### 🚀 **Performance Optimized** +- **Next.js 15** with App Router +- **TypeScript** for type safety +- **Tailwind CSS** for utility-first styling +- **Optimized images** and lazy loading +- **Smooth scrolling** and navigation + +### 🎭 **Interactive Components** +- **AnimatedCounter** - Smooth number animations +- **FeatureCard** - Hover effects and gradients +- **PricingCard** - Interactive pricing plans +- **TechStackCard** - Technology showcase + +## 🏗️ **Project Structure** + +``` +frontend/candidat-frontend/ +├── src/ +│ ├── app/ +│ │ ├── page.tsx # Main landing page +│ │ ├── globals.css # Global styles & animations +│ │ ├── layout.tsx # Root layout +│ │ └── favicon.ico +│ └── components/ +│ ├── AnimatedCounter.tsx # Animated number counter +│ ├── FeatureCard.tsx # Feature showcase card +│ ├── PricingCard.tsx # Pricing plan card +│ └── TechStackCard.tsx # Technology stack card +├── public/ # Static assets +├── package.json +├── next.config.js +├── tailwind.config.js +└── tsconfig.json +``` + +## 🎨 **Design System** + +### **Color Palette** +- **Primary**: Blue (#3B82F6) to Indigo (#6366F1) +- **Secondary**: Purple (#8B5CF6) to Pink (#EC4899) +- **Accent**: Green (#10B981) for success states +- **Neutral**: Gray scale for text and backgrounds + +### **Typography** +- **Headings**: Bold, large sizes with gradient text +- **Body**: Clean, readable font with proper line height +- **Responsive**: Scales appropriately on all devices + +### **Animations** +- **Gradient animations** for text and backgrounds +- **Hover effects** with scale and shadow transitions +- **Smooth scrolling** between sections +- **Loading animations** with custom spinners + +## 🚀 **Getting Started** + +### **Prerequisites** +- Node.js 18+ +- npm or yarn +- Next.js 15 + +### **Installation** +```bash +# Install dependencies +npm install + +# Start development server +npm run dev + +# Build for production +npm run build + +# Start production server +npm start +``` + +### **Development** +```bash +# Run with hot reload +npm run dev + +# Type checking +npm run type-check + +# Linting +npm run lint +``` + +## 🎯 **Key Sections** + +### **1. Hero Section** +- **Compelling headline** with gradient text animation +- **Clear value proposition** for AI recruitment +- **Call-to-action buttons** with hover effects +- **Interactive illustration** showing the workflow + +### **2. Features Section** +- **Multi-tenant architecture** explanation +- **Flexible link system** showcase +- **AI-powered intelligence** highlights +- **Visual dashboard mockup** with animations + +### **3. Pricing Section** +- **Token-based pricing** with clear tiers +- **Interactive pricing cards** with hover effects +- **Feature comparison** for each plan +- **Popular plan highlighting** + +### **4. Technology Stack** +- **Modern tech showcase** with icons +- **Hover animations** for each technology +- **Performance benefits** explanation +- **Developer experience** highlights + +### **5. Stats Section** +- **Animated counters** showing platform success +- **Trust indicators** for credibility +- **Social proof** elements + +### **6. Call-to-Action** +- **Compelling final CTA** with gradient background +- **Multiple action options** for different users +- **Urgency and value** messaging + +## 🎨 **Custom Animations** + +### **CSS Animations** +```css +/* Gradient text animation */ +.animate-gradient-x { + animation: gradient-x 3s ease infinite; +} + +/* Floating animation */ +.animate-float { + animation: float 6s ease-in-out infinite; +} + +/* Pulse glow effect */ +.animate-pulse-glow { + animation: pulse-glow 2s ease-in-out infinite; +} +``` + +### **Component Animations** +- **Staggered animations** for feature cards +- **Hover transformations** for interactive elements +- **Smooth transitions** between states +- **Loading states** with custom spinners + +## 📱 **Responsive Design** + +### **Breakpoints** +- **Mobile**: 320px - 768px +- **Tablet**: 768px - 1024px +- **Desktop**: 1024px+ + +### **Mobile Optimizations** +- **Touch-friendly** button sizes +- **Optimized typography** for small screens +- **Swipe gestures** for carousels +- **Fast loading** on mobile networks + +## 🎯 **Performance Features** + +### **Optimization** +- **Image optimization** with Next.js Image component +- **Code splitting** for faster loading +- **Lazy loading** for below-the-fold content +- **Minimal bundle size** with tree shaking + +### **SEO Ready** +- **Semantic HTML** structure +- **Meta tags** for social sharing +- **Structured data** for search engines +- **Fast loading** for better rankings + +## 🔧 **Customization** + +### **Theming** +- **CSS variables** for easy color changes +- **Tailwind config** for design system +- **Component props** for flexibility +- **Dark mode** support ready + +### **Content Management** +- **Easy text updates** in components +- **Image replacement** in public folder +- **Configuration** in separate files +- **Environment variables** for API URLs + +## 🚀 **Deployment** + +### **Production Build** +```bash +# Build optimized production bundle +npm run build + +# Start production server +npm start +``` + +### **Docker Support** +```dockerfile +# Multi-stage build for optimization +FROM node:18-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci --only=production +COPY . . +RUN npm run build + +FROM node:18-alpine AS runner +WORKDIR /app +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/public ./public +COPY --from=builder /app/package*.json ./ +EXPOSE 3000 +CMD ["npm", "start"] +``` + +## 🎉 **Result** + +A stunning, modern frontend that: +- ✅ **Showcases** the complete Candivista platform +- ✅ **Engages** users with beautiful animations +- ✅ **Converts** visitors with clear value propositions +- ✅ **Performs** excellently on all devices +- ✅ **Scales** for future feature additions + +The frontend perfectly represents the sophisticated AI recruitment platform with a professional, modern design that will impress users and drive conversions. + +## 📞 **Support** + +For questions or support regarding the frontend: +- **Documentation**: Check component READMEs +- **Issues**: Create GitHub issues +- **Contributions**: Submit pull requests +- **Contact**: Reach out to the development team + +--- + +**Built with ❤️ using Next.js 15, TypeScript, and Tailwind CSS** diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/frontend/next.config.js b/frontend/next.config.js new file mode 100644 index 0000000..816ffe8 --- /dev/null +++ b/frontend/next.config.js @@ -0,0 +1,24 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + // Only use standalone output for Docker builds + ...(process.env.NODE_ENV === 'production' && { output: 'standalone' }), + + env: { + NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'https://candivista.com', + }, + + // Only add rewrites for production (Docker) + ...(process.env.NODE_ENV === 'production' && { + async rewrites() { + const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'https://candivista.com'; + return [ + { + source: '/rest/:path*', + destination: `${apiUrl}/rest/:path*`, + }, + ]; + }, + }), +} + +module.exports = nextConfig diff --git a/frontend/next.config.ts b/frontend/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/frontend/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..164d3b0 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3761 @@ +{ + "name": "candivista-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "candivista-frontend", + "version": "0.1.0", + "dependencies": { + "@hookform/resolvers": "^5.2.1", + "axios": "^1.11.0", + "next": "15.5.2", + "next-themes": "^0.4.6", + "react": "19.1.0", + "react-dom": "19.1.0", + "react-hook-form": "^7.62.0", + "swagger-ui-react": "^5.29.0", + "zod": "^4.1.5" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "@types/swagger-ui-react": "^5.18.0", + "tailwindcss": "^4", + "typescript": "^5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz", + "integrity": "sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==", + "dependencies": { + "core-js-pure": "^3.43.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@hookform/resolvers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.1.tgz", + "integrity": "sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ==", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", + "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", + "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", + "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", + "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", + "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", + "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", + "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", + "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", + "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", + "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", + "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", + "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", + "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", + "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", + "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", + "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.4" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", + "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", + "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", + "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.2.tgz", + "integrity": "sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg==" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.2.tgz", + "integrity": "sha512-8bGt577BXGSd4iqFygmzIfTYizHb0LGWqH+qgIF/2EDxS5JsSdERJKA8WgwDyNBZgTIIA4D8qUtoQHmxIIquoQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.2.tgz", + "integrity": "sha512-2DjnmR6JHK4X+dgTXt5/sOCu/7yPtqpYt8s8hLkHFK3MGkka2snTv3yRMdHvuRtJVkPwCGsvBSwmoQCHatauFQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.2.tgz", + "integrity": "sha512-3j7SWDBS2Wov/L9q0mFJtEvQ5miIqfO4l7d2m9Mo06ddsgUK8gWfHGgbjdFlCp2Ek7MmMQZSxpGFqcC8zGh2AA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.2.tgz", + "integrity": "sha512-s6N8k8dF9YGc5T01UPQ08yxsK6fUow5gG1/axWc1HVVBYQBgOjca4oUZF7s4p+kwhkB1bDSGR8QznWrFZ/Rt5g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.2.tgz", + "integrity": "sha512-o1RV/KOODQh6dM6ZRJGZbc+MOAHww33Vbs5JC9Mp1gDk8cpEO+cYC/l7rweiEalkSm5/1WGa4zY7xrNwObN4+Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.2.tgz", + "integrity": "sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.2.tgz", + "integrity": "sha512-sMPyTvRcNKXseNQ/7qRfVRLa0VhR0esmQ29DD6pqvG71+JdVnESJaHPA8t7bc67KD5spP3+DOCNLhqlEI2ZgQg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.2.tgz", + "integrity": "sha512-W5VvyZHnxG/2ukhZF/9Ikdra5fdNftxI6ybeVKYvBPDtyx7x4jPPSNduUkfH5fo3zG0JQ0bPxgy41af2JX5D4Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==" + }, + "node_modules/@swagger-api/apidom-ast": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ast/-/apidom-ast-1.0.0-beta.48.tgz", + "integrity": "sha512-Euyqg5ueJEaNP3N0Gq62fl/713QM0EgmI03Jp2lJRCB7ew7QHfqrXKXnjVKUj450pZ54itmUPHEqg0U8VwgqIA==", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-error": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "unraw": "^3.0.0" + } + }, + "node_modules/@swagger-api/apidom-core": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-core/-/apidom-core-1.0.0-beta.48.tgz", + "integrity": "sha512-J5WWbd0Hhr3l9NgiqaPLDbStEtgcL0gsFwwt/CkaprCfTR4+jRoqs8kpAxO1mR2+MNLsdNeTbEdQQ3quD6L41Q==", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-ast": "^1.0.0-beta.48", + "@swagger-api/apidom-error": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "minim": "~0.23.8", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "short-unique-id": "^5.3.2", + "ts-mixer": "^6.0.3" + } + }, + "node_modules/@swagger-api/apidom-error": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-error/-/apidom-error-1.0.0-beta.48.tgz", + "integrity": "sha512-f6zrC0D2hcOn+pHSOvL/uOrDTLAOVTOoi3OEA9NtxOwzJS6pJV1epG95Xn7CLSnZJoG/DRk4WMyWDUFbseekHg==", + "dependencies": { + "@babel/runtime-corejs3": "^7.20.7" + } + }, + "node_modules/@swagger-api/apidom-json-pointer": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-json-pointer/-/apidom-json-pointer-1.0.0-beta.48.tgz", + "integrity": "sha512-IvMZnNjHu+PdQ1hrAnNRewnxYp6WeZm488plnlmgz+BDQ/KQYfBtJ/5oqa+YGuitcNlJEl30g8CHsWOEsDvNwQ==", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-error": "^1.0.0-beta.48", + "@swaggerexpert/json-pointer": "^2.10.1" + } + }, + "node_modules/@swagger-api/apidom-ns-api-design-systems": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-api-design-systems/-/apidom-ns-api-design-systems-1.0.0-beta.48.tgz", + "integrity": "sha512-261lrM+dO00UkKVrdFJAuE6YIJnf1PsIVqBUHKbn8qND9MPl4vLcznoa0Uj2g/Adul723OxcTzXDiaGPNnJ/1A==", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-error": "^1.0.0-beta.48", + "@swagger-api/apidom-ns-openapi-3-1": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.3" + } + }, + "node_modules/@swagger-api/apidom-ns-arazzo-1": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-arazzo-1/-/apidom-ns-arazzo-1-1.0.0-beta.48.tgz", + "integrity": "sha512-8vrBh8/OaEls89xB2H/V39ioo6zTIDooFdZYWlZekBJOUt7k1q3fpWoTtj+OC/tmUAlMAN0JSImb5WnPxh2BTg==", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-ns-json-schema-2020-12": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.3" + } + }, + "node_modules/@swagger-api/apidom-ns-asyncapi-2": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-asyncapi-2/-/apidom-ns-asyncapi-2-1.0.0-beta.48.tgz", + "integrity": "sha512-FyZ8TrUU+6ZholTHw6VzAQdUErLp/cYmR9TKP/AWc1lN/iSnkWI9hzHIR2vxNPmV0+0ykshtVILW4E1d/MYW6Q==", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-ns-json-schema-draft-7": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.3" + } + }, + "node_modules/@swagger-api/apidom-ns-json-schema-2019-09": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-2019-09/-/apidom-ns-json-schema-2019-09-1.0.0-beta.48.tgz", + "integrity": "sha512-bpNlGwP6ImODwyzQFnV6700KPjWO/+7epVNHrXoHAK1Go55oHsZZ/bfJiRX1F44Vs6VzVvdsGvCX+DH5Ig3fqQ==", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-error": "^1.0.0-beta.48", + "@swagger-api/apidom-ns-json-schema-draft-7": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.4" + } + }, + "node_modules/@swagger-api/apidom-ns-json-schema-2020-12": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-2020-12/-/apidom-ns-json-schema-2020-12-1.0.0-beta.48.tgz", + "integrity": "sha512-YQkXnqHf7TrQZHSbSOudA+7g7ItQi7hW+H3IZK8XDYaOxcqtKhwSvIl2/L8zEUrZ/d3ObAlK/vUs+6WX8u9Ktg==", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-error": "^1.0.0-beta.48", + "@swagger-api/apidom-ns-json-schema-2019-09": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.4" + } + }, + "node_modules/@swagger-api/apidom-ns-json-schema-draft-4": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-4/-/apidom-ns-json-schema-draft-4-1.0.0-beta.48.tgz", + "integrity": "sha512-Kv8wkL1kGrdnmTU0s5EPaoRDMWDualWqCLW7bxvhtczUsmHXQLpvD5pKHsBEF731MkZovtxMWEvBMZZrft1jeg==", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-ast": "^1.0.0-beta.48", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.4" + } + }, + "node_modules/@swagger-api/apidom-ns-json-schema-draft-6": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-6/-/apidom-ns-json-schema-draft-6-1.0.0-beta.48.tgz", + "integrity": "sha512-cUeltQJmAre1P8n3rr6aURKvIy82845ZqD0I0s93qXPXbJ8tI74P2GSFbT/o2K8mOcltDgt/6hfsBbxuPkKN4g==", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-error": "^1.0.0-beta.48", + "@swagger-api/apidom-ns-json-schema-draft-4": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.4" + } + }, + "node_modules/@swagger-api/apidom-ns-json-schema-draft-7": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-7/-/apidom-ns-json-schema-draft-7-1.0.0-beta.48.tgz", + "integrity": "sha512-lJpzZKWurhvI/PlpCIG6lHGnYpx+O2m9HwXFuy0n0bj59hrxbLqQAg94cEc8u0ix8jPWQaBY5Bicm+/9/BsLUw==", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-error": "^1.0.0-beta.48", + "@swagger-api/apidom-ns-json-schema-draft-6": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.4" + } + }, + "node_modules/@swagger-api/apidom-ns-openapi-2": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-2/-/apidom-ns-openapi-2-1.0.0-beta.48.tgz", + "integrity": "sha512-oDtcNpWevXZG3X4GJLhsBIZ7kD/TbIsZ9c/bjbW1DfEhKemkpayO0zDRpNVqEoCmp3BI/6Iye0cjxPQhavd49A==", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-error": "^1.0.0-beta.48", + "@swagger-api/apidom-ns-json-schema-draft-4": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.3" + } + }, + "node_modules/@swagger-api/apidom-ns-openapi-3-0": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-3-0/-/apidom-ns-openapi-3-0-1.0.0-beta.48.tgz", + "integrity": "sha512-1lKdHHmJOBvKGQJMSZU5d5EeYu1JvcXZEZlVtnM2UBig70+RG2oDzZUfkrwgfLp47QbE34fqJdJU7YiwqetGhA==", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-error": "^1.0.0-beta.48", + "@swagger-api/apidom-ns-json-schema-draft-4": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.3" + } + }, + "node_modules/@swagger-api/apidom-ns-openapi-3-1": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-3-1/-/apidom-ns-openapi-3-1-1.0.0-beta.48.tgz", + "integrity": "sha512-d2q3aPK7/s6YfQGwL9M4AB6aC2Z4HjpGLO8nFzrnzdHxJeW0+PVEaZ3+vwCestZvm5KENwXi2E6wtdDk5jBMIw==", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-ast": "^1.0.0-beta.48", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-json-pointer": "^1.0.0-beta.48", + "@swagger-api/apidom-ns-json-schema-2020-12": "^1.0.0-beta.48", + "@swagger-api/apidom-ns-openapi-3-0": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.3" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-api-design-systems-json": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-api-design-systems-json/-/apidom-parser-adapter-api-design-systems-json-1.0.0-beta.48.tgz", + "integrity": "sha512-9PbK8sLotn1VoEnR1CIRGc1ZABNYWCUWvtZ0Q1CKEn+/O6kuJ7t8+sW1gNcULV/t0YPqMfrkAd+YA3+eDki4zw==", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-ns-api-design-systems": "^1.0.0-beta.48", + "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-api-design-systems-yaml": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-api-design-systems-yaml/-/apidom-parser-adapter-api-design-systems-yaml-1.0.0-beta.48.tgz", + "integrity": "sha512-K7b0wLtq3vUovr0TnIlr02TexGB1zj1LQmU5YFNZEntg+H/pA7dWSVISbpfR58NDhNEZ7JETJJwhDqOVQufbEw==", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-ns-api-design-systems": "^1.0.0-beta.48", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-arazzo-json-1": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-arazzo-json-1/-/apidom-parser-adapter-arazzo-json-1-1.0.0-beta.48.tgz", + "integrity": "sha512-ILiUAYb9wOBHpjlxd2LkC/2QJQCPD9Uj0U4TAKnRieR/yAQzbjIKYt1kvWr3A7CIOMlZQE3orE6d9I//J0SENQ==", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-ns-arazzo-1": "^1.0.0-beta.48", + "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-arazzo-yaml-1": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-arazzo-yaml-1/-/apidom-parser-adapter-arazzo-yaml-1-1.0.0-beta.48.tgz", + "integrity": "sha512-KzsskMDhoukAJxC7Q94+4Ad5qmU7JYp/LI1r2VpH/ePzjMfQlMSNi16n4Iob4POBuFTej0CgJjnC1geXhVp6Nw==", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-ns-arazzo-1": "^1.0.0-beta.48", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-asyncapi-json-2": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-asyncapi-json-2/-/apidom-parser-adapter-asyncapi-json-2-1.0.0-beta.48.tgz", + "integrity": "sha512-oIm4Rc+jWZspkS5LMgt4SyX7WzV+UufelzRZ/UAD7daGQmbNA7NnRmR/1Nbm+CUu9bqXTIDW84UP1GqgOfbc4Q==", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-ns-asyncapi-2": "^1.0.0-beta.48", + "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-asyncapi-yaml-2/-/apidom-parser-adapter-asyncapi-yaml-2-1.0.0-beta.48.tgz", + "integrity": "sha512-roErhkfaRjCOGUu+JMbWApP7dDLgW+6lHJy3/iMIxo/jU3fvLyjvJdualzPNOmr4z08ocBwZJErkI3dmUG/Rzg==", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-ns-asyncapi-2": "^1.0.0-beta.48", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-json": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-json/-/apidom-parser-adapter-json-1.0.0-beta.48.tgz", + "integrity": "sha512-9AlHpfNkL0BoKyTF4MrUJuST8JzsJYUjq0LRTmut1xTRM/qzvteXPymUo8vuTpyPqEhMneGTseeu3YAPplSOmA==", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-ast": "^1.0.0-beta.48", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-error": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "tree-sitter": "=0.21.1", + "tree-sitter-json": "=0.24.8", + "web-tree-sitter": "=0.24.5" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-2": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-2/-/apidom-parser-adapter-openapi-json-2-1.0.0-beta.48.tgz", + "integrity": "sha512-N08FmErRUzTNeKscLGs1IO5a3M5JXo4EsyOAZu47xYke7WFihK8QzDK/W4zvWdnm5zAh5NqzmVqB64uzVYjCOw==", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-ns-openapi-2": "^1.0.0-beta.48", + "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-3-0": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-3-0/-/apidom-parser-adapter-openapi-json-3-0-1.0.0-beta.48.tgz", + "integrity": "sha512-0tLMHyuBLYiTkzwCONQmarL1gjGc1LP/V2FnmBUAFl2ShV1ErIlW9OhmM9x0ZlpxUhktOJMlbQsX8FNSOOExBA==", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-ns-openapi-3-0": "^1.0.0-beta.48", + "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-3-1": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-3-1/-/apidom-parser-adapter-openapi-json-3-1-1.0.0-beta.48.tgz", + "integrity": "sha512-DPV7oN8SQKgmj9OIT05vNTDoYlV1sxgvFMFbX+b2ph0i+YU0PToOt/BYMFjmfz/EdqyQpXqEFilhA4zXf0OHfA==", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-ns-openapi-3-1": "^1.0.0-beta.48", + "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-2": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-2/-/apidom-parser-adapter-openapi-yaml-2-1.0.0-beta.48.tgz", + "integrity": "sha512-fKA1y9KgFzUhtbWduJBM8s8hRxBvcAx6iXcr4A2mTqeQcSYFyX9HyBeIdAyFJXGladGW+AZOdg1HatP7IMevDQ==", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-ns-openapi-2": "^1.0.0-beta.48", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-3-0/-/apidom-parser-adapter-openapi-yaml-3-0-1.0.0-beta.48.tgz", + "integrity": "sha512-bT1HB285oXrGY6fqKm5rDY/xTHeGOopv4yTrOYwTVMbGdc7RU8jrJ2eAsRMnv6AgRH9HeOEE5Uf2sCUS8LdYtQ==", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-ns-openapi-3-0": "^1.0.0-beta.48", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-3-1/-/apidom-parser-adapter-openapi-yaml-3-1-1.0.0-beta.48.tgz", + "integrity": "sha512-jAB1OwPRcMR1nwFSy+U+qjCXZArATifnQhEfxsAU0TUtP3um5bHdz1ALwjS1zUBMhySE4MXiPsvI4O4Tl0BC6Q==", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-ns-openapi-3-1": "^1.0.0-beta.48", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-yaml-1-2/-/apidom-parser-adapter-yaml-1-2-1.0.0-beta.48.tgz", + "integrity": "sha512-FXKjiBbSCw3ZgZKjVgo7bOdrSIr8ltJDR4SaEaqzXTFXOhpHln0idJSnfJoMnvHJH0oSieKiHbPs1+OjHXmK5A==", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-ast": "^1.0.0-beta.48", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-error": "^1.0.0-beta.48", + "@tree-sitter-grammars/tree-sitter-yaml": "=0.7.1", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "tree-sitter": "=0.22.4", + "web-tree-sitter": "=0.24.5" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2/node_modules/@tree-sitter-grammars/tree-sitter-yaml": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@tree-sitter-grammars/tree-sitter-yaml/-/tree-sitter-yaml-0.7.1.tgz", + "integrity": "sha512-AynBwkIoQCTgjDR33bDUp9Mqq+YTco0is3n5hRApMqG9of/6A4eQsfC1/uSEeHSUyMQSYawcAWamsexnVpIP4Q==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-addon-api": "^8.3.1", + "node-gyp-build": "^4.8.4" + }, + "peerDependencies": { + "tree-sitter": "^0.22.4" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2/node_modules/tree-sitter": { + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.22.4.tgz", + "integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + } + }, + "node_modules/@swagger-api/apidom-reference": { + "version": "1.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-reference/-/apidom-reference-1.0.0-beta.48.tgz", + "integrity": "sha512-Z11xCSDC9Y6/cOvrKlqLMCZxyj/s2xctE5CCL2N+GHhNdZGSF7YoRRms84X9Rs2qZ8YQCZo5QlV3KwweHnLB2w==", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.48", + "@swagger-api/apidom-error": "^1.0.0-beta.48", + "@types/ramda": "~0.30.0", + "axios": "^1.9.0", + "minimatch": "^7.4.3", + "process": "^0.11.10", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + }, + "optionalDependencies": { + "@swagger-api/apidom-json-pointer": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-ns-arazzo-1": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-ns-asyncapi-2": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-ns-openapi-2": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-ns-openapi-3-0": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-ns-openapi-3-1": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-api-design-systems-json": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-api-design-systems-yaml": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-arazzo-json-1": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-arazzo-yaml-1": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-asyncapi-json-2": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-openapi-json-2": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-openapi-json-3-0": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-openapi-json-3-1": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-openapi-yaml-2": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.40 <1.0.0-rc.0" + } + }, + "node_modules/@swaggerexpert/cookie": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@swaggerexpert/cookie/-/cookie-2.0.2.tgz", + "integrity": "sha512-DPI8YJ0Vznk4CT+ekn3rcFNq1uQwvUHZhH6WvTSPD0YKBIlMS9ur2RYKghXuxxOiqOam/i4lHJH4xTIiTgs3Mg==", + "dependencies": { + "apg-lite": "^1.0.3" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@swaggerexpert/json-pointer": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/@swaggerexpert/json-pointer/-/json-pointer-2.10.2.tgz", + "integrity": "sha512-qMx1nOrzoB+PF+pzb26Q4Tc2sOlrx9Ba2UBNX9hB31Omrq+QoZ2Gly0KLrQWw4Of1AQ4J9lnD+XOdwOdcdXqqw==", + "dependencies": { + "apg-lite": "^1.0.4" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.13.tgz", + "integrity": "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==", + "dev": true, + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.5.1", + "lightningcss": "1.30.1", + "magic-string": "^0.30.18", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.13" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.13.tgz", + "integrity": "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.13", + "@tailwindcss/oxide-darwin-arm64": "4.1.13", + "@tailwindcss/oxide-darwin-x64": "4.1.13", + "@tailwindcss/oxide-freebsd-x64": "4.1.13", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.13", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.13", + "@tailwindcss/oxide-linux-x64-musl": "4.1.13", + "@tailwindcss/oxide-wasm32-wasi": "4.1.13", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.13" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.13.tgz", + "integrity": "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.13.tgz", + "integrity": "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.13.tgz", + "integrity": "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.13.tgz", + "integrity": "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.13.tgz", + "integrity": "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.13.tgz", + "integrity": "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.13.tgz", + "integrity": "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.13.tgz", + "integrity": "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.13.tgz", + "integrity": "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.13.tgz", + "integrity": "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.5", + "@emnapi/runtime": "^1.4.5", + "@emnapi/wasi-threads": "^1.0.4", + "@napi-rs/wasm-runtime": "^0.2.12", + "@tybys/wasm-util": "^0.10.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.13.tgz", + "integrity": "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.13.tgz", + "integrity": "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.13.tgz", + "integrity": "sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.13", + "@tailwindcss/oxide": "4.1.13", + "postcss": "^8.4.41", + "tailwindcss": "4.1.13" + } + }, + "node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/@types/node": { + "version": "20.19.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.13.tgz", + "integrity": "sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==", + "dev": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/ramda": { + "version": "0.30.2", + "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.30.2.tgz", + "integrity": "sha512-PyzHvjCalm2BRYjAU6nIB3TprYwMNOUY/7P/N8bSzp9W/yM2YrtGtAnnVtaCNSeOZ8DzKyFDvaqQs7LnWwwmBA==", + "dependencies": { + "types-ramda": "^0.30.1" + } + }, + "node_modules/@types/react": { + "version": "19.1.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz", + "integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==", + "devOptional": true, + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", + "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", + "dev": true, + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@types/swagger-ui-react": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-react/-/swagger-ui-react-5.18.0.tgz", + "integrity": "sha512-c2M9adVG7t28t1pq19K9Jt20VLQf0P/fwJwnfcmsVVsdkwCWhRmbKDu+tIs0/NGwJ/7GY8lBx+iKZxuDI5gDbw==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "optional": true + }, + "node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==" + }, + "node_modules/apg-lite": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/apg-lite/-/apg-lite-1.0.5.tgz", + "integrity": "sha512-SlI+nLMQDzCZfS39ihzjGp3JNBQfJXyMi6cg9tkLOCPVErgFsUIAEdO9IezR7kbP5Xd0ozcPNQBkf9TO5cHgWw==" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/autolinker": { + "version": "3.16.2", + "resolved": "https://registry.npmjs.org/autolinker/-/autolinker-3.16.2.tgz", + "integrity": "sha512-JiYl7j2Z19F9NdTmirENSUUIIL/9MytEWtmzhfmsKPCp9E+G35Y0UNCMoM9tFigxT59qSc8Ml2dlZXOCVTYwuA==", + "dependencies": { + "tslib": "^2.3.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001741", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", + "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "optional": true + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/core-js-pure": { + "version": "3.45.1", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.45.1.tgz", + "integrity": "sha512-OHnWFKgTUshEU8MK+lOs1H8kC8GkTi9Z1tvNkxrCcw9wl3MJIO7q2ld77wjWn4/xuGrVu2X+nME1iIIPBSdyEQ==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dompurify": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz", + "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/drange": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/drange/-/drange-1.1.1.tgz", + "integrity": "sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==" + }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "engines": { + "node": "*" + } + }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "optional": true + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, + "node_modules/jiti": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "dev": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-file-download": { + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/js-file-download/-/js-file-download-0.4.12.tgz", + "integrity": "sha512-rML+NkoD08p5Dllpjo0ffy4jRHeY6Zsapvr/W86N7E0yuzAO6qa5X9+xog6zQNlH102J7IXljNY2FtS6Lj3ucg==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "dev": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minim": { + "version": "0.23.8", + "resolved": "https://registry.npmjs.org/minim/-/minim-0.23.8.tgz", + "integrity": "sha512-bjdr2xW1dBCMsMGGsUeqM4eFI60m94+szhxWys+B1ztIt6gWSfeGBdSVCIawezeHYLYn0j6zrsXdQS/JllBzww==", + "dependencies": { + "lodash": "^4.15.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neotraverse": { + "version": "0.6.18", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz", + "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/next": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.2.tgz", + "integrity": "sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q==", + "dependencies": { + "@next/env": "15.5.2", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.2", + "@next/swc-darwin-x64": "15.5.2", + "@next/swc-linux-arm64-gnu": "15.5.2", + "@next/swc-linux-arm64-musl": "15.5.2", + "@next/swc-linux-x64-gnu": "15.5.2", + "@next/swc-linux-x64-musl": "15.5.2", + "@next/swc-win32-arm64-msvc": "15.5.2", + "@next/swc-win32-x64-msvc": "15.5.2", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==" + }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "optional": true, + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch-commonjs": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch-commonjs/-/node-fetch-commonjs-3.3.2.tgz", + "integrity": "sha512-VBlAiynj3VMLrotgwOS3OyECFxas5y7ltLcK4t41lMUZeaK15Ym4QRkqN0EQKAFL42q9i21EPKjzLUPfltR72A==", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/openapi-path-templating": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/openapi-path-templating/-/openapi-path-templating-2.2.1.tgz", + "integrity": "sha512-eN14VrDvl/YyGxxrkGOHkVkWEoPyhyeydOUrbvjoz8K5eIGgELASwN1eqFOJ2CTQMGCy2EntOK1KdtJ8ZMekcg==", + "dependencies": { + "apg-lite": "^1.0.4" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/openapi-server-url-templating": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/openapi-server-url-templating/-/openapi-server-url-templating-1.3.0.tgz", + "integrity": "sha512-DPlCms3KKEbjVQb0spV6Awfn6UWNheuG/+folQPzh/wUaKwuqvj8zt5gagD7qoyxtE03cIiKPgLFS3Q8Bz00uQ==", + "dependencies": { + "apg-lite": "^1.0.4" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "node_modules/ramda": { + "version": "0.30.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz", + "integrity": "sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ramda" + } + }, + "node_modules/ramda-adjunct": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-5.1.0.tgz", + "integrity": "sha512-8qCpl2vZBXEJyNbi4zqcgdfHtcdsWjOGbiNSEnEBrM6Y0OKOT8UxJbIVGm1TIcjaSu2MxaWcgtsNlKlCk7o7qg==", + "engines": { + "node": ">=0.10.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ramda-adjunct" + }, + "peerDependencies": { + "ramda": ">= 0.30.0" + } + }, + "node_modules/randexp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.5.3.tgz", + "integrity": "sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==", + "dependencies": { + "drange": "^1.0.2", + "ret": "^0.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.62.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz", + "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-immutable-proptypes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/react-immutable-proptypes/-/react-immutable-proptypes-2.2.0.tgz", + "integrity": "sha512-Vf4gBsePlwdGvSZoLSBfd4HAP93HDauMY4fDjXhreg/vg6F3Fj/MXDNyTbltPC/xZKmZc+cjLu3598DdYK6sgQ==", + "dependencies": { + "invariant": "^2.2.2" + }, + "peerDependencies": { + "immutable": ">=3.6.2" + } + }, + "node_modules/react-immutable-pure-component": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/react-immutable-pure-component/-/react-immutable-pure-component-2.2.2.tgz", + "integrity": "sha512-vkgoMJUDqHZfXXnjVlG3keCxSO/U6WeDQ5/Sl0GK2cH8TOxEzQ5jXqDXHEL/jqk6fsNxV05oH5kD7VNMUE2k+A==", + "peerDependencies": { + "immutable": ">= 2 || >= 4.0.0-rc", + "react": ">= 16.6", + "react-dom": ">= 16.6" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-syntax-highlighter": { + "version": "15.6.6", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz", + "integrity": "sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", + "lowlight": "^1.17.0", + "prismjs": "^1.30.0", + "refractor": "^3.6.0" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + }, + "node_modules/redux-immutable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/redux-immutable/-/redux-immutable-4.0.0.tgz", + "integrity": "sha512-SchSn/DWfGb3oAejd+1hhHx01xUoxY+V7TeK0BKqpkLKiQPVFf7DYzEaKmrEVxsWxielKfSK9/Xq66YyxgR1cg==", + "peerDependencies": { + "immutable": "^3.8.1 || ^4.0.0-rc.1" + } + }, + "node_modules/refractor": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", + "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "dependencies": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.27.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/prismjs": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/remarkable": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/remarkable/-/remarkable-2.0.1.tgz", + "integrity": "sha512-YJyMcOH5lrR+kZdmB0aJJ4+93bEojRZ1HGDn9Eagu6ibg7aVZhc3OWbbShRid+Q5eAfsEqWxpe+g5W5nYNfNiA==", + "dependencies": { + "argparse": "^1.0.10", + "autolinker": "^3.11.0" + }, + "bin": { + "remarkable": "bin/remarkable.js" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/remarkable/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==" + }, + "node_modules/ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-error": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sharp": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" + } + }, + "node_modules/short-unique-id": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/short-unique-id/-/short-unique-id-5.3.2.tgz", + "integrity": "sha512-KRT/hufMSxXKEDSQujfVE0Faa/kZ51ihUcZQAcmP04t00DvPj7Ox5anHke1sJYUtzSuiT/Y5uyzg/W7bBEGhCg==", + "bin": { + "short-unique-id": "bin/short-unique-id", + "suid": "bin/short-unique-id" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/swagger-client": { + "version": "3.35.6", + "resolved": "https://registry.npmjs.org/swagger-client/-/swagger-client-3.35.6.tgz", + "integrity": "sha512-OgwNneIdC45KXwOfwrlkwgWPeAKiV4K75mOnZioTddo1mpp9dTboCDVJas7185Ww1ziBwzShBqXpNGmyha9ZQg==", + "dependencies": { + "@babel/runtime-corejs3": "^7.22.15", + "@scarf/scarf": "=1.4.0", + "@swagger-api/apidom-core": ">=1.0.0-beta.41 <1.0.0-rc.0", + "@swagger-api/apidom-error": ">=1.0.0-beta.41 <1.0.0-rc.0", + "@swagger-api/apidom-json-pointer": ">=1.0.0-beta.41 <1.0.0-rc.0", + "@swagger-api/apidom-ns-openapi-3-1": ">=1.0.0-beta.41 <1.0.0-rc.0", + "@swagger-api/apidom-reference": ">=1.0.0-beta.41 <1.0.0-rc.0", + "@swaggerexpert/cookie": "^2.0.2", + "deepmerge": "~4.3.0", + "fast-json-patch": "^3.0.0-1", + "js-yaml": "^4.1.0", + "neotraverse": "=0.6.18", + "node-abort-controller": "^3.1.1", + "node-fetch-commonjs": "^3.3.2", + "openapi-path-templating": "^2.2.1", + "openapi-server-url-templating": "^1.3.0", + "ramda": "^0.30.1", + "ramda-adjunct": "^5.1.0" + } + }, + "node_modules/swagger-ui-react": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/swagger-ui-react/-/swagger-ui-react-5.29.0.tgz", + "integrity": "sha512-dEDqs3et9cnflzwog0oBMjkrq7Uf6PNPVva4VFB4GICDWMRb6T2C+CzIfqJp9XX9eDvbmiZa/dTcnwvGcDpi5A==", + "dependencies": { + "@babel/runtime-corejs3": "^7.27.1", + "@scarf/scarf": "=1.4.0", + "base64-js": "^1.5.1", + "buffer": "^6.0.3", + "classnames": "^2.5.1", + "css.escape": "1.5.1", + "deep-extend": "0.6.0", + "dompurify": "=3.2.4", + "ieee754": "^1.2.1", + "immutable": "^3.x.x", + "js-file-download": "^0.4.12", + "js-yaml": "=4.1.0", + "lodash": "^4.17.21", + "prop-types": "^15.8.1", + "randexp": "^0.5.3", + "randombytes": "^2.1.0", + "react-copy-to-clipboard": "5.1.0", + "react-debounce-input": "=3.3.0", + "react-immutable-proptypes": "2.2.0", + "react-immutable-pure-component": "^2.2.0", + "react-inspector": "^6.0.1", + "react-redux": "^9.2.0", + "react-syntax-highlighter": "^15.6.1", + "redux": "^5.0.1", + "redux-immutable": "^4.0.0", + "remarkable": "^2.0.1", + "reselect": "^5.1.1", + "serialize-error": "^8.1.0", + "sha.js": "^2.4.12", + "swagger-client": "^3.35.5", + "url-parse": "^1.5.10", + "xml": "=1.0.1", + "xml-but-prettier": "^1.0.1", + "zenscroll": "^4.0.2" + }, + "peerDependencies": { + "react": ">=16.8.0 <20", + "react-dom": ">=16.8.0 <20" + } + }, + "node_modules/swagger-ui-react/node_modules/react-copy-to-clipboard": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz", + "integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==", + "dependencies": { + "copy-to-clipboard": "^3.3.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^15.3.0 || 16 || 17 || 18" + } + }, + "node_modules/swagger-ui-react/node_modules/react-debounce-input": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/react-debounce-input/-/react-debounce-input-3.3.0.tgz", + "integrity": "sha512-VEqkvs8JvY/IIZvh71Z0TC+mdbxERvYF33RcebnodlsUZ8RSgyKe2VWaHXv4+/8aoOgXLxWrdsYs2hDhcwbUgA==", + "dependencies": { + "lodash.debounce": "^4", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^15.3.0 || 16 || 17 || 18" + } + }, + "node_modules/swagger-ui-react/node_modules/react-inspector": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-6.0.2.tgz", + "integrity": "sha512-x+b7LxhmHXjHoU/VrFAzw5iutsILRoYyDq97EDYdFpPLcvqtEzk4ZSZSQjnFPbr5T57tLXnHcqFYoN1pI6u8uQ==", + "peerDependencies": { + "react": "^16.8.4 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", + "integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==", + "dev": true + }, + "node_modules/tapable": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", + "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/to-buffer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", + "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, + "node_modules/tree-sitter": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz", + "integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.8.0" + } + }, + "node_modules/tree-sitter-json": { + "version": "0.24.8", + "resolved": "https://registry.npmjs.org/tree-sitter-json/-/tree-sitter-json-0.24.8.tgz", + "integrity": "sha512-Tc9ZZYwHyWZ3Tt1VEw7Pa2scu1YO7/d2BCBbKTx5hXwig3UfdQjsOPkPyLpDJOn/m1UBEWYAtSdGAwCSyagBqQ==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-addon-api": "^8.2.2", + "node-gyp-build": "^4.8.2" + }, + "peerDependencies": { + "tree-sitter": "^0.21.1" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==" + }, + "node_modules/ts-toolbelt": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz", + "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/types-ramda": { + "version": "0.30.1", + "resolved": "https://registry.npmjs.org/types-ramda/-/types-ramda-0.30.1.tgz", + "integrity": "sha512-1HTsf5/QVRmLzcGfldPFvkVsAdi1db1BBKzi7iW3KBUlOICg/nKnFS+jGqDJS3YD8VsWbAh7JiHeBvbsw8RPxA==", + "dependencies": { + "ts-toolbelt": "^9.6.0" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, + "node_modules/unraw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unraw/-/unraw-3.0.0.tgz", + "integrity": "sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg==" + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/web-tree-sitter": { + "version": "0.24.5", + "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.24.5.tgz", + "integrity": "sha512-+J/2VSHN8J47gQUAvF8KDadrfz6uFYVjxoxbKWDoXVsH2u7yLdarCnIURnrMA6uSRkgX3SdmqM5BOoQjPdSh5w==", + "optional": true + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==" + }, + "node_modules/xml-but-prettier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml-but-prettier/-/xml-but-prettier-1.0.1.tgz", + "integrity": "sha512-C2CJaadHrZTqESlH03WOyw0oZTtoy2uEg6dSDF6YRg+9GnYNub53RRemLpnvtbHDFelxMx4LajiFsYeR6XJHgQ==", + "dependencies": { + "repeat-string": "^1.5.2" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/zenscroll": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zenscroll/-/zenscroll-4.0.2.tgz", + "integrity": "sha512-jEA1znR7b4C/NnaycInCU6h/d15ZzCd1jmsruqOKnZP6WXQSMH3W2GL+OXbkruslU4h+Tzuos0HdswzRUk/Vgg==" + }, + "node_modules/zod": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.5.tgz", + "integrity": "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..c2823bc --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,30 @@ +{ + "name": "candivista-frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "@hookform/resolvers": "^5.2.1", + "axios": "^1.11.0", + "next": "15.5.2", + "next-themes": "^0.4.6", + "react": "19.1.0", + "react-dom": "19.1.0", + "react-hook-form": "^7.62.0", + "swagger-ui-react": "^5.29.0", + "zod": "^4.1.5" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "@types/swagger-ui-react": "^5.18.0", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs new file mode 100644 index 0000000..c7bcb4b --- /dev/null +++ b/frontend/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/frontend/public/file.svg b/frontend/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/frontend/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/globe.svg b/frontend/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/frontend/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/next.svg b/frontend/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/frontend/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/vercel.svg b/frontend/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/frontend/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/window.svg b/frontend/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/frontend/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/app/admin/page.tsx b/frontend/src/app/admin/page.tsx new file mode 100644 index 0000000..3d026ab --- /dev/null +++ b/frontend/src/app/admin/page.tsx @@ -0,0 +1,145 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import axios from "axios"; +import AdminLayout from "../../components/AdminLayout"; +import AdminDashboard from "../../components/AdminDashboard"; +import UserManagement from "../../components/UserManagement"; +import JobManagement from "../../components/JobManagement"; +import TokenManagement from "../../components/TokenManagement"; +import SystemStats from "../../components/SystemStats"; +import DeveloperTools from "../../components/DeveloperTools"; + +interface User { + id: string; + email: string; + first_name: string; + last_name: string; + role: string; + company_name?: string; + avatar_url?: string; + is_active: boolean; + last_login_at?: string; + email_verified_at?: string; + created_at: string; + updated_at: string; +} + +interface SystemStatistics { + total_users: number; + active_users: number; + total_jobs: number; + total_interviews: number; + total_tokens_purchased: number; + total_tokens_used: number; + total_revenue: number; + generated_at: string; +} + +export default function AdminPage() { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [activeTab, setActiveTab] = useState("dashboard"); + const [systemStats, setSystemStats] = useState(null); + const router = useRouter(); + + useEffect(() => { + const token = localStorage.getItem("token"); + if (!token) { + router.push("/login"); + return; + } + + // Verify token and check if user is admin + axios.get(`${process.env.NEXT_PUBLIC_API_URL}/rest/auth/me`, { + headers: { + Authorization: `Bearer ${token}` + } + }) + .then(response => { + const userData = response.data; + if (userData.role !== 'admin') { + router.push("/dashboard"); + return; + } + setUser(userData); + + // Fetch system statistics + fetchSystemStats(); + }) + .catch(() => { + localStorage.removeItem("token"); + localStorage.removeItem("user"); + router.push("/login"); + }) + .finally(() => { + setLoading(false); + }); + }, [router]); + + const fetchSystemStats = async () => { + try { + const token = localStorage.getItem("token"); + const response = await axios.get(`${process.env.NEXT_PUBLIC_API_URL}/rest/admin/statistics`, { + headers: { + Authorization: `Bearer ${token}` + } + }); + setSystemStats(response.data); + } catch (error) { + console.error("Failed to fetch system statistics:", error); + } + }; + + const handleLogout = () => { + localStorage.removeItem("token"); + localStorage.removeItem("user"); + router.push("/login"); + }; + + const handleTabChange = (tab: string) => { + setActiveTab(tab); + }; + + if (loading) { + return ( +
+
+
+

Loading admin dashboard...

+
+
+ ); + } + + const renderContent = () => { + switch (activeTab) { + case "dashboard": + return ; + case "users": + return ; + case "jobs": + return ; + case "tokens": + return ; + case "stats": + return ; + case "devtools": + return ; + default: + return ; + } + }; + + return ( + + {renderContent()} + + ); +} diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx new file mode 100644 index 0000000..7a57e93 --- /dev/null +++ b/frontend/src/app/dashboard/page.tsx @@ -0,0 +1,204 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import axios from "axios"; +import Layout from "../../components/Layout"; +import JobsList from "../../components/JobsList"; + +interface User { + id: string; + email: string; + first_name: string; + last_name: string; + role: string; + company_name?: string; + avatar_url?: string; + is_active: boolean; + last_login_at?: string; + email_verified_at?: string; + created_at: string; + updated_at: string; +} + +interface Job { + id: string; + title: string; + description: string; + requirements: string; + skills_required?: string[]; + location?: string; + employment_type: string; + experience_level: string; + salary_min?: number; + salary_max?: number; + currency: string; + status: string; + icon?: string; + created_at: string; + updated_at: string; + // Metrics + total_interviews?: number; + interviews_completed?: number; + available_interviews?: number; + running_days?: number; + applications?: number; +} + +export default function DashboardPage() { + const [user, setUser] = useState(null); + const [jobs, setJobs] = useState([]); + const [loading, setLoading] = useState(true); + const [activeSidebarItem, setActiveSidebarItem] = useState("jobs"); + const router = useRouter(); + + useEffect(() => { + console.log("Dashboard useEffect triggered"); + const token = localStorage.getItem("token"); + console.log("Token found:", !!token); + + if (!token) { + console.log("No token, redirecting to login"); + router.push("/login"); + return; + } + + // Verify token with backend + console.log("Verifying token with backend..."); + axios.get(`${process.env.NEXT_PUBLIC_API_URL}/rest/auth/me`, { + headers: { + Authorization: `Bearer ${token}` + } + }) + .then(async response => { + console.log("Auth response received:", response.data); + const userData = response.data; + setUser(userData); + + // Redirect admins to admin panel + if (userData.role === 'admin') { + console.log("Admin user, redirecting to admin panel"); + router.push("/admin"); + return; + } + + // Fetch jobs from backend + try { + console.log("Fetching jobs from backend..."); + const jobsResponse = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/rest/jobs`, { + headers: { + Authorization: `Bearer ${token}` + } + }); + + console.log("Jobs response status:", jobsResponse.status); + + if (jobsResponse.ok) { + const jobsData = await jobsResponse.json(); + console.log("Jobs data received:", jobsData); + console.log("Jobs array:", jobsData.jobs); + console.log("Jobs count:", jobsData.jobs?.length || 0); + setJobs(jobsData.jobs || []); + } else { + // Silencing console usage to satisfy linter in Server Components + await jobsResponse.text().catch(() => undefined); + setJobs([]); + } + } catch (error) { + console.error("Error fetching jobs:", error); + setJobs([]); + } + }) + .catch((error) => { + console.error("Auth error:", error); + localStorage.removeItem("token"); + localStorage.removeItem("user"); + router.push("/login"); + }) + .finally(() => { + console.log("Setting loading to false"); + setLoading(false); + }); + }, [router]); + + const handleLogout = () => { + localStorage.removeItem("token"); + localStorage.removeItem("user"); + router.push("/login"); + }; + + const handleSidebarItemClick = (item: string) => { + setActiveSidebarItem(item); + // TODO: Handle navigation to different pages + console.log("Navigate to:", item); + }; + + + const handleEditJob = (job: Job) => { + // TODO: Navigate to edit job page or open modal + console.log("Edit job:", job.id); + }; + + const handleDeleteJob = (job: Job) => { + // TODO: Show confirmation dialog and delete job + console.log("Delete job:", job.id); + }; + + const handleViewJob = (job: Job) => { + // This will be handled by the JobsList component now + console.log("View job:", job.id); + }; + + const refreshJobs = async () => { + try { + const token = localStorage.getItem("token"); + if (!token) return; + + console.log("Refreshing jobs from backend..."); + const jobsResponse = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/rest/jobs`, { + headers: { + Authorization: `Bearer ${token}` + } + }); + + if (jobsResponse.ok) { + const jobsData = await jobsResponse.json(); + console.log("Jobs refreshed:", jobsData); + setJobs(jobsData.jobs || []); + } else { + console.error("Failed to refresh jobs:", jobsResponse.statusText); + } + } catch (error) { + console.error("Error refreshing jobs:", error); + } + }; + + if (loading) { + return ( +
+
+
+

Loading...

+
+
+ ); + } + + return ( + + + + ); +} \ No newline at end of file diff --git a/frontend/src/app/docs/page.tsx b/frontend/src/app/docs/page.tsx new file mode 100644 index 0000000..0e1d37e --- /dev/null +++ b/frontend/src/app/docs/page.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { useEffect, useState } from "react"; +import dynamic from "next/dynamic"; + +// Dynamically import SwaggerUI to avoid SSR issues +const SwaggerUI = dynamic(() => import("swagger-ui-react"), { ssr: false }); + +export default function DocsPage() { + const [swaggerSpec, setSwaggerSpec] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchSwaggerSpec = async () => { + try { + const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8083"; + const response = await fetch(`${apiUrl}/doc/swagger.json`); + + if (!response.ok) { + throw new Error(`Failed to fetch API spec: ${response.status}`); + } + + const spec = await response.json(); + setSwaggerSpec(spec); + } catch (err) { + console.error("Error fetching Swagger spec:", err); + setError(err instanceof Error ? err.message : "Failed to load API documentation"); + } finally { + setLoading(false); + } + }; + + fetchSwaggerSpec(); + }, []); + + if (loading) { + return ( +
+
+
+

Loading API documentation...

+
+
+ ); + } + + if (error) { + return ( +
+
+
⚠️
+

API Documentation Unavailable

+

{error}

+
+

Make sure the backend is running on:

+ + {process.env.NEXT_PUBLIC_API_URL || "http://localhost:8083"} + +

And Swagger is available at:

+ + /doc and /doc/swagger.json + +
+
+
+ ); + } + + return ( +
+
+
+

API Documentation

+

+ Interactive API documentation for Candivista backend services +

+
+
+ +
+ {swaggerSpec && ( + { + const token = localStorage.getItem("token"); + if (token) { + request.headers.Authorization = `Bearer ${token}`; + } + return request; + }} + /> + )} +
+
+ ); +} diff --git a/frontend/src/app/favicon.ico b/frontend/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 zcmeHv30#a{`}aL_*G&7qml|y<+KVaDM2m#dVr!KsA!#An?kSQM(q<_dDNCpjEux83 zLb9Z^XxbDl(w>%i@8hT6>)&Gu{h#Oeyszu?xtw#Zb1mO{pgX9699l+Qppw7jXaYf~-84xW z)w4x8?=youko|}Vr~(D$UXIbiXABHh`p1?nn8Po~fxRJv}|0e(BPs|G`(TT%kKVJAdg5*Z|x0leQq0 zkdUBvb#>9F()jo|T~kx@OM8$9wzs~t2l;K=woNssA3l6|sx2r3+kdfVW@e^8e*E}v zA1y5{bRi+3Z`uD3{F7LgFJDdvm;nJilkzDku>BwXH(8ItVCXk*-lSJnR?-2UN%hJ){&rlvg`CDTj z)Bzo!3v7Ou#83zEDEFcKt(f1E0~=rqeEbTnMvWR#{+9pg%7G8y>u1OVRUSoox-ovF z2Ydma(;=YuBY(eI|04{hXzZD6_f(v~H;C~y5=DhAC{MMS>2fm~1H_t2$56pc$NH8( z5bH|<)71dV-_oCHIrzrT`2s-5w_+2CM0$95I6X8p^r!gHp+j_gd;9O<1~CEQQGS8) zS9Qh3#p&JM-G8rHekNmKVewU;pJRcTAog68KYo^dRo}(M>36U4Us zfgYWSiHZL3;lpWT=zNAW>Dh#mB!_@Lg%$ms8N-;aPqMn+C2HqZgz&9~Eu z4|Kp<`$q)Uw1R?y(~S>ePdonHxpV1#eSP1B;Ogo+-Pk}6#0GsZZ5!||ev2MGdh}_m z{DeR7?0-1^zVs&`AV6Vt;r3`I`OI_wgs*w=eO%_#7Kepl{B@xiyCANc(l zzIyd4y|c6PXWq9-|KM8(zIk8LPk(>a)zyFWjhT!$HJ$qX1vo@d25W<fvZQ2zUz5WRc(UnFMKHwe1| zWmlB1qdbiA(C0jmnV<}GfbKtmcu^2*P^O?MBLZKt|As~ge8&AAO~2K@zbXelK|4T<{|y4`raF{=72kC2Kn(L4YyenWgrPiv z@^mr$t{#X5VuIMeL!7Ab6_kG$&#&5p*Z{+?5U|TZ`B!7llpVmp@skYz&n^8QfPJzL z0G6K_OJM9x+Wu2gfN45phANGt{7=C>i34CV{Xqlx(fWpeAoj^N0Biu`w+MVcCUyU* zDZuzO0>4Z6fbu^T_arWW5n!E45vX8N=bxTVeFoep_G#VmNlQzAI_KTIc{6>c+04vr zx@W}zE5JNSU>!THJ{J=cqjz+4{L4A{Ob9$ZJ*S1?Ggg3klFp!+Y1@K+pK1DqI|_gq z5ZDXVpge8-cs!o|;K73#YXZ3AShj50wBvuq3NTOZ`M&qtjj#GOFfgExjg8Gn8>Vq5 z`85n+9|!iLCZF5$HJ$Iu($dm?8~-ofu}tEc+-pyke=3!im#6pk_Wo8IA|fJwD&~~F zc16osQ)EBo58U7XDuMexaPRjU@h8tXe%S{fA0NH3vGJFhuyyO!Uyl2^&EOpX{9As0 zWj+P>{@}jxH)8|r;2HdupP!vie{sJ28b&bo!8`D^x}TE$%zXNb^X1p@0PJ86`dZyj z%ce7*{^oo+6%&~I!8hQy-vQ7E)0t0ybH4l%KltWOo~8cO`T=157JqL(oq_rC%ea&4 z2NcTJe-HgFjNg-gZ$6!Y`SMHrlj}Etf7?r!zQTPPSv}{so2e>Fjs1{gzk~LGeesX%r(Lh6rbhSo_n)@@G-FTQy93;l#E)hgP@d_SGvyCp0~o(Y;Ee8{ zdVUDbHm5`2taPUOY^MAGOw*>=s7=Gst=D+p+2yON!0%Hk` zz5mAhyT4lS*T3LS^WSxUy86q&GnoHxzQ6vm8)VS}_zuqG?+3td68_x;etQAdu@sc6 zQJ&5|4(I?~3d-QOAODHpZ=hlSg(lBZ!JZWCtHHSj`0Wh93-Uk)_S%zsJ~aD>{`A0~ z9{AG(e|q3g5B%wYKRxiL2Y$8(4w6bzchKuloQW#e&S3n+P- z8!ds-%f;TJ1>)v)##>gd{PdS2Oc3VaR`fr=`O8QIO(6(N!A?pr5C#6fc~Ge@N%Vvu zaoAX2&(a6eWy_q&UwOhU)|P3J0Qc%OdhzW=F4D|pt0E4osw;%<%Dn58hAWD^XnZD= z>9~H(3bmLtxpF?a7su6J7M*x1By7YSUbxGi)Ot0P77`}P3{)&5Un{KD?`-e?r21!4vTTnN(4Y6Lin?UkSM z`MXCTC1@4A4~mvz%Rh2&EwY))LeoT=*`tMoqcEXI>TZU9WTP#l?uFv+@Dn~b(>xh2 z;>B?;Tz2SR&KVb>vGiBSB`@U7VIWFSo=LDSb9F{GF^DbmWAfpms8Sx9OX4CnBJca3 zlj9(x!dIjN?OG1X4l*imJNvRCk}F%!?SOfiOq5y^mZW)jFL@a|r-@d#f7 z2gmU8L3IZq0ynIws=}~m^#@&C%J6QFo~Mo4V`>v7MI-_!EBMMtb%_M&kvAaN)@ZVw z+`toz&WG#HkWDjnZE!6nk{e-oFdL^$YnbOCN}JC&{$#$O27@|Tn-skXr)2ml2~O!5 zX+gYoxhoc7qoU?C^3~&!U?kRFtnSEecWuH0B0OvLodgUAi}8p1 zrO6RSXHH}DMc$&|?D004DiOVMHV8kXCP@7NKB zgaZq^^O<7PoKEp72kby@W0Z!Y*Ay{&vfg#C&gG@YVR9g?FEocMUi1gSN$+V+ayF45{a zuDZDTN}mS|;BO%gEf}pjBfN2-gIrU#G5~cucA;dokXW89%>AyXJJI z9X4UlIWA|ZYHgbI z5?oFk@A=Ik7lrEQPDH!H+b`7_Y~aDb_qa=B2^Y&Ow41cU=4WDd40dp5(QS-WMN-=Y z9g;6_-JdNU;|6cPwf$ak*aJIcwL@1n$#l~zi{c{EW?T;DaW*E8DYq?Umtz{nJ&w-M zEMyTDrC&9K$d|kZe2#ws6)L=7K+{ zQw{XnV6UC$6-rW0emqm8wJoeZK)wJIcV?dST}Z;G0Arq{dVDu0&4kd%N!3F1*;*pW zR&qUiFzK=@44#QGw7k1`3t_d8&*kBV->O##t|tonFc2YWrL7_eqg+=+k;!F-`^b8> z#KWCE8%u4k@EprxqiV$VmmtiWxDLgnGu$Vs<8rppV5EajBXL4nyyZM$SWVm!wnCj-B!Wjqj5-5dNXukI2$$|Bu3Lrw}z65Lc=1G z^-#WuQOj$hwNGG?*CM_TO8Bg-1+qc>J7k5c51U8g?ZU5n?HYor;~JIjoWH-G>AoUP ztrWWLbRNqIjW#RT*WqZgPJXU7C)VaW5}MiijYbABmzoru6EmQ*N8cVK7a3|aOB#O& zBl8JY2WKfmj;h#Q!pN%9o@VNLv{OUL?rixHwOZuvX7{IJ{(EdPpuVFoQqIOa7giLVkBOKL@^smUA!tZ1CKRK}#SSM)iQHk)*R~?M!qkCruaS!#oIL1c z?J;U~&FfH#*98^G?i}pA{ z9Jg36t4=%6mhY(quYq*vSxptes9qy|7xSlH?G=S@>u>Ebe;|LVhs~@+06N<4CViBk zUiY$thvX;>Tby6z9Y1edAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91P8|av8hQoCmQXkd?7wIJwb z_^v8bbg`SAn{I*4bH$u(RZ6*xUhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq36!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=pC^S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk($?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvhCL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397*_cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111aH}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*IcmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU&68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-=A=yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v#ix45EVrcEhr>!NMhprl$InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~&^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7<4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C zI@}scZlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css new file mode 100644 index 0000000..fa25d96 --- /dev/null +++ b/frontend/src/app/globals.css @@ -0,0 +1,236 @@ +@import "tailwindcss"; + +/* Custom animations */ +@keyframes gradient-x { + 0%, 100% { + background-size: 200% 200%; + background-position: left center; + } + 50% { + background-size: 200% 200%; + background-position: right center; + } +} + +@keyframes float { + 0%, 100% { + transform: translateY(0px); + } + 50% { + transform: translateY(-20px); + } +} + +@keyframes pulse-glow { + 0%, 100% { + box-shadow: 0 0 20px rgba(59, 130, 246, 0.3); + } + 50% { + box-shadow: 0 0 40px rgba(59, 130, 246, 0.6); + } +} + +@keyframes slide-up { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes scale-in { + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } +} + +/* Utility classes */ +.animate-gradient-x { + animation: gradient-x 3s ease infinite; +} + +.animate-float { + animation: float 6s ease-in-out infinite; +} + +.animate-pulse-glow { + animation: pulse-glow 2s ease-in-out infinite; +} + +.animate-slide-up { + animation: slide-up 0.6s ease-out; +} + +.animate-fade-in { + animation: fade-in 0.8s ease-out; +} + +.animate-scale-in { + animation: scale-in 0.5s ease-out; +} + +/* Custom scrollbar */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f5f9; +} + +::-webkit-scrollbar-thumb { + background: linear-gradient(to bottom, #3b82f6, #8b5cf6); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: linear-gradient(to bottom, #2563eb, #7c3aed); +} + +/* Glass morphism effect */ +.glass { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +/* Gradient text */ +.gradient-text { + background: linear-gradient(135deg, #3b82f6, #8b5cf6, #ec4899); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Hover effects */ +.hover-lift { + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.hover-lift:hover { + transform: translateY(-5px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); +} + +/* Custom button styles */ +.btn-primary { + background: linear-gradient(135deg, #3b82f6, #8b5cf6); + transition: all 0.3s ease; +} + +.btn-primary:hover { + background: linear-gradient(135deg, #2563eb, #7c3aed); + transform: translateY(-2px); + box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3); +} + +/* Card hover effects */ +.card-hover { + transition: all 0.3s ease; +} + +.card-hover:hover { + transform: translateY(-8px); + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15); +} + +/* Loading animation */ +.loading-dots { + display: inline-block; +} + +.loading-dots::after { + content: ''; + animation: loading-dots 1.5s infinite; +} + +@keyframes loading-dots { + 0%, 20% { + content: ''; + } + 40% { + content: '.'; + } + 60% { + content: '..'; + } + 80%, 100% { + content: '...'; + } +} + +/* Responsive text */ +@media (max-width: 640px) { + .hero-title { + font-size: 3rem; + line-height: 1.1; + } +} + +@media (min-width: 641px) { + .hero-title { + font-size: 4rem; + line-height: 1.1; + } +} + +@media (min-width: 1024px) { + .hero-title { + font-size: 5rem; + line-height: 1.1; + } +} + +/* Smooth scrolling */ +html { + scroll-behavior: smooth; +} + +/* Focus styles */ +.focus-ring:focus { + outline: none; + ring: 2px; + ring-color: #3b82f6; + ring-offset: 2px; +} + +/* Custom selection */ +::selection { + background: rgba(59, 130, 246, 0.2); + color: #1e40af; +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .dark-mode-text { + color: #f8fafc; + } + + .dark-mode-bg { + background: #0f172a; + } +} + +/* Print styles */ +@media print { + .no-print { + display: none !important; + } +} \ No newline at end of file diff --git a/frontend/src/app/interview/page.tsx b/frontend/src/app/interview/page.tsx new file mode 100644 index 0000000..ac955ae --- /dev/null +++ b/frontend/src/app/interview/page.tsx @@ -0,0 +1,275 @@ +"use client"; + +import { useEffect, useState, Suspense } from "react"; +import { useSearchParams } from "next/navigation"; +import ConsentScreen from "../../components/ConsentScreen"; +import NameInputScreen from "../../components/NameInputScreen"; +import MandatoryQuestionsScreen from "../../components/MandatoryQuestionsScreen"; +import ChatScreen from "../../components/ChatScreen"; + +interface Job { + id: string; + title: string; + description: string; + requirements: string; + skills_required?: string[]; + location?: string; + employment_type: string; + experience_level: string; + salary_min?: number; + salary_max?: number; + currency: string; + status: string; + icon?: string; + created_at: string; + updated_at: string; +} + +interface InterviewState { + step: 'loading' | 'consent' | 'name_input' | 'mandatory_questions' | 'chat' | 'completed' | 'error'; + job: Job | null; + candidateName: string; + error: string | null; + consentGiven: boolean; + mandatoryAnswers: string[]; +} + +function InterviewPageInner() { + const searchParams = useSearchParams(); + const linkId = searchParams.get('id'); + const isTestMode = searchParams.get('test') === 'true'; + + const [state, setState] = useState({ + step: 'loading', + job: null, + candidateName: '', + error: null, + consentGiven: false, + mandatoryAnswers: [] + }); + + useEffect(() => { + if (linkId) { + fetchJobByLink(); + } else { + setState(prev => ({ + ...prev, + step: 'error', + error: "Invalid interview link" + })); + } + }, [linkId]); + + const fetchJobByLink = async () => { + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/rest/jobs/interview/${linkId}`); + if (response.ok) { + const data = await response.json(); + setState(prev => ({ + ...prev, + step: 'consent', + job: data.job + })); + } else { + setState(prev => ({ + ...prev, + step: 'error', + error: "Interview link not found or expired" + })); + } + } catch (err) { + setState(prev => ({ + ...prev, + step: 'error', + error: "Failed to load interview" + })); + } + }; + + const handleConsent = (consent: boolean) => { + if (consent) { + setState(prev => ({ + ...prev, + step: 'name_input', + consentGiven: true + })); + } else { + // Log failed attempt and show sad smiley + logFailedAttempt(); + setState(prev => ({ + ...prev, + step: 'completed' + })); + } + }; + + const handleNameSubmit = (name: string) => { + setState(prev => ({ + ...prev, + step: 'mandatory_questions', + candidateName: name + })); + }; + + const handleMandatoryQuestionsComplete = (answers: string[]) => { + setState(prev => ({ + ...prev, + step: 'chat', + mandatoryAnswers: answers + })); + }; + + const handleInterviewComplete = () => { + setState(prev => ({ + ...prev, + step: 'completed' + })); + }; + + const logFailedAttempt = async () => { + try { + await fetch(`${process.env.NEXT_PUBLIC_API_URL}/rest/jobs/interview/${linkId}/failed`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }); + } catch (error) { + console.error('Failed to log failed attempt:', error); + } + }; + + if (state.step === 'loading') { + return ( +
+
+
+

Loading interview...

+
+
+ ); + } + + if (state.step === 'error' || !state.job) { + return ( +
+
+
+ + + +
+

Interview Not Available

+

{state.error}

+
+
+ ); + } + + if (state.step === 'consent') { + return ( + + ); + } + + if (state.step === 'name_input') { + return ( + + ); + } + + if (state.step === 'mandatory_questions') { + return ( + + ); + } + + if (state.step === 'chat') { + return ( + + ); + } + + if (state.step === 'completed') { + return ( +
+
+ {state.consentGiven ? ( + <> +
+ + + +
+

Interview Completed

+

+ Thank you for completing the interview. We'll review your responses and get back to you soon. +

+
+

+ Position: {state.job.title} +

+

+ Company: {state.job.location || "Remote"} +

+
+ + ) : ( + <> +
+ 😢 +
+

Interview Declined

+

+ We understand you've chosen not to proceed with the interview. Thank you for your time. +

+
+

+ Position: {state.job.title} +

+

+ Company: {state.job.location || "Remote"} +

+
+ + )} +
+
+ ); + } + + return null; +} + +export default function InterviewPage() { + return ( + +
+
+

Loading interview...

+
+ + }> + +
+ ); +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx new file mode 100644 index 0000000..a4cbe5a --- /dev/null +++ b/frontend/src/app/layout.tsx @@ -0,0 +1,37 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; +import { ThemeProvider } from "next-themes"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Candivista App", + description: "A modern authentication system with Next.js and TypeScript", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + {children} + + + + ); +} diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx new file mode 100644 index 0000000..1624398 --- /dev/null +++ b/frontend/src/app/login/page.tsx @@ -0,0 +1,262 @@ +"use client"; + +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import axios from "axios"; +import { useRouter } from "next/navigation"; + +const loginSchema = z.object({ + email: z.string().email("Please enter a valid email address"), + password: z.string().min(1, "Password is required"), +}); + +const registerSchema = z.object({ + first_name: z.string().min(2, "First name must be at least 2 characters"), + last_name: z.string().min(2, "Last name must be at least 2 characters"), + email: z.string().email("Please enter a valid email address"), + password: z.string().min(8, "Password must be at least 8 characters"), + confirmPassword: z.string(), + company_name: z.string().optional(), +}).refine((data) => data.password === data.confirmPassword, { + message: "Passwords don't match", + path: ["confirmPassword"], +}); + +type LoginForm = z.infer; +type RegisterForm = z.infer; + +export default function LoginPage() { + const [isLogin, setIsLogin] = useState(true); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(""); + const router = useRouter(); + + const loginForm = useForm({ + resolver: zodResolver(loginSchema), + }); + + const registerForm = useForm({ + resolver: zodResolver(registerSchema), + }); + + const onLogin = async (data: LoginForm) => { + setIsLoading(true); + setError(""); + + try { + const response = await axios.post(`${process.env.NEXT_PUBLIC_API_URL}/rest/auth/login`, data); + const { token, user } = response.data; + + localStorage.setItem("token", token); + localStorage.setItem("user", JSON.stringify(user)); + + router.push("/dashboard"); + } catch (err: any) { + setError(err.response?.data?.message || "Login failed"); + } finally { + setIsLoading(false); + } + }; + + const onRegister = async (data: RegisterForm) => { + setIsLoading(true); + setError(""); + + try { + const { confirmPassword, ...registerData } = data; + const response = await axios.post(`${process.env.NEXT_PUBLIC_API_URL}/rest/auth/register`, registerData); + const { token, user } = response.data; + + localStorage.setItem("token", token); + localStorage.setItem("user", JSON.stringify(user)); + + router.push("/dashboard"); + } catch (err: any) { + setError(err.response?.data?.message || "Registration failed"); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+
+
+

+ {isLogin ? "Welcome back" : "Create account"} +

+

+ {isLogin ? "Sign in to your account" : "Sign up for a new account"} +

+
+ + {error && ( +
+ {error} +
+ )} + + {isLogin ? ( +
+
+ + + {loginForm.formState.errors.email && ( +

{loginForm.formState.errors.email.message}

+ )} +
+ +
+ + + {loginForm.formState.errors.password && ( +

{loginForm.formState.errors.password.message}

+ )} +
+ + +
+ ) : ( +
+
+
+ + + {registerForm.formState.errors.first_name && ( +

{registerForm.formState.errors.first_name.message}

+ )} +
+
+ + + {registerForm.formState.errors.last_name && ( +

{registerForm.formState.errors.last_name.message}

+ )} +
+
+ +
+ + + {registerForm.formState.errors.company_name && ( +

{registerForm.formState.errors.company_name.message}

+ )} +
+ +
+ + + {registerForm.formState.errors.email && ( +

{registerForm.formState.errors.email.message}

+ )} +
+ +
+ + + {registerForm.formState.errors.password && ( +

{registerForm.formState.errors.password.message}

+ )} +
+ +
+ + + {registerForm.formState.errors.confirmPassword && ( +

{registerForm.formState.errors.confirmPassword.message}

+ )} +
+ + +
+ )} + +
+ +
+
+
+
+ ); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx new file mode 100644 index 0000000..7b46634 --- /dev/null +++ b/frontend/src/app/page.tsx @@ -0,0 +1,491 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import Image from "next/image"; +import AnimatedCounter from "@/components/AnimatedCounter"; +import FeatureCard from "@/components/FeatureCard"; +import PricingCard from "@/components/PricingCard"; +import TechStackCard from "@/components/TechStackCard"; + +export default function Home() { + const router = useRouter(); + const [isLoading, setIsLoading] = useState(true); + const [activeFeature, setActiveFeature] = useState(0); + + useEffect(() => { + // Check if user is already logged in + const token = localStorage.getItem("token"); + if (token) { + router.push("/dashboard"); + } else { + setIsLoading(false); + } + }, [router]); + + useEffect(() => { + const interval = setInterval(() => { + setActiveFeature((prev) => (prev + 1) % 4); + }, 3000); + return () => clearInterval(interval); + }, []); + + if (isLoading) { + return ( +
+
+
+

Loading Candivista...

+
+
+ ); + } + + return ( +
+ {/* Navigation */} + + + {/* Hero Section */} +
+
+
+ 🚀 AI-Powered Interview Platform +
+ +

+ The Future of{" "} + + AI Recruitment + +

+ +

+ Transform your hiring process with our comprehensive AI-powered multi-tenant interview platform. + Create job listings, conduct intelligent interviews, and manage candidates with unprecedented flexibility. +

+ +
+ + +
+ + {/* Hero Illustration */} +
+
+
+
+
+ + + +
+

Create Jobs

+

Design compelling job postings with AI assistance

+
+ +
+
+ + + +
+

AI Interviews

+

Conduct intelligent interviews with automated scoring

+
+ +
+
+ + + +
+

Analytics

+

Get insights and track candidate performance

+
+
+
+
+
+ + {/* Features Section */} +
+
+

+ Powerful Features for Modern Recruitment +

+

+ Everything you need to streamline your hiring process and find the best talent +

+
+ +
+
+ + + + } + title="Multi-Tenant Architecture" + description="Complete data isolation between companies with enterprise-grade security. Scale to thousands of tenants with confidence." + gradient="bg-gradient-to-r from-blue-500 to-blue-600" + delay={0} + /> + + + + + } + title="Flexible Link System" + description="Revolutionary token-based interview distribution. Create custom links with flexible application limits for maximum control." + gradient="bg-gradient-to-r from-purple-500 to-purple-600" + delay={200} + /> + + + + + } + title="AI-Powered Intelligence" + description="Local Ollama integration with gpt-oss:20b model for privacy-focused AI interviews. Automated question generation and real-time scoring." + gradient="bg-gradient-to-r from-green-500 to-green-600" + delay={400} + /> +
+ +
+
+
+
+
+
+
+
+
+

Interview Dashboard

+
+
+
+
+
+
+
+
+
+
+
+
+ +
+

AI Analysis

+
+
+ Technical Skills + 85% +
+
+ Communication + 92% +
+
+ Problem Solving + 78% +
+
+
+
+
+
+
+
+ + {/* Pricing Section */} +
+
+

+ Simple, Transparent Pricing +

+

+ Pay only for what you use with our flexible token-based system +

+
+ +
+ + + + + + + +
+
+ + {/* Stats Section */} +
+
+

+ Trusted by Companies Worldwide +

+

+ Join thousands of companies already using Candivista to streamline their recruitment process +

+
+ +
+
+
+ +
+

Interviews Conducted

+
+
+
+ +
+

Companies

+
+
+
+ +
+

Countries

+
+
+
+ +
+

Satisfaction Rate

+
+
+
+ + {/* Technology Stack */} +
+
+

+ Built with Modern Technology +

+

+ Leveraging the latest technologies for optimal performance and developer experience +

+
+ +
+ + + + + + + + +
+
+ + {/* CTA Section */} +
+
+
+

+ Ready to Transform Your Hiring? +

+

+ Join thousands of companies already using Candivista to streamline their recruitment process + and find the best talent with AI-powered interviews. +

+
+ + +
+
+
+
+ + {/* Footer */} + +
+ ); +} diff --git a/frontend/src/components/AdminDashboard.tsx b/frontend/src/components/AdminDashboard.tsx new file mode 100644 index 0000000..ab3ada2 --- /dev/null +++ b/frontend/src/components/AdminDashboard.tsx @@ -0,0 +1,240 @@ +"use client"; + +import { useState } from "react"; + +interface SystemStatistics { + total_users: number; + active_users: number; + total_jobs: number; + total_interviews: number; + total_tokens_purchased: number; + total_tokens_used: number; + total_revenue: number; + generated_at: string; +} + +interface AdminDashboardProps { + stats: SystemStatistics | null; + onRefresh: () => void; +} + +export default function AdminDashboard({ stats, onRefresh }: AdminDashboardProps) { + const [isRefreshing, setIsRefreshing] = useState(false); + + const handleRefresh = async () => { + setIsRefreshing(true); + await onRefresh(); + setIsRefreshing(false); + }; + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD' + }).format(amount); + }; + + const formatNumber = (num: number) => { + return new Intl.NumberFormat('en-US').format(num); + }; + + const getTokenUtilization = () => { + if (!stats || stats.total_tokens_purchased === 0) return 0; + return Math.round((stats.total_tokens_used / stats.total_tokens_purchased) * 100); + }; + + const getActiveUserPercentage = () => { + if (!stats || stats.total_users === 0) return 0; + return Math.round((stats.active_users / stats.total_users) * 100); + }; + + const recentActivities = [ + { + id: 1, + type: "user_registration", + message: "New user registered: john.doe@company.com", + timestamp: "2 minutes ago", + icon: "👤" + }, + { + id: 2, + type: "job_created", + message: "New job posted: Senior Frontend Developer", + timestamp: "15 minutes ago", + icon: "📢" + }, + { + id: 3, + type: "token_purchase", + message: "Token package purchased: Professional Pack (20 tokens)", + timestamp: "1 hour ago", + icon: "🪙" + }, + { + id: 4, + type: "interview_completed", + message: "Interview completed for Software Engineer position", + timestamp: "2 hours ago", + icon: "✅" + } + ]; + + return ( +
+ {/* Header */} +
+
+

System Overview

+

+ Monitor system performance and user activity +

+
+ +
+ + {/* Stats Grid */} +
+ {/* Total Users */} +
+
+
+

Total Users

+

+ {stats ? formatNumber(stats.total_users) : '--'} +

+
+
+ 👥 +
+
+
+ + {stats ? getActiveUserPercentage() : 0}% active + + + ({stats ? formatNumber(stats.active_users) : 0} active) + +
+
+ + {/* Total Jobs */} +
+
+
+

Total Jobs

+

+ {stats ? formatNumber(stats.total_jobs) : '--'} +

+
+
+ 📢 +
+
+
+ Job postings created +
+
+ + {/* Token Utilization */} +
+
+
+

Token Utilization

+

+ {stats ? getTokenUtilization() : 0}% +

+
+
+ 🪙 +
+
+
+ {stats ? `${formatNumber(stats.total_tokens_used)} / ${formatNumber(stats.total_tokens_purchased)} used` : 'No data'} +
+
+ + {/* Total Revenue */} +
+
+
+

Total Revenue

+

+ {stats ? formatCurrency(stats.total_revenue) : '$0'} +

+
+
+ 💰 +
+
+
+ From token sales +
+
+
+ + {/* Charts and Recent Activity */} +
+ {/* Recent Activity */} +
+

Recent Activity

+
+ {recentActivities.map((activity) => ( +
+
+ {activity.icon} +
+
+

{activity.message}

+

{activity.timestamp}

+
+
+ ))} +
+
+ + {/* Quick Actions */} +
+

Quick Actions

+
+ + + +
+
+
+
+ ); +} diff --git a/frontend/src/components/AdminHeader.tsx b/frontend/src/components/AdminHeader.tsx new file mode 100644 index 0000000..8d535de --- /dev/null +++ b/frontend/src/components/AdminHeader.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { useState } from "react"; + +interface User { + first_name: string; + last_name: string; + email: string; + role: string; +} + +interface AdminHeaderProps { + user?: User; + onLogout?: () => void; +} + +export default function AdminHeader({ user, onLogout }: AdminHeaderProps) { + const [isProfileOpen, setIsProfileOpen] = useState(false); + + return ( +
+
+
+ {/* Page Title */} +
+

+ Admin Dashboard +

+

+ Manage users, jobs, and system settings +

+
+ + {/* User Menu */} +
+ {/* Notifications */} + + + {/* Profile Dropdown */} +
+ + + {/* Dropdown Menu */} + {isProfileOpen && ( +
+
+
+
+ {user?.first_name} {user?.last_name} +
+
+ {user?.email} +
+
+ + +
+ +
+
+ )} +
+
+
+
+
+ ); +} diff --git a/frontend/src/components/AdminLayout.tsx b/frontend/src/components/AdminLayout.tsx new file mode 100644 index 0000000..12f03f0 --- /dev/null +++ b/frontend/src/components/AdminLayout.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { ReactNode } from "react"; +import AdminSidebar from "./AdminSidebar"; +import AdminHeader from "./AdminHeader"; + +interface User { + first_name: string; + last_name: string; + email: string; + role: string; +} + +interface AdminLayoutProps { + children: ReactNode; + user?: User; + activeTab?: string; + onTabChange?: (tab: string) => void; + onLogout?: () => void; +} + +export default function AdminLayout({ + children, + user, + activeTab = "dashboard", + onTabChange, + onLogout +}: AdminLayoutProps) { + return ( +
+ +
+ +
+ {children} +
+
+
+ ); +} diff --git a/frontend/src/components/AdminSidebar.tsx b/frontend/src/components/AdminSidebar.tsx new file mode 100644 index 0000000..52b6355 --- /dev/null +++ b/frontend/src/components/AdminSidebar.tsx @@ -0,0 +1,101 @@ +"use client"; + +interface AdminSidebarProps { + activeTab?: string; + onTabChange?: (tab: string) => void; +} + +export default function AdminSidebar({ activeTab = "dashboard", onTabChange }: AdminSidebarProps) { + const menuItems = [ + { + id: "dashboard", + label: "Dashboard", + icon: "📊", + description: "Overview and analytics" + }, + { + id: "users", + label: "User Management", + icon: "👥", + description: "Manage users and permissions" + }, + { + id: "jobs", + label: "Job Management", + icon: "📢", + description: "View and manage all jobs" + }, + { + id: "tokens", + label: "Token Management", + icon: "🪙", + description: "Manage interview tokens" + }, + { + id: "stats", + label: "System Statistics", + icon: "📈", + description: "Detailed system metrics" + }, + { + id: "devtools", + label: "Developer Tools", + icon: "🛠️", + description: "Swagger, Portainer, Docs" + } + ]; + + return ( +
+ {/* Logo */} +
+
+
+ A +
+
+ Admin Panel +

Candivista

+
+
+
+ + {/* Navigation */} + + + {/* Admin Badge */} +
+
+
+ + Admin Access + +
+
+
+ ); +} diff --git a/frontend/src/components/AnimatedCounter.tsx b/frontend/src/components/AnimatedCounter.tsx new file mode 100644 index 0000000..bdb6ccb --- /dev/null +++ b/frontend/src/components/AnimatedCounter.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { useEffect, useState } from "react"; + +interface AnimatedCounterProps { + end: number; + duration?: number; + prefix?: string; + suffix?: string; + className?: string; +} + +export default function AnimatedCounter({ + end, + duration = 2000, + prefix = "", + suffix = "", + className = "" +}: AnimatedCounterProps) { + const [count, setCount] = useState(0); + + useEffect(() => { + let startTime: number; + let animationFrame: number; + + const animate = (currentTime: number) => { + if (!startTime) startTime = currentTime; + const progress = Math.min((currentTime - startTime) / duration, 1); + + // Easing function for smooth animation + const easeOutCubic = 1 - Math.pow(1 - progress, 3); + const currentCount = Math.floor(easeOutCubic * end); + + setCount(currentCount); + + if (progress < 1) { + animationFrame = requestAnimationFrame(animate); + } + }; + + animationFrame = requestAnimationFrame(animate); + + return () => { + if (animationFrame) { + cancelAnimationFrame(animationFrame); + } + }; + }, [end, duration]); + + return ( + + {prefix}{count.toLocaleString()}{suffix} + + ); +} diff --git a/frontend/src/components/ChatScreen.tsx b/frontend/src/components/ChatScreen.tsx new file mode 100644 index 0000000..571c504 --- /dev/null +++ b/frontend/src/components/ChatScreen.tsx @@ -0,0 +1,403 @@ +"use client"; + +import { useState, useEffect, useRef } from 'react'; +import { Job, Message } from '../types'; + +interface ChatScreenProps { + job: Job; + candidateName: string; + linkId: string; + isTestMode?: boolean; + mandatoryAnswers?: string[]; + onComplete: () => void; +} + +export default function ChatScreen({ job, candidateName, linkId, isTestMode = false, mandatoryAnswers = [], onComplete }: ChatScreenProps) { + const [messages, setMessages] = useState([]); + const [inputMessage, setInputMessage] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [isInitializing, setIsInitializing] = useState(true); + const [error, setError] = useState(''); + const [isTyping, setIsTyping] = useState(false); + const [typingMessage, setTypingMessage] = useState(''); + const messagesEndRef = useRef(null); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + + // Build conversation history from mandatory answers for test mode + const buildConversationHistory = () => { + if (!isTestMode || !mandatoryAnswers.length) return []; + + const history: Message[] = []; + const mandatoryQuestions = job.interview_questions || []; + + for (let i = 0; i < mandatoryQuestions.length; i++) { + if (mandatoryAnswers[i]) { + history.push({ + id: `q-${i + 1}`, + sender: 'ai', + content: `Question ${i + 1}: ${mandatoryQuestions[i]}`, + timestamp: new Date() + }); + history.push({ + id: `a-${i + 1}`, + sender: 'user', + content: mandatoryAnswers[i], + timestamp: new Date() + }); + } + } + + return history; + }; + + useEffect(() => { + scrollToBottom(); + }, [messages, typingMessage]); + + // Typing effect function + const simulateTyping = (text: string, onComplete: (finalText: string) => void) => { + setIsTyping(true); + setTypingMessage(''); + + let currentIndex = 0; + const typingSpeed = 20 + Math.random() * 30; // Random speed between 20-50ms per character + + const typeNextCharacter = () => { + if (currentIndex < text.length) { + setTypingMessage(text.slice(0, currentIndex + 1)); + currentIndex++; + setTimeout(typeNextCharacter, typingSpeed); + } else { + setIsTyping(false); + onComplete(text); + } + }; + + // Small delay before starting to type + setTimeout(typeNextCharacter, 500); + }; + + useEffect(() => { + initializeChat(); + }, []); + + const initializeChat = async () => { + try { + setIsInitializing(true); + + // Send initial data to AI endpoint + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/rest/ai/start-interview${isTestMode ? '?test=true' : ''}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + candidateName, + job: job, + linkId, + test: isTestMode + }) + }); + + if (response.ok) { + const data = await response.json(); + + // Use typing effect for AI's initial message + const aiMessage = data.message || "Hello! I'm your evaluation agent. Let's begin the interview for the " + job.title + " position. Please tell me about yourself and your interest in this role."; + + simulateTyping(aiMessage, (finalText) => { + setMessages([{ + id: '1', + content: finalText, + sender: 'ai', + timestamp: new Date() + }]); + }); + } else { + throw new Error('Failed to initialize chat'); + } + } catch (error) { + console.error('Error initializing chat:', error); + setError('Failed to start the interview. Please try again.'); + + // Add fallback message with typing effect + const fallbackMessage = "Hello! I'm your evaluation agent. Let's begin the interview for the " + job.title + " position. Please tell me about yourself and your interest in this role."; + + simulateTyping(fallbackMessage, (finalText) => { + setMessages([{ + id: '1', + content: finalText, + sender: 'ai', + timestamp: new Date() + }]); + }); + } finally { + setIsInitializing(false); + } + }; + + const sendMessage = async () => { + if (!inputMessage.trim() || isLoading) return; + + const userMessage: Message = { + id: Date.now().toString(), + content: inputMessage.trim(), + sender: 'user', + timestamp: new Date() + }; + + setMessages(prev => [...prev, userMessage]); + setInputMessage(''); + setIsLoading(true); + + try { + // Build conversation history for test mode + const conversationHistory = isTestMode ? + [...buildConversationHistory(), ...messages.filter(msg => msg.content && msg.content !== 'undefined')] : + messages; + + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/rest/ai/chat${isTestMode ? '?test=true' : ''}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message: userMessage.content, + candidateName, + job: job, + linkId, + conversationHistory: conversationHistory.map(msg => ({ + sender: msg.sender === 'user' ? 'candidate' : msg.sender, + message: msg.content, + timestamp: msg.timestamp + })), + test: isTestMode + }) + }); + + if (response.ok) { + const data = await response.json(); + + // Use typing effect for AI response + const aiResponseText = data.message || "Thank you for your response. Let me ask you another question..."; + + simulateTyping(aiResponseText, (finalText) => { + const aiMessage: Message = { + id: (Date.now() + 1).toString(), + content: finalText, + sender: 'ai', + timestamp: new Date() + }; + + setMessages(prev => [...prev, aiMessage]); + + // Check if interview is complete + if (data.isComplete) { + setTimeout(() => { + onComplete(); + }, 2000); + } + }); + } else { + throw new Error('Failed to send message'); + } + } catch (error) { + console.error('Error sending message:', error); + setError('Failed to send message. Please try again.'); + } finally { + setIsLoading(false); + } + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + sendMessage(); + } + }; + + const formatTime = (date: Date) => { + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + }; + + if (isInitializing) { + return ( +
+
+
+

Initializing interview...

+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+
+

+ Interview: {job.title} +

+

+ Candidate: {candidateName} • {job.location || "Remote"} +

+
+
+
+ AI Agent Online +
+
+
+
+ + {/* Error Banner */} + {error && ( +
+
+
+ + + + {error} + +
+
+
+ )} + + {/* Messages */} +
+
+ {messages.map((message) => ( +
+
+
+ {message.sender === 'ai' && ( +
+ + + +
+ )} +
+

{message.content}

+

+ {formatTime(message.timestamp)} +

+
+ {message.sender === 'user' && ( +
+ + + +
+ )} +
+
+
+ ))} + + {isLoading && !isTyping && ( +
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+ )} + + {isTyping && ( +
+
+
+
+ + + +
+
+

{typingMessage}

+
+
+
+
+
+
+
+ )} + +
+
+
+ + {/* Input */} +
+
+
+
+