commit 146ef598473e8aaeb7a653f65fca68b88fee56da Author: hardik Date: Fri Jul 24 14:47:09 2026 +0530 first commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..fbe0e55 --- /dev/null +++ b/.env.example @@ -0,0 +1,61 @@ +# Luxe Platform Master Environment Variables + +# Node & General App Config +NODE_ENV=development +PORT=3000 +HOST=0.0.0.0 +GATEWAY_URL=http://localhost:3000 +FRONTEND_URL=http://localhost:5173 + +# Database (PostgreSQL) +DATABASE_URL=postgresql://luxe_admin:luxe_secure_password_2026@localhost:5432/luxe_db?schema=public&connection_limit=20&pool_timeout=10 + +# Caching & Queue (Redis) +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= + +# Security & Secrets +JWT_SECRET=super_secret_luxe_jwt_signing_key_2026_production_grade +JWT_ACCESS_EXPIRATION=15m +JWT_REFRESH_EXPIRATION=7d +ARGON_MEMORY_COST=65536 +ARGON_TIME_COST=3 + +# Object Storage (MinIO / AWS S3 / Cloudflare R2) +S3_ENDPOINT=http://localhost:9000 +S3_REGION=us-east-1 +S3_ACCESS_KEY=minioadmin +S3_SECRET_KEY=minioadmin +S3_BUCKET=luxe-media +S3_USE_SSL=false + +# Search Service (Meilisearch / OpenSearch) +MEILI_HOST=http://localhost:7700 +MEILI_MASTER_KEY=luxe_meili_master_key_2026 + +# Email / Notifications (SMTP / Nodemailer) +SMTP_HOST=localhost +SMTP_PORT=1025 +SMTP_USER= +SMTP_PASS= +EMAIL_FROM="Luxe Marketplace " + +# SMS & WhatsApp (Twilio) +TWILIO_ACCOUNT_SID=AC_dummy_account_sid +TWILIO_AUTH_TOKEN=dummy_auth_token +TWILIO_PHONE_NUMBER=+15005550006 + +# Telegram Bot Integration +TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyZ + +# Payment Gateways +STRIPE_SECRET_KEY=sk_test_51LuxeMarketplaceDummyStripeKey +STRIPE_WEBHOOK_SECRET=whsec_dummy_webhook_secret +RAZORPAY_KEY_ID=rzp_test_luxe_dummy_key +RAZORPAY_KEY_SECRET=dummy_razorpay_secret + +# Observability & Metrics +OTEL_SERVICE_NAME=luxe-gateway +OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 +PROMETHEUS_METRICS_ENABLED=true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b76111 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Node dependencies +node_modules/ +.pnpm-store/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build outputs +dist/ +build/ +*.tsbuildinfo + +# Environment files +.env +.env.local +.env.production +.env.staging + +# Prisma +/packages/database/src/generated/ + +# Logs & Coverage +logs/ +*.log +coverage/ +.nyc_output/ + +# IDE & System files +.vscode/ +.idea/ +.DS_Store +Thumbs.db diff --git a/backend_api_specification(luxe) b/backend_api_specification(luxe) new file mode 100644 index 0000000..ee87df5 --- /dev/null +++ b/backend_api_specification(luxe) @@ -0,0 +1,310 @@ +# 📖 Luxe Platform - Backend API Specification & Technical Documentation + +This document provides a comprehensive technical specification for building the backend RESTful API services and database architecture for **Luxe**. + +--- + +## 1. Executive Summary & System Architecture + +### 1.1 Overview +Luxe is a multi-tier platform connecting **Users** (members/visitors) with **Clients** (service providers/listed profiles) and managed by **Admins**. + +### 1.2 User Roles & Access Rights Matrix +| Role | Code Identifier | Permissions & Capabilities | +| :--- | :--- | :--- | +| **User** | `user` | Browse profiles, search with filters, save favorite listings, write reviews, interact with blog/forum, send messages. | +| **Client** | `advertiser` / `client` | All `user` capabilities + create and manage listed profile(s), upload gallery media, set availability & pricing, request verification, view analytics. | +| **Admin** | `admin` / `super_admin` | System-wide management, verify client profiles, moderate user content, full CRUD for Blogs & Forum, manage Cities & Categories, view dashboard analytics & audit logs. | + +--- + +## 2. API Design Conventions & Protocols + +* **Base URL**: `https://api.luxe.com/api/v1` +* **Transport**: HTTPS with TLS 1.3 +* **Authentication**: JSON Web Tokens (JWT) + * Header: `Authorization: Bearer ` + * Access Token expiry: 15–60 minutes + * Refresh Token expiry: 7–30 days (stored in HttpOnly, Secure Cookie) + +### Standard Response Schemas + +#### Success (`200 OK`, `201 Created`) +```json +{ + "success": true, + "message": "Operation executed successfully", + "data": { ... } +} +``` + +#### Paginated List Response +```json +{ + "success": true, + "data": [ ... ], + "pagination": { + "total": 245, + "page": 1, + "pageSize": 20, + "hasMore": true + } +} +``` + +#### Error Response (`400 Bad Request`, `401 Unauthorized`, `403 Forbidden`, `404 Not Found`, `422 Unprocessable Entity`, `500 Internal Error`) +```json +{ + "success": false, + "message": "Validation Error", + "errors": [ + { + "field": "email", + "message": "Email address is already in use" + } + ] +} +``` + +--- + +## 3. Complete API Endpoint Catalog + +--- + +### 🔑 Module 1: Authentication & Identity (`/auth`) + +| Method | Endpoint | Access Level | Description | +| :--- | :--- | :--- | :--- | +| `POST` | `/auth/register` | Public | Register new user as `USER` or `CLIENT`. Returns access token & user object. | +| `POST` | `/auth/login` | Public | Authenticate user credentials. Sets HttpOnly refresh token cookie. | +| `POST` | `/auth/logout` | Authenticated | Revoke refresh token & invalidate current session. | +| `POST` | `/auth/refresh-token` | Public | Exchange refresh token cookie for a new access token. | +| `POST` | `/auth/forgot-password` | Public | Send password reset link or OTP to user email. | +| `/auth/reset-password` | `POST` | Public | Reset password using reset token/OTP. | +| `/auth/verify-otp` | `POST` | Authenticated | Verify email or phone number OTP code. | +| `/auth/me` | `GET` | Authenticated | Get current authenticated user profile & permissions. | + +--- + +### 👤 Module 2: User Account & Favorites (`/users`) + +| Method | Endpoint | Access Level | Description | +| :--- | :--- | :--- | :--- | +| `GET` | `/users/profile` | Authenticated | Fetch logged-in user profile metadata. | +| `PUT` | `/users/profile` | Authenticated | Update user display name, avatar, bio, and preferences. | +| `GET` | `/users/favorites` | User | Get paginated list of user's saved/bookmarked client profiles. | +| `POST` | `/users/favorites/:profileId` | User | Add client profile to user favorites. | +| `DELETE` | `/users/favorites/:profileId` | User | Remove client profile from user favorites. | +| `PUT` | `/users/change-password` | Authenticated | Update user password (requires old password). | +| `GET` | `/users/notifications` | Authenticated | Fetch notifications for current user. | + +--- + +### 💋 Module 3: Client & Listed Profile Management (`/clients`) +*Dedicated to `CLIENT` accounts to manage their public listing.* + +| Method | Endpoint | Access Level | Description | +| :--- | :--- | :--- | :--- | +| `GET` | `/clients/my-profile` | Client | Get client's listing details, completion status, and verification state. | +| `POST` | `/clients/my-profile` | Client | Create listed profile draft. | +| `PUT` | `/clients/my-profile` | Client | Full update of listed profile (name, age, city, area, priceFrom, tagline, categories, languages, height). | +| `PATCH` | `/clients/my-profile/status` | Client | Toggle online availability (`isOnline: true/false`). | +| `POST` | `/clients/my-profile/gallery` | Client | Upload & append image/video to profile gallery. | +| `DELETE` | `/clients/my-profile/gallery/:mediaId` | Client | Delete image/video from gallery. | +| `POST` | `/clients/my-profile/verification` | Client | Submit verification photos/documents for Admin approval. | +| `GET` | `/clients/analytics` | Client | Fetch client analytics (profile views, unique visitors, phone clicks, favorite counts). | + +--- + +### 🔍 Module 4: Public Directory & Search (`/profiles`, `/search`, `/cities`, `/categories`) + +| Method | Endpoint | Access Level | Description | +| :--- | :--- | :--- | :--- | +| `GET` | `/search/profiles` | Public | Multi-filter profile search (Query params: `query`, `city`, `area`, `categories[]`, `ageMin`, `ageMax`, `priceMin`, `priceMax`, `verified`, `online`, `premium`, `rating`, `sortBy`, `page`, `pageSize`). | +| `GET` | `/profiles/featured` | Public | Fetch featured & high-ranking client profiles. | +| `/profiles/trending` | `GET` | Public | Fetch trending/popular client profiles. | +| `GET` | `/profiles/:slug` | Public | Get full public profile information by unique slug. | +| `GET` | `/profiles/:slug/reviews` | Public | Get reviews and aggregate ratings for a client profile. | +| `POST` | `/profiles/:slug/reviews` | User | Submit a review & rating (1 to 5 stars) for a client profile. | +| `GET` | `/cities` | Public | Fetch active cities with listing counts and cover images. | +| `GET` | `/categories` | Public | Fetch all available profile service categories and icons. | + +--- + +### 📰 Module 5: Blog & CMS (`/blogs`) + +| Method | Endpoint | Access Level | Description | +| :--- | :--- | :--- | :--- | +| `GET` | `/blogs` | Public | Fetch paginated published blog posts (filters: `category`, `tag`, `search`). | +| `GET` | `/blogs/:slug` | Public | Get single blog post by slug with author info and reading time. | +| `POST` | `/blogs/:id/like` | Authenticated | Toggle like on a blog post. | +| `GET` | `/blogs/:id/comments` | Public | Fetch comments on a blog post. | +| `POST` | `/blogs/:id/comments` | Authenticated | Post a comment on a blog post. | +| `GET` | `/blogs/categories` | Public | Fetch all blog categories. | + +--- + +### 💬 Module 6: Forum & Community (`/forum`) + +| Method | Endpoint | Access Level | Description | +| :--- | :--- | :--- | :--- | +| `GET` | `/forum/topics` | Public | Get forum discussion topics (supports sorting by `newest`, `popular`, `pinned`). | +| `GET` | `/forum/topics/:slug` | Public | View forum topic detail with replies thread. | +| `POST` | `/forum/topics` | Authenticated | Create a new discussion thread. | +| `POST` | `/forum/topics/:id/reply` | Authenticated | Add a reply to a discussion thread. | +| `POST` | `/forum/topics/:id/like` | Authenticated | Upvote/like a topic or reply. | + +--- + +### 💬 Module 7: Messaging & Notifications (`/messages`, `/notifications`) + +| Method | Endpoint | Access Level | Description | +| :--- | :--- | :--- | :--- | +| `GET` | `/messages/conversations` | Authenticated | Get list of direct message conversations. | +| `GET` | `/messages/conversations/:id` | Authenticated | Get message history with another user/client. | +| `POST` | `/messages/send` | Authenticated | Send a direct message. (Emits WebSockets event `new_message`). | +| `GET` | `/notifications` | Authenticated | Fetch notifications (unread & read). | +| `PATCH` | `/notifications/:id/read` | Authenticated | Mark notification as read. | +| `PATCH` | `/notifications/read-all` | Authenticated | Mark all notifications as read. | + +--- + +### 📁 Module 8: Media & Cloud File Upload (`/media`) + +| Method | Endpoint | Access Level | Description | +| :--- | :--- | :--- | :--- | +| `POST` | `/media/presigned-url` | Authenticated | Generate AWS S3 / Cloudinary presigned upload URL for client-side upload. | +| `POST` | `/media/upload` | Authenticated | Direct multipart form file upload endpoint (fallback). | + +--- + +### 🛡️ Module 9: Admin Panel API Suite (`/admin`) + +#### 9.1 Dashboard & Analytics +| Method | Endpoint | Access Level | Description | +| :--- | :--- | :--- | :--- | +| `GET` | `/admin/stats/overview` | Admin | Overall system counters (Total Users, Total Clients, Active Listings, Verification Queue, Revenue). | +| `GET` | `/admin/stats/growth` | Admin | Time-series data for registrations, pageviews, active subscriptions. | + +#### 9.2 User & Client Moderation +| Method | Endpoint | Access Level | Description | +| :--- | :--- | :--- | :--- | +| `GET` | `/admin/users` | Admin | Get paginated list of all users with search, role filter (`USER`, `CLIENT`, `ADMIN`), status filter. | +| `GET` | `/admin/users/:id` | Admin | Get detailed user record, IPs, login logs, and associated profiles. | +| `PATCH` | `/admin/users/:id/status` | Admin | Update user status (`active`, `suspended`, `banned`). | +| `PATCH` | `/admin/users/:id/role` | Admin | Modify user role (e.g. promote to `ADMIN` or `CLIENT`). | + +#### 9.3 Client Verification Queue & Profile Controls +| Method | Endpoint | Access Level | Description | +| :--- | :--- | :--- | :--- | +| `GET` | `/admin/verifications` | Admin | Get pending client verification requests with uploaded ID proofs. | +| `POST` | `/admin/verifications/:id/approve` | Admin | Approve verification (`isVerified = true`), notify client. | +| `POST` | `/admin/verifications/:id/reject` | Admin | Reject verification with reason note. | +| `PATCH` | `/admin/profiles/:id/premium` | Admin | Toggle client profile badge (`isPremium = true/false`). | +| `DELETE` | `/admin/profiles/:id` | Admin | Moderation action: Soft-delete or remove violating listing. | + +#### 9.4 Blog CMS Management +| Method | Endpoint | Access Level | Description | +| :--- | :--- | :--- | :--- | +| `GET` | `/admin/blogs` | Admin | List all blog posts including drafts and scheduled posts. | +| `POST` | `/admin/blogs` | Admin | Create a new blog post (`title`, `slug`, `excerpt`, `content`, `coverImage`, `category`, `tags`). | +| `PUT` | `/admin/blogs/:id` | Admin | Edit existing blog post. | +| `DELETE` | `/admin/blogs/:id` | Admin | Delete blog post. | +| `POST` | `/admin/blogs/categories` | Admin | Create blog category. | + +#### 9.5 Platform Meta Data Management (Cities & Categories) +| Method | Endpoint | Access Level | Description | +| :--- | :--- | :--- | :--- | +| `POST` | `/admin/cities` | Admin | Add new city to directory. | +| `PUT` | `/admin/cities/:id` | Admin | Edit city details, image, or slug. | +| `DELETE` | `/admin/cities/:id` | Admin | Delete city. | +| `POST` | `/admin/categories` | Admin | Add new profile category/service. | +| `PUT` | `/admin/categories/:id` | Admin | Edit category details. | +| `DELETE` | `/admin/categories/:id` | Admin | Delete category. | + +#### 9.6 Moderation, Reports & Audit Logs +| Method | Endpoint | Access Level | Description | +| :--- | :--- | :--- | :--- | +| `GET` | `/admin/reviews` | Admin | Moderate user-submitted profile reviews. | +| `POST` | `/admin/reviews/:id/approve` | Admin | Approve published review. | +| `DELETE` | `/admin/reviews/:id` | Admin | Delete fake/inappropriate review. | +| `GET` | `/admin/audit-logs` | Admin | View system audit log of admin actions. | + +--- + +## 4. Database Entity Schema Specification (PostgreSQL / Prisma / MongoDB) + +### 4.1 `User` +* `id`: String (UUID / Primary Key) +* `email`: String (Unique, Indexed) +* `username`: String (Unique) +* `passwordHash`: String +* `role`: Enum (`user`, `client`, `admin`, `super_admin`) +* `isVerified`: Boolean (default: false) +* `isPremium`: Boolean (default: false) +* `createdAt`: Timestamp +* `updatedAt`: Timestamp + +### 4.2 `Profile` (Client Listings) +* `id`: String (UUID) +* `userId`: String (Foreign Key -> User.id) +* `slug`: String (Unique, Indexed) +* `name`: String +* `age`: Integer +* `cityId`: String (Foreign Key -> City.id) +* `area`: String +* `avatar`: String (URL) +* `coverImage`: String (URL) +* `rating`: Float (default: 0.0) +* `reviewCount`: Integer (default: 0) +* `isVerified`: Boolean +* `isPremium`: Boolean +* `isOnline`: Boolean +* `priceFrom`: Float +* `currency`: String (default: 'USD') +* `height`: String +* `tagline`: String +* `categories`: String[] (Array of Category slugs) +* `languages`: String[] +* `createdAt`: Timestamp + +### 4.3 `BlogPost` +* `id`: String (UUID) +* `slug`: String (Unique) +* `title`: String +* `excerpt`: Text +* `content`: Text +* `coverImage`: String (URL) +* `authorName`: String +* `authorAvatar`: String +* `category`: String +* `tags`: String[] +* `readingTime`: Integer (minutes) +* `likes`: Integer (default: 0) +* `publishedAt`: Timestamp + +### 4.4 `City` +* `id`: String (UUID) +* `slug`: String (Unique) +* `name`: String +* `country`: String +* `profileCount`: Integer +* `image`: String (URL) + +### 4.5 `Category` +* `id`: String (UUID) +* `slug`: String (Unique) +* `name`: String +* `icon`: String +* `profileCount`: Integer + +--- + +## 5. Security & Implementation Best Practices + +1. **Password Hashing**: Use **Argon2id** or **Bcrypt** with salt rounds >= 12. +2. **CORS Configuration**: Restrict origins to trusted domains (`https://luxe.com`). +3. **Rate Limiting**: Enforce 100 requests per 15-minute window for standard endpoints; 5 attempts per 15 minutes for `/auth/login`. +4. **Input Sanitization**: Validate all requests using `Zod` or `Joi` schemas to prevent SQL Injection and XSS. +5. **Real-time Engine**: Use **Socket.io** or WebSockets for messaging and online status events. diff --git a/docker/Dockerfile.gateway b/docker/Dockerfile.gateway new file mode 100644 index 0000000..6ca8849 --- /dev/null +++ b/docker/Dockerfile.gateway @@ -0,0 +1,36 @@ +# Multi-stage production Dockerfile for Luxe API Gateway +FROM node:22-alpine AS builder + +WORKDIR /app + +# Copy monorepo configuration & package files +COPY package.json tsconfig.json pnpm-workspace.yaml ./ +COPY packages/database/package.json ./packages/database/ +COPY packages/shared/package.json ./packages/shared/ +COPY services/gateway/package.json ./services/gateway/ + +# Install dependencies +RUN npm install --ignore-scripts + +# Copy source code +COPY packages/database ./packages/database +COPY packages/shared ./packages/shared +COPY services/gateway ./services/gateway + +# Build Prisma & TypeScript packages +RUN npm run db:generate +RUN npm run build + +# Production Runner stage +FROM node:22-alpine AS runner + +WORKDIR /app + +ENV NODE_ENV=production +ENV PORT=3000 + +COPY --from=builder /app ./ + +EXPOSE 3000 + +CMD ["npm", "start", "--workspace=@luxe/gateway"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..ddc4730 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,79 @@ +version: '3.8' + +services: + postgres: + image: postgres:16-alpine + container_name: luxe_postgres + restart: always + environment: + POSTGRES_USER: luxe_admin + POSTGRES_PASSWORD: luxe_secure_password_2026 + POSTGRES_DB: luxe_db + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U luxe_admin -d luxe_db"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + container_name: luxe_redis + restart: always + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 5 + + minio: + image: minio/minio:RELEASE.2024-01-18T22-51-28Z + container_name: luxe_minio + restart: always + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + command: server /data --console-address ":9001" + ports: + - "9000:9000" + - "9001:9001" + volumes: + - minio_data:/data + + mailpit: + image: axllent/mailpit:v1.15 + container_name: luxe_mailpit + restart: always + ports: + - "1025:1025" # SMTP port + - "8025:8025" # Web UI + + prometheus: + image: prom/prometheus:v2.50.1 + container_name: luxe_prometheus + restart: always + ports: + - "9090:9090" + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro + + grafana: + image: grafana/grafana:10.3.3 + container_name: luxe_grafana + restart: always + ports: + - "3001:3000" + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin + +volumes: + postgres_data: + redis_data: + minio_data: diff --git a/docker/prometheus.yml b/docker/prometheus.yml new file mode 100644 index 0000000..c6f4ae2 --- /dev/null +++ b/docker/prometheus.yml @@ -0,0 +1,8 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: "luxe-gateway" + static_configs: + - targets: ["host.docker.internal:3000"] diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..73ad9d6 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,50 @@ +# 🏛️ Luxe Platform - Backend Architecture Blueprint + +## System Overview +**Luxe Platform** is an enterprise classified marketplace designed for high availability, zero-downtime deployment, horizontal scale, and millions of concurrent users & listings. + +The backend is built following **Clean Architecture** as a **Microservice-Ready Monorepo**. + +``` + +------------------------+ + | Cloudflare WAF / CDN | + +-----------+------------+ + | + v + +------------------------+ + | Fastify API Gateway | + +-----------+------------+ + | + +-------------------------------+-------------------------------+ + | | | + v v v ++--------------+ +---------------+ +---------------+ +| Identity & | | Listing & | | Wallet & | +| Auth Service | | Search Service| | Ledger Service| ++--------------+ +---------------+ +---------------+ + | | | + +-------------------------------+-------------------------------+ + | + v + +----------------------------+ + | PostgreSQL 16 (Primary) | + | Redis 7 (Cache & BullMQ) | + | MinIO / Cloudflare R2 | + +----------------------------+ +``` + +--- + +## Workspace Structure +- **`packages/database`**: Central Prisma ORM data layer with PostgreSQL schemas, soft deletes, GIN/B-tree indexing, and seeder scripts. +- **`packages/shared`**: Enterprise core utilities, standard HTTP response wrappers, Argon2 password hashing, JWT token rotation, Zod request schemas, and dynamic RBAC policies. +- **`services/gateway`**: High-performance Fastify API Gateway handling TLS termination, rate limiting, request validation, authentication hooks, Swagger UI docs, and Socket.IO real-time websockets. +- **`services/notification-service`**: Async background worker powered by BullMQ processing multi-channel notifications (Email, SMS, WhatsApp, Telegram). + +--- + +## Clean Architecture Layers +1. **Domain Layer**: Entity models defined in Prisma & TypeScript schemas. +2. **Use-Case / Service Layer**: Business logic for verification queues, immutable wallet ledgers, coupon processing, and search filters. +3. **Interface Adapters**: Fastify routes, controllers, and socket handlers. +4. **Infrastructure Layer**: PostgreSQL, Redis, MinIO S3, Prometheus, Grafana, and Docker/Kubernetes orchestrators. diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..378cb03 --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,55 @@ +# 🚀 Luxe Platform - Production Deployment Guide + +This guide covers deployment procedures for local Docker Compose development, production Kubernetes environments, and database migrations. + +--- + +## 1. Quick Local Setup with Docker Compose + +### Prerequisites +- Node.js >= 22.0.0 LTS +- Docker & Docker Compose + +### Step 1: Clone & Configure Environment +```bash +cp .env.example .env +``` + +### Step 2: Spin Up Infrastructure Containers +```bash +npm run docker:up +``` +This launches: +- **PostgreSQL 16**: `localhost:5432` +- **Redis 7**: `localhost:6379` +- **MinIO S3**: `http://localhost:9000` (Console: `http://localhost:9001`) +- **Mailpit SMTP**: `http://localhost:8025` +- **Prometheus**: `http://localhost:9090` +- **Grafana**: `http://localhost:3001` + +### Step 3: Run Database Migrations & Seeding +```bash +npm run db:push +npm run db:seed +``` + +### Step 4: Launch Gateway & Workers in Dev Mode +```bash +npm run dev +``` +Open [http://localhost:3000/documentation](http://localhost:3000/documentation) to view the live OpenAPI Swagger UI. + +--- + +## 2. Production Deployment on Kubernetes + +### Step 1: Apply Kubernetes Manifests +```bash +kubectl apply -f k8s/deployment.yaml +``` + +### Step 2: Verify Autoscaling (HPA) +```bash +kubectl get hpa luxe-gateway-hpa +``` +The Gateway will automatically scale up to **20 replicas** when CPU utilization exceeds 75%. diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..23a3e0c --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,20 @@ +# 🛡️ Luxe Platform - Enterprise Security Model & Audit Checklist + +## Security Mitigations & Standards + +### 1. OWASP Top 10 Protections +- **SQL Injection**: Handled natively by Prisma ORM prepared statements and parameterized queries. +- **Cross-Site Scripting (XSS)**: Handled by `@fastify/helmet` setting strict HTTP security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options). +- **Cross-Site Request Forgery (CSRF)**: Refresh tokens are stored in `HttpOnly`, `SameSite=Strict`, `Secure` cookies. +- **Brute Force Protection**: Rate limiting enforced at 100 requests / 15 minutes per IP via `@fastify/rate-limit`, plus progressive account login lockouts. + +### 2. Password & Encryption Standards +- Password Hashing: **Argon2id** with 64MB memory cost, 3 iterations, and 4 degree parallelism. +- Tokens: **JWT (ES2022 / RS256 or HS256)** short-lived access tokens (15m) with refresh token rotation (7d). + +### 3. Dynamic Policy RBAC +- Role-based and Permission-based dynamic policy evaluation on every sensitive endpoint. +- Database-driven permissions preventing hardcoded authorization checks. + +### 4. Audit Logging +- Every sensitive action (Status updates, User suspensions, Verification approvals, Balance adjustments) records an immutable `AuditLog` entry tagged with IP, User Agent, Timestamp, and Admin ID. diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml new file mode 100644 index 0000000..d565dad --- /dev/null +++ b/k8s/deployment.yaml @@ -0,0 +1,102 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: luxe-gateway + namespace: default + labels: + app: luxe-gateway +spec: + replicas: 3 + selector: + matchLabels: + app: luxe-gateway + template: + metadata: + labels: + app: luxe-gateway + spec: + containers: + - name: gateway + image: luxe/gateway:1.0.0 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 3000 + envFrom: + - configMapRef: + name: luxe-config + - secretRef: + name: luxe-secrets + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "1000m" + livenessProbe: + httpGet: + path: /health + port: 3000 + initialDelaySeconds: 15 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /ready + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 5 +--- +apiVersion: v1 +kind: Service +metadata: + name: luxe-gateway-service +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 3000 + selector: + app: luxe-gateway +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: luxe-ingress + annotations: + kubernetes.io/ingress.class: "nginx" + cert-manager.io/cluster-issuer: "letsencrypt-prod" +spec: + tls: + - hosts: + - api.luxe.com + secretName: luxe-tls-secret + rules: + - host: api.luxe.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: luxe-gateway-service + port: + number: 80 +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: luxe-gateway-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: luxe-gateway + minReplicas: 3 + maxReplicas: 20 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 75 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d17f75b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2969 @@ +{ + "name": "luxe-backend-monorepo", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "luxe-backend-monorepo", + "version": "1.0.0", + "workspaces": [ + "packages/*", + "services/*" + ], + "devDependencies": { + "@types/node": "^22.10.1", + "prettier": "^3.4.2", + "typescript": "^5.7.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/accept-negotiator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz", + "integrity": "sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", + "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz", + "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==", + "license": "MIT" + }, + "node_modules/@fastify/cookie": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-10.0.1.tgz", + "integrity": "sha512-NV/wbCUv4ETJ5KM1KMu0fLx0nSCm9idIxwg66NZnNbfPQH3rdbx6k0qRs5uy0y+MhBgvDudYRA30KlK659chyw==", + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.1", + "fastify-plugin": "^5.0.0" + } + }, + "node_modules/@fastify/cors": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-10.1.0.tgz", + "integrity": "sha512-MZyBCBJtII60CU9Xme/iE4aEy8G7QpzGR8zkdXZkDFt7ElEMachbE61tfhAG/bvSaULlqlf0huMT12T7iqEmdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "mnemonist": "0.40.0" + } + }, + "node_modules/@fastify/deepmerge": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-3.2.1.tgz", + "integrity": "sha512-N5Oqvltoa2r9z1tbx4xjky0oRR60v+T47Ic4J1ukoVQcptLOrIdRnCSdTGmOmajZuHVKlTnfcmrjyqsGEW1ztA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.1.0.tgz", + "integrity": "sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^7.0.0" + } + }, + "node_modules/@fastify/formbody": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@fastify/formbody/-/formbody-8.0.2.tgz", + "integrity": "sha512-84v5J2KrkXzjgBpYnaNRPqwgMsmY7ZDjuj0YVuMR3NXCJRCgKEZy/taSP1wUYGn0onfxJpLyRGDLa+NMaDJtnA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-querystring": "^1.1.2", + "fastify-plugin": "^5.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", + "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/helmet": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@fastify/helmet/-/helmet-12.0.1.tgz", + "integrity": "sha512-kkjBcedWwdflRThovGuvN9jB2QQLytBqArCFPdMIb7o2Fp0l/H3xxYi/6x/SSRuH/FFt9qpTGIfJz2bfnMrLqA==", + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "helmet": "^7.1.0" + } + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/multipart": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@fastify/multipart/-/multipart-9.4.0.tgz", + "integrity": "sha512-Z404bzZeLSXTBmp/trCBuoVFX28pM7rhv849Q5TsbTFZHuk1lc4QjQITTPK92DKVpXmNtJXeHSSc7GYvqFpxAQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^3.0.0", + "@fastify/deepmerge": "^3.0.0", + "@fastify/error": "^4.0.0", + "fastify-plugin": "^5.0.0", + "secure-json-parse": "^4.0.0" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@fastify/rate-limit": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@fastify/rate-limit/-/rate-limit-10.3.0.tgz", + "integrity": "sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "fastify-plugin": "^5.0.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/@fastify/send": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.1.0.tgz", + "integrity": "sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "escape-html": "~1.0.3", + "fast-decode-uri-component": "^1.0.1", + "http-errors": "^2.0.0", + "mime": "^3" + } + }, + "node_modules/@fastify/static": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-9.3.0.tgz", + "integrity": "sha512-9YMYRpCOtMBrqKYWcqiw7ykOrn4D0jogHpJrFS0KGeSuOwzKMM5/mjj7B0CFLVoQ6htqKYw//Zs7APn9DBq05w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/accept-negotiator": "^2.0.0", + "@fastify/send": "^4.0.0", + "content-disposition": "^1.0.1", + "fastify-plugin": "^6.0.0", + "fastq": "^1.17.1", + "glob": "^13.0.0" + } + }, + "node_modules/@fastify/static/node_modules/fastify-plugin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-6.0.0.tgz", + "integrity": "sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/swagger": { + "version": "9.8.1", + "resolved": "https://registry.npmjs.org/@fastify/swagger/-/swagger-9.8.1.tgz", + "integrity": "sha512-VpHMnqZTY8iBZYJE8WWkbKPrXIYWy2rDfIf5qLr6DzZSpQYZ+KxQVcJFiq/AMlvNwI4gCBd66++iUlxXXGT0IQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fastify-plugin": "^6.0.0", + "json-schema-resolver": "^3.0.0", + "openapi-types": "^12.1.3", + "rfdc": "^1.3.1", + "yaml": "^2.4.2" + } + }, + "node_modules/@fastify/swagger-ui": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@fastify/swagger-ui/-/swagger-ui-5.2.6.tgz", + "integrity": "sha512-OMnms0O5s9wb6wis/K5nlrAMLsgUbr1GA8uphM41IasWe3AFdgxz6r/3bA9HTxlDNUYc2FGGKeqMp3ntxmSiNA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/static": "^9.1.2", + "fastify-plugin": "^5.0.0", + "openapi-types": "^12.1.3", + "rfdc": "^1.3.1", + "yaml": "^2.4.1" + } + }, + "node_modules/@fastify/swagger/node_modules/fastify-plugin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-6.0.0.tgz", + "integrity": "sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@ioredis/commands": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.10.0.tgz", + "integrity": "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==", + "license": "MIT" + }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@luxe/database": { + "resolved": "packages/database", + "link": true + }, + "node_modules/@luxe/gateway": { + "resolved": "services/gateway", + "link": true + }, + "node_modules/@luxe/notification-service": { + "resolved": "services/notification-service", + "link": true + }, + "node_modules/@luxe/shared": { + "resolved": "packages/shared", + "link": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@phc/format": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", + "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@prisma/client": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.3.tgz", + "integrity": "sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.3.tgz", + "integrity": "sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.21.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.3.tgz", + "integrity": "sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.3.tgz", + "integrity": "sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.3", + "@prisma/get-platform": "6.19.3" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.3.tgz", + "integrity": "sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.3" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.3.tgz", + "integrity": "sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@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==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/nodemailer": { + "version": "6.4.24", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.24.tgz", + "integrity": "sha512-Ww4u0rT9wQNXh4JiQaIwx3QWdcOFXzOjQA2zc+jtFYNmQiT4mIUqcDin51bDFdkzKubFnQCZNK7FIHlPKQ/q9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "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-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/argon2": { + "version": "0.41.1", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.41.1.tgz", + "integrity": "sha512-dqCW8kJXke8Ik+McUcMDltrbuAWETPyU6iq+4AhxqKphWi7pChB/Zgd/Tp/o8xRLbg8ksMj46F/vph9wnxpTzQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@phc/format": "^1.0.0", + "node-addon-api": "^8.1.0", + "node-gyp-build": "^4.8.1" + }, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.3.0.tgz", + "integrity": "sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "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==", + "license": "BSD-3-Clause" + }, + "node_modules/bullmq": { + "version": "5.81.1", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.81.1.tgz", + "integrity": "sha512-sofyDem/1oPT3BSe4MGJQYhDM5+D+js41VIfhd47db9b/xfY6wZ9Use9RAZBRwqtxQqZ1NhUTae19+kCysWbBg==", + "license": "MIT", + "dependencies": { + "cron-parser": "4.9.0", + "ioredis": "5.11.1", + "msgpackr": "2.0.4", + "node-abort-controller": "3.1.1", + "semver": "7.8.5", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=12.22.0" + }, + "peerDependencies": { + "redis": ">=5.0.0" + }, + "peerDependenciesMeta": { + "redis": { + "optional": true + } + } + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz", + "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/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==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "license": "MIT", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/effect": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.21.0.tgz", + "integrity": "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/engine.io": { + "version": "6.6.9", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.9.tgz", + "integrity": "sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/exsolve": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.0.tgz", + "integrity": "sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", + "integrity": "sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^4.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-json-stringify/node_modules/fast-uri": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.1.1.tgz", + "integrity": "sha512-YPOs1zD5TG2+EZt+r88LwF6mclA7TPkpwMP7ZN3TO2HiHS8TXvq7QA/17iJsV9dubcLo/f8eEYqMBruyQV21hQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.10.0.tgz", + "integrity": "sha512-A9L0ziuWGQHgEEVgF3davQ9vbD93IuX+lo2IsxapQmu5b/Y/ynn9m9K5JHt9dvyJXOFc5iN0Zk5GHEOqnzhWjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^7.0.0", + "find-my-way": "^9.6.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastify-plugin": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz", + "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/find-my-way": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.7.0.tgz", + "integrity": "sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ioredis": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.11.1.tgz", + "integrity": "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.10.0", + "cluster-key-slot": "1.1.1", + "debug": "4.4.3", + "denque": "2.1.0", + "redis-errors": "1.2.0", + "redis-parser": "3.0.0", + "standard-as-callback": "2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ipaddr.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-resolver/-/json-schema-resolver-3.0.0.tgz", + "integrity": "sha512-HqMnbz0tz2DaEJ3ntsqtx3ezzZyDE7G56A/pPY/NGmrPu76UzsWquOpHFRAf5beTNXoH2LU5cQePVvRli1nchA==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fast-uri": "^3.0.5", + "rfdc": "^1.1.4" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/Eomm/json-schema-resolver?sponsor=1" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "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": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mnemonist": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.0.tgz", + "integrity": "sha512-kdd8AFNig2AD5Rkih7EPCXhu/iMvwevQFX/uEiGhZyPZi7fHqOoF4V4kHLpCfysxXMgQ4B52kdPMCwARshKvEg==", + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/msgpackr": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-2.0.4.tgz", + "integrity": "sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.4" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.0.tgz", + "integrity": "sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "devOptional": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/nodemailer": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nypm": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.8.tgz", + "integrity": "sha512-Q9K4Diu6l5u6xJQogeFSs/zKtyMSgFKFtRQV+tHP4kL7KPm2grpBU0dFIwFaXwNxN0MtfKWc43VpCugAa+LPsw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.2.2", + "pathe": "^2.0.3", + "tinyexec": "^1.2.4" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obliterator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", + "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", + "license": "MIT" + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prisma": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.3.tgz", + "integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.19.3", + "@prisma/engines": "6.19.3" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "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" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "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.8", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz", + "integrity": "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.21.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz", + "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/thread-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz", + "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/toad-cache": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz", + "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "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==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "license": "MIT", + "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/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "packages/database": { + "name": "@luxe/database", + "version": "1.0.0", + "dependencies": { + "@prisma/client": "^6.0.1" + }, + "devDependencies": { + "prisma": "^6.0.1", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + } + }, + "packages/shared": { + "name": "@luxe/shared", + "version": "1.0.0", + "dependencies": { + "argon2": "^0.41.1", + "fastify": "^5.2.0", + "jsonwebtoken": "^9.0.2", + "pino": "^9.6.0", + "zod": "^3.24.1" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.7", + "typescript": "^5.7.2" + } + }, + "services/gateway": { + "name": "@luxe/gateway", + "version": "1.0.0", + "dependencies": { + "@fastify/cookie": "^10.0.1", + "@fastify/cors": "^10.0.1", + "@fastify/formbody": "^8.0.0", + "@fastify/helmet": "^12.0.1", + "@fastify/multipart": "^9.0.2", + "@fastify/rate-limit": "^10.2.1", + "@fastify/swagger": "^9.4.0", + "@fastify/swagger-ui": "^5.2.0", + "@luxe/database": "*", + "@luxe/shared": "*", + "bullmq": "^5.34.8", + "fastify": "^5.2.0", + "ioredis": "^5.4.2", + "socket.io": "^4.8.1", + "zod": "^3.24.1" + }, + "devDependencies": { + "@types/node": "^22.10.1", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + } + }, + "services/notification-service": { + "name": "@luxe/notification-service", + "version": "1.0.0", + "dependencies": { + "@luxe/database": "*", + "@luxe/shared": "*", + "bullmq": "^5.34.8", + "ioredis": "^5.4.2", + "nodemailer": "^6.9.16" + }, + "devDependencies": { + "@types/node": "^22.10.1", + "@types/nodemailer": "^6.4.17", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a2f5cc9 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "luxe-backend-monorepo", + "version": "1.0.0", + "private": true, + "description": "Production-ready enterprise-grade microservice backend for Luxe classified marketplace platform", + "workspaces": [ + "packages/*", + "services/*" + ], + "scripts": { + "dev": "node scripts/dev.mjs", + "build": "npm run build --workspaces --if-present", + "test": "npm run test --workspaces --if-present", + "lint": "npm run lint --workspaces --if-present", + "format": "prettier --write \"**/*.{ts,json,md}\"", + "db:generate": "npm run generate --workspace=@luxe/database", + "db:push": "npm run push --workspace=@luxe/database", + "db:seed": "npm run seed --workspace=@luxe/database", + "docker:up": "docker-compose -f docker/docker-compose.yml up -d", + "docker:down": "docker-compose -f docker/docker-compose.yml down" + }, + "devDependencies": { + "@types/node": "^22.10.1", + "prettier": "^3.4.2", + "typescript": "^5.7.2" + }, + "engines": { + "node": ">=22.0.0" + } +} diff --git a/packages/database/package.json b/packages/database/package.json new file mode 100644 index 0000000..3be3e42 --- /dev/null +++ b/packages/database/package.json @@ -0,0 +1,21 @@ +{ + "name": "@luxe/database", + "version": "1.0.0", + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "generate": "prisma generate", + "push": "prisma db push", + "seed": "tsx prisma/seed.ts" + }, + "dependencies": { + "@prisma/client": "^6.0.1" + }, + "devDependencies": { + "prisma": "^6.0.1", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + } +} diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma new file mode 100644 index 0000000..df3e4bc --- /dev/null +++ b/packages/database/prisma/schema.prisma @@ -0,0 +1,685 @@ +// Luxe Platform Production Database Schema +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +generator client { + provider = "prisma-client-js" +} + +// ========================================== +// ENUMS +// ========================================== + +enum RoleType { + GUEST + USER + ADVERTISER + AGENCY + MODERATOR + SUPPORT + CONTENT_MODERATOR + FINANCE + ADMIN + SUPER_ADMIN +} + +enum VerificationStatus { + UNVERIFIED + PENDING + VERIFIED + REJECTED +} + +enum ListingStatus { + DRAFT + PENDING_APPROVAL + ACTIVE + REJECTED + EXPIRED + ARCHIVED + SUSPENDED +} + +enum TransactionType { + RECHARGE + DEBIT + CREDIT + WITHDRAWAL + REFUND + BONUS +} + +enum TransactionStatus { + PENDING + SUCCESS + FAILED + CANCELLED +} + +enum PaymentProvider { + STRIPE + RAZORPAY + PAYPAL + WALLET +} + +enum AdType { + BANNER + SPONSORED_LISTING + BOOST_LISTING +} + +enum AdTargetType { + CITY + CATEGORY + GLOBAL +} + +enum NotificationChannel { + EMAIL + SMS + PUSH + WHATSAPP + TELEGRAM + IN_APP +} + +enum ReportStatus { + OPEN + UNDER_REVIEW + RESOLVED + DISMISSED +} + +enum TicketPriority { + LOW + MEDIUM + HIGH + URGENT +} + +enum TicketStatus { + OPEN + IN_PROGRESS + WAITING_ON_USER + RESOLVED + CLOSED +} + +// ========================================== +// AUTH & USER IDENTITY +// ========================================== + +model User { + id String @id @default(uuid()) @db.Uuid + email String @unique + phone String? @unique + username String @unique + passwordHash String + role RoleType @default(USER) + status String @default("ACTIVE") // ACTIVE, SUSPENDED, BANNED + emailVerifiedAt DateTime? + phoneVerifiedAt DateTime? + twoFactorEnabled Boolean @default(false) + twoFactorSecret String? + avatar String? + bio String? + language String @default("en") + currency String @default("USD") + lastLoginAt DateTime? + lastLoginIp String? + failedLoginCount Int @default(0) + lockedUntil DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + // Relations + sessions Session[] + devices Device[] + userRoles UserRole[] + profile AdvertiserProfile? + wallet Wallet? + reviews Review[] + favorites Favorite[] + savedSearches SavedSearch[] + notifications Notification[] + sentMessages Message[] @relation("SentMessages") + receivedMessages Message[] @relation("ReceivedMessages") + blogComments BlogComment[] + forumThreads ForumThread[] + forumReplies ForumReply[] + reportsSubmitted Report[] @relation("ReporterUser") + supportTickets SupportTicket[] + auditLogs AuditLog[] + + @@index([email]) + @@index([phone]) + @@index([username]) + @@index([role, status]) +} + +model Role { + id String @id @default(uuid()) @db.Uuid + name String @unique + code RoleType @unique + description String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + rolePermissions RolePermission[] + userRoles UserRole[] +} + +model Permission { + id String @id @default(uuid()) @db.Uuid + resource String // e.g. listing, user, invoice + action String // e.g. create, read, update, delete, approve + description String? + createdAt DateTime @default(now()) + + rolePermissions RolePermission[] + + @@unique([resource, action]) +} + +model RolePermission { + roleId String @db.Uuid + permissionId String @db.Uuid + + role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) + permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade) + + @@id([roleId, permissionId]) +} + +model UserRole { + userId String @db.Uuid + roleId String @db.Uuid + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) + + @@id([userId, roleId]) +} + +model Session { + id String @id @default(uuid()) @db.Uuid + userId String @db.Uuid + refreshToken String @unique + ipAddress String? + userAgent String? + isRevoked Boolean @default(false) + expiresAt DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@index([refreshToken]) +} + +model Device { + id String @id @default(uuid()) @db.Uuid + userId String @db.Uuid + deviceToken String @unique + platform String // ios, android, web + ipAddress String? + lastSeenAt DateTime @default(now()) + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) +} + +// ========================================== +// ADVERTISER & LISTING DOMAIN +// ========================================== + +model AdvertiserProfile { + id String @id @default(uuid()) @db.Uuid + userId String @unique @db.Uuid + type String @default("INDIVIDUAL") // INDIVIDUAL, AGENCY + agencyName String? + contactEmail String? + contactPhone String? + whatsapp String? + telegram String? + website String? + verificationStatus VerificationStatus @default(UNVERIFIED) + rating Float @default(0.0) + reviewCount Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + listings Listing[] + verifications VerificationRequest[] +} + +model Category { + id String @id @default(uuid()) @db.Uuid + name String + slug String @unique + icon String? + description String? + parentId String? @db.Uuid + isFeatured Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + parent Category? @relation("CategoryHierarchy", fields: [parentId], references: [id]) + children Category[] @relation("CategoryHierarchy") + listings Listing[] + + @@index([slug]) +} + +model City { + id String @id @default(uuid()) @db.Uuid + name String + slug String @unique + state String? + country String @default("US") + image String? + latitude Float? + longitude Float? + createdAt DateTime @default(now()) + + areas Area[] + listings Listing[] + + @@index([slug]) +} + +model Area { + id String @id @default(uuid()) @db.Uuid + cityId String @db.Uuid + name String + slug String + latitude Float? + longitude Float? + createdAt DateTime @default(now()) + + city City @relation(fields: [cityId], references: [id], onDelete: Cascade) + listings Listing[] + + @@unique([cityId, slug]) +} + +model Listing { + id String @id @default(uuid()) @db.Uuid + profileId String @db.Uuid + categoryId String @db.Uuid + cityId String @db.Uuid + areaId String? @db.Uuid + title String + slug String @unique + description String + price Float + currency String @default("USD") + age Int? + height String? + tagline String? + isOnline Boolean @default(true) + isFeatured Boolean @default(false) + isPremium Boolean @default(false) + isVerified Boolean @default(false) + status ListingStatus @default(PENDING_APPROVAL) + viewsCount Int @default(0) + callsCount Int @default(0) + languages String[] @default([]) + tags String[] @default([]) + attributes Json? @default("{}") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + advertiser AdvertiserProfile @relation(fields: [profileId], references: [id], onDelete: Cascade) + category Category @relation(fields: [categoryId], references: [id]) + city City @relation(fields: [cityId], references: [id]) + area Area? @relation(fields: [areaId], references: [id]) + gallery ListingGallery[] + reviews Review[] + favorites Favorite[] + ads Advertisement[] + + @@index([slug]) + @@index([cityId, categoryId, status]) + @@index([isFeatured, isOnline]) + @@index([price]) +} + +model ListingGallery { + id String @id @default(uuid()) @db.Uuid + listingId String @db.Uuid + url String + mediaType String @default("IMAGE") // IMAGE, VIDEO + thumbnail String? + isPrimary Boolean @default(false) + order Int @default(0) + createdAt DateTime @default(now()) + + listing Listing @relation(fields: [listingId], references: [id], onDelete: Cascade) + + @@index([listingId]) +} + +model Review { + id String @id @default(uuid()) @db.Uuid + listingId String @db.Uuid + userId String @db.Uuid + rating Int // 1 to 5 + comment String + isApproved Boolean @default(true) + createdAt DateTime @default(now()) + + listing Listing @relation(fields: [listingId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([listingId]) +} + +model Favorite { + id String @id @default(uuid()) @db.Uuid + userId String @db.Uuid + listingId String @db.Uuid + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + listing Listing @relation(fields: [listingId], references: [id], onDelete: Cascade) + + @@unique([userId, listingId]) +} + +model SavedSearch { + id String @id @default(uuid()) @db.Uuid + userId String @db.Uuid + name String + filters Json + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +// ========================================== +// WALLET, FINANCIAL LEDGER & PAYMENTS +// ========================================== + +model Wallet { + id String @id @default(uuid()) @db.Uuid + userId String @unique @db.Uuid + balance Float @default(0.0) + credits Float @default(0.0) + currency String @default("USD") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + ledgers WalletLedger[] + invoices Invoice[] +} + +model WalletLedger { + id String @id @default(uuid()) @db.Uuid + walletId String @db.Uuid + type TransactionType + amount Float + balanceBefore Float + balanceAfter Float + referenceId String? // Order or Transaction ID + description String + createdAt DateTime @default(now()) + + wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade) + + @@index([walletId]) +} + +model Transaction { + id String @id @default(uuid()) @db.Uuid + userId String @db.Uuid + provider PaymentProvider + type TransactionType + status TransactionStatus @default(PENDING) + amount Float + currency String @default("USD") + gatewayTxnId String? @unique + metadata Json? @default("{}") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([userId]) + @@index([gatewayTxnId]) +} + +model Invoice { + id String @id @default(uuid()) @db.Uuid + walletId String @db.Uuid + invoiceNumber String @unique + amount Float + taxAmount Float @default(0.0) + totalAmount Float + currency String @default("USD") + pdfUrl String? + createdAt DateTime @default(now()) + + wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade) +} + +model Coupon { + id String @id @default(uuid()) @db.Uuid + code String @unique + discountPct Float? + fixedAmount Float? + minPurchase Float @default(0.0) + maxUses Int @default(100) + usedCount Int @default(0) + expiresAt DateTime? + createdAt DateTime @default(now()) +} + +model Subscription { + id String @id @default(uuid()) @db.Uuid + userId String @db.Uuid + planName String // BASIC, VIP, AGENCY_PRO + price Float + status String @default("ACTIVE") + startsAt DateTime @default(now()) + endsAt DateTime + createdAt DateTime @default(now()) +} + +model Advertisement { + id String @id @default(uuid()) @db.Uuid + listingId String? @db.Uuid + type AdType + target AdTargetType @default(GLOBAL) + targetId String? // City ID or Category ID + imageUrl String? + linkUrl String? + impressions Int @default(0) + clicks Int @default(0) + startsAt DateTime + endsAt DateTime + status String @default("ACTIVE") + createdAt DateTime @default(now()) + + listing Listing? @relation(fields: [listingId], references: [id], onDelete: Cascade) +} + +// ========================================== +// MESSAGING & NOTIFICATIONS +// ========================================== + +model Message { + id String @id @default(uuid()) @db.Uuid + senderId String @db.Uuid + receiverId String @db.Uuid + content String + isRead Boolean @default(false) + createdAt DateTime @default(now()) + + sender User @relation("SentMessages", fields: [senderId], references: [id], onDelete: Cascade) + receiver User @relation("ReceivedMessages", fields: [receiverId], references: [id], onDelete: Cascade) + + @@index([senderId, receiverId]) +} + +model Notification { + id String @id @default(uuid()) @db.Uuid + userId String @db.Uuid + channel NotificationChannel @default(IN_APP) + title String + body String + data Json? + isRead Boolean @default(false) + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId, isRead]) +} + +// ========================================== +// BLOG, FORUM & CONTENT +// ========================================== + +model BlogPost { + id String @id @default(uuid()) @db.Uuid + title String + slug String @unique + excerpt String + content String + coverImage String? + authorName String @default("Luxe Editorial") + category String + tags String[] @default([]) + readingTime Int @default(3) + likesCount Int @default(0) + isPublished Boolean @default(true) + publishedAt DateTime @default(now()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + comments BlogComment[] + + @@index([slug]) +} + +model BlogComment { + id String @id @default(uuid()) @db.Uuid + postId String @db.Uuid + userId String @db.Uuid + comment String + createdAt DateTime @default(now()) + + post BlogPost @relation(fields: [postId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model ForumThread { + id String @id @default(uuid()) @db.Uuid + userId String @db.Uuid + title String + slug String @unique + content String + category String + isPinned Boolean @default(false) + viewsCount Int @default(0) + likesCount Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + replies ForumReply[] + + @@index([slug]) +} + +model ForumReply { + id String @id @default(uuid()) @db.Uuid + threadId String @db.Uuid + userId String @db.Uuid + content String + createdAt DateTime @default(now()) + + thread ForumThread @relation(fields: [threadId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +// ========================================== +// MODERATION, VERIFICATION & AUDIT LOGS +// ========================================== + +model VerificationRequest { + id String @id @default(uuid()) @db.Uuid + profileId String @db.Uuid + idProofUrl String + selfieUrl String + notes String? + status VerificationStatus @default(PENDING) + rejectionReason String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + profile AdvertiserProfile @relation(fields: [profileId], references: [id], onDelete: Cascade) +} + +model Report { + id String @id @default(uuid()) @db.Uuid + reporterId String @db.Uuid + targetType String // LISTING, USER, REVIEW, FORUM + targetId String + reason String + description String? + status ReportStatus @default(OPEN) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + reporter User @relation("ReporterUser", fields: [reporterId], references: [id], onDelete: Cascade) +} + +model SupportTicket { + id String @id @default(uuid()) @db.Uuid + userId String @db.Uuid + ticketNum String @unique + subject String + description String + priority TicketPriority @default(MEDIUM) + status TicketStatus @default(OPEN) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model AuditLog { + id String @id @default(uuid()) @db.Uuid + userId String? @db.Uuid + action String // e.g. USER_BAN, PROFILE_VERIFY, ROLE_UPDATE + resource String // e.g. User, Listing + resourceId String? + ipAddress String? + userAgent String? + metadata Json? @default("{}") + createdAt DateTime @default(now()) + + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + + @@index([action]) + @@index([createdAt]) +} + +model FeatureFlag { + id String @id @default(uuid()) @db.Uuid + key String @unique + isEnabled Boolean @default(false) + description String? + metadata Json? @default("{}") + updatedAt DateTime @updatedAt +} diff --git a/packages/database/prisma/seed.d.ts b/packages/database/prisma/seed.d.ts new file mode 100644 index 0000000..0986b1c --- /dev/null +++ b/packages/database/prisma/seed.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=seed.d.ts.map \ No newline at end of file diff --git a/packages/database/prisma/seed.d.ts.map b/packages/database/prisma/seed.d.ts.map new file mode 100644 index 0000000..c1a0e0b --- /dev/null +++ b/packages/database/prisma/seed.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"seed.d.ts","sourceRoot":"","sources":["seed.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/packages/database/prisma/seed.js b/packages/database/prisma/seed.js new file mode 100644 index 0000000..cc43eca --- /dev/null +++ b/packages/database/prisma/seed.js @@ -0,0 +1,157 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +const client_1 = require("@prisma/client"); +const argon2 = __importStar(require("argon2")); +const prisma = new client_1.PrismaClient(); +async function main() { + console.log('🌱 Starting Luxe Platform Database Seeding...'); + // 1. Seed Roles + const rolesData = [ + { name: 'Super Admin', code: client_1.RoleType.SUPER_ADMIN, description: 'Full system root access' }, + { name: 'Admin', code: client_1.RoleType.ADMIN, description: 'Platform administration and management' }, + { name: 'Moderator', code: client_1.RoleType.MODERATOR, description: 'Content and report moderation' }, + { name: 'Content Moderator', code: client_1.RoleType.CONTENT_MODERATOR, description: 'Blog and forum moderation' }, + { name: 'Finance Manager', code: client_1.RoleType.FINANCE, description: 'Wallet, ledger, transaction and invoice access' }, + { name: 'Support Agent', code: client_1.RoleType.SUPPORT, description: 'User support and verification processing' }, + { name: 'Agency Advertiser', code: client_1.RoleType.AGENCY, description: 'Agency advertiser account managing multiple staff' }, + { name: 'Advertiser', code: client_1.RoleType.ADVERTISER, description: 'Individual advertiser listing creator' }, + { name: 'Registered User', code: client_1.RoleType.USER, description: 'Standard platform member' }, + { name: 'Guest', code: client_1.RoleType.GUEST, description: 'Unauthenticated visitor' }, + ]; + for (const role of rolesData) { + await prisma.role.upsert({ + where: { code: role.code }, + update: { name: role.name, description: role.description }, + create: role, + }); + } + // 2. Seed Permissions + const permissionsData = [ + { resource: 'users', action: 'read', description: 'View user profiles' }, + { resource: 'users', action: 'write', description: 'Modify users' }, + { resource: 'users', action: 'ban', description: 'Ban user accounts' }, + { resource: 'listings', action: 'create', description: 'Create marketplace listings' }, + { resource: 'listings', action: 'approve', description: 'Approve pending listings' }, + { resource: 'listings', action: 'delete', description: 'Delete marketplace listings' }, + { resource: 'verifications', action: 'approve', description: 'Approve ID verifications' }, + { resource: 'wallet', action: 'credit', description: 'Add bonus credits to user wallet' }, + { resource: 'admin', action: 'view_dashboard', description: 'Access admin analytics overview' }, + ]; + for (const perm of permissionsData) { + await prisma.permission.upsert({ + where: { resource_action: { resource: perm.resource, action: perm.action } }, + update: { description: perm.description }, + create: perm, + }); + } + // 3. Seed Super Admin User + const superAdminPasswordHash = await argon2.hash('LuxeSuperAdmin2026Secure!'); + const superAdminUser = await prisma.user.upsert({ + where: { email: 'admin@luxe.com' }, + update: {}, + create: { + email: 'admin@luxe.com', + username: 'superadmin', + passwordHash: superAdminPasswordHash, + role: client_1.RoleType.SUPER_ADMIN, + status: 'ACTIVE', + emailVerifiedAt: new Date(), + }, + }); + // Seed Wallet for Super Admin + await prisma.wallet.upsert({ + where: { userId: superAdminUser.id }, + update: {}, + create: { + userId: superAdminUser.id, + balance: 100000.0, + credits: 5000.0, + currency: 'USD', + }, + }); + // 4. Seed Cities + const cities = [ + { name: 'New York', slug: 'new-york', state: 'NY', country: 'US' }, + { name: 'Los Angeles', slug: 'los-angeles', state: 'CA', country: 'US' }, + { name: 'Miami', slug: 'miami', state: 'FL', country: 'US' }, + { name: 'London', slug: 'london', state: 'Greater London', country: 'UK' }, + { name: 'Dubai', slug: 'dubai', state: 'Dubai', country: 'AE' }, + ]; + for (const city of cities) { + await prisma.city.upsert({ + where: { slug: city.slug }, + update: {}, + create: city, + }); + } + // 5. Seed Categories + const categories = [ + { name: 'VIP Escorts', slug: 'vip-escorts', icon: 'crown', description: 'Premium verified VIP companionship' }, + { name: 'Massage & Spa', slug: 'massage-spa', icon: 'sparkles', description: 'Relaxing holistic & therapeutic wellness' }, + { name: 'Model & Hostess', slug: 'model-hostess', icon: 'camera', description: 'High fashion models for events & dates' }, + { name: 'Nightlife Companions', slug: 'nightlife-companions', icon: 'glass-cheers', description: 'Party & event escorts' }, + ]; + for (const cat of categories) { + await prisma.category.upsert({ + where: { slug: cat.slug }, + update: {}, + create: cat, + }); + } + // 6. Seed Feature Flags + const featureFlags = [ + { key: 'ENABLE_AI_IMAGE_MODERATION', isEnabled: true, description: 'Automatic NSFW and duplicate detection' }, + { key: 'ENABLE_CRYPTO_PAYMENTS', isEnabled: false, description: 'Accept Web3 crypto payments for wallet recharge' }, + { key: 'ENABLE_WHATSAPP_OTP', isEnabled: true, description: 'WhatsApp Business API for phone verification' }, + ]; + for (const flag of featureFlags) { + await prisma.featureFlag.upsert({ + where: { key: flag.key }, + update: {}, + create: flag, + }); + } + console.log('✅ Database Seeding Completed Successfully!'); +} +main() + .catch((e) => { + console.error('❌ Seeding failed:', e); + process.exit(1); +}) + .finally(async () => { + await prisma.$disconnect(); +}); +//# sourceMappingURL=seed.js.map \ No newline at end of file diff --git a/packages/database/prisma/seed.js.map b/packages/database/prisma/seed.js.map new file mode 100644 index 0000000..198b992 --- /dev/null +++ b/packages/database/prisma/seed.js.map @@ -0,0 +1 @@ +{"version":3,"file":"seed.js","sourceRoot":"","sources":["seed.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/database/prisma/seed.ts b/packages/database/prisma/seed.ts new file mode 100644 index 0000000..419bb2b --- /dev/null +++ b/packages/database/prisma/seed.ts @@ -0,0 +1,137 @@ +import { PrismaClient, RoleType } from '@prisma/client'; +import * as argon2 from 'argon2'; + +const prisma = new PrismaClient(); + +async function main() { + console.log('🌱 Starting Luxe Platform Database Seeding...'); + + // 1. Seed Roles + const rolesData: { name: string; code: RoleType; description: string }[] = [ + { name: 'Super Admin', code: RoleType.SUPER_ADMIN, description: 'Full system root access' }, + { name: 'Admin', code: RoleType.ADMIN, description: 'Platform administration and management' }, + { name: 'Moderator', code: RoleType.MODERATOR, description: 'Content and report moderation' }, + { name: 'Content Moderator', code: RoleType.CONTENT_MODERATOR, description: 'Blog and forum moderation' }, + { name: 'Finance Manager', code: RoleType.FINANCE, description: 'Wallet, ledger, transaction and invoice access' }, + { name: 'Support Agent', code: RoleType.SUPPORT, description: 'User support and verification processing' }, + { name: 'Agency Advertiser', code: RoleType.AGENCY, description: 'Agency advertiser account managing multiple staff' }, + { name: 'Advertiser', code: RoleType.ADVERTISER, description: 'Individual advertiser listing creator' }, + { name: 'Registered User', code: RoleType.USER, description: 'Standard platform member' }, + { name: 'Guest', code: RoleType.GUEST, description: 'Unauthenticated visitor' }, + ]; + + for (const role of rolesData) { + await prisma.role.upsert({ + where: { code: role.code }, + update: { name: role.name, description: role.description }, + create: role, + }); + } + + // 2. Seed Permissions + const permissionsData = [ + { resource: 'users', action: 'read', description: 'View user profiles' }, + { resource: 'users', action: 'write', description: 'Modify users' }, + { resource: 'users', action: 'ban', description: 'Ban user accounts' }, + { resource: 'listings', action: 'create', description: 'Create marketplace listings' }, + { resource: 'listings', action: 'approve', description: 'Approve pending listings' }, + { resource: 'listings', action: 'delete', description: 'Delete marketplace listings' }, + { resource: 'verifications', action: 'approve', description: 'Approve ID verifications' }, + { resource: 'wallet', action: 'credit', description: 'Add bonus credits to user wallet' }, + { resource: 'admin', action: 'view_dashboard', description: 'Access admin analytics overview' }, + ]; + + for (const perm of permissionsData) { + await prisma.permission.upsert({ + where: { resource_action: { resource: perm.resource, action: perm.action } }, + update: { description: perm.description }, + create: perm, + }); + } + + // 3. Seed Super Admin User + const superAdminPasswordHash = await argon2.hash('LuxeSuperAdmin2026Secure!'); + const superAdminUser = await prisma.user.upsert({ + where: { email: 'admin@luxe.com' }, + update: {}, + create: { + email: 'admin@luxe.com', + username: 'superadmin', + passwordHash: superAdminPasswordHash, + role: RoleType.SUPER_ADMIN, + status: 'ACTIVE', + emailVerifiedAt: new Date(), + }, + }); + + // Seed Wallet for Super Admin + await prisma.wallet.upsert({ + where: { userId: superAdminUser.id }, + update: {}, + create: { + userId: superAdminUser.id, + balance: 100000.0, + credits: 5000.0, + currency: 'USD', + }, + }); + + // 4. Seed Cities + const cities = [ + { name: 'New York', slug: 'new-york', state: 'NY', country: 'US' }, + { name: 'Los Angeles', slug: 'los-angeles', state: 'CA', country: 'US' }, + { name: 'Miami', slug: 'miami', state: 'FL', country: 'US' }, + { name: 'London', slug: 'london', state: 'Greater London', country: 'UK' }, + { name: 'Dubai', slug: 'dubai', state: 'Dubai', country: 'AE' }, + ]; + + for (const city of cities) { + await prisma.city.upsert({ + where: { slug: city.slug }, + update: {}, + create: city, + }); + } + + // 5. Seed Categories + const categories = [ + { name: 'VIP Escorts', slug: 'vip-escorts', icon: 'crown', description: 'Premium verified VIP companionship' }, + { name: 'Massage & Spa', slug: 'massage-spa', icon: 'sparkles', description: 'Relaxing holistic & therapeutic wellness' }, + { name: 'Model & Hostess', slug: 'model-hostess', icon: 'camera', description: 'High fashion models for events & dates' }, + { name: 'Nightlife Companions', slug: 'nightlife-companions', icon: 'glass-cheers', description: 'Party & event escorts' }, + ]; + + for (const cat of categories) { + await prisma.category.upsert({ + where: { slug: cat.slug }, + update: {}, + create: cat, + }); + } + + // 6. Seed Feature Flags + const featureFlags = [ + { key: 'ENABLE_AI_IMAGE_MODERATION', isEnabled: true, description: 'Automatic NSFW and duplicate detection' }, + { key: 'ENABLE_CRYPTO_PAYMENTS', isEnabled: false, description: 'Accept Web3 crypto payments for wallet recharge' }, + { key: 'ENABLE_WHATSAPP_OTP', isEnabled: true, description: 'WhatsApp Business API for phone verification' }, + ]; + + for (const flag of featureFlags) { + await prisma.featureFlag.upsert({ + where: { key: flag.key }, + update: {}, + create: flag, + }); + } + + console.log('✅ Database Seeding Completed Successfully!'); +} + +main() + .catch((e) => { + console.error('❌ Seeding failed:', e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/packages/database/src/index.d.ts b/packages/database/src/index.d.ts new file mode 100644 index 0000000..0f82c00 --- /dev/null +++ b/packages/database/src/index.d.ts @@ -0,0 +1,7 @@ +import { PrismaClient } from '@prisma/client'; +declare global { + var prismaGlobal: PrismaClient | undefined; +} +export declare const prisma: PrismaClient; +export * from '@prisma/client'; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/database/src/index.d.ts.map b/packages/database/src/index.d.ts.map new file mode 100644 index 0000000..e2a885a --- /dev/null +++ b/packages/database/src/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,OAAO,CAAC,MAAM,CAAC;IAGb,IAAI,YAAY,EAAE,YAAY,GAAG,SAAS,CAAC;CAC5C;AAED,eAAO,MAAM,MAAM,gIAIf,CAAC;AAML,cAAc,gBAAgB,CAAC"} \ No newline at end of file diff --git a/packages/database/src/index.js b/packages/database/src/index.js new file mode 100644 index 0000000..6de7189 --- /dev/null +++ b/packages/database/src/index.js @@ -0,0 +1,27 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.prisma = void 0; +const client_1 = require("@prisma/client"); +exports.prisma = globalThis.prismaGlobal ?? + new client_1.PrismaClient({ + log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], + }); +if (process.env.NODE_ENV !== 'production') { + globalThis.prismaGlobal = exports.prisma; +} +__exportStar(require("@prisma/client"), exports); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/packages/database/src/index.js.map b/packages/database/src/index.js.map new file mode 100644 index 0000000..61aae7a --- /dev/null +++ b/packages/database/src/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,2CAA8C;AAQjC,QAAA,MAAM,GACjB,UAAU,CAAC,YAAY;IACvB,IAAI,qBAAY,CAAC;QACf,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,aAAa,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;KACrF,CAAC,CAAC;AAEL,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;IAC1C,UAAU,CAAC,YAAY,GAAG,cAAM,CAAC;AACnC,CAAC;AAED,iDAA+B"} \ No newline at end of file diff --git a/packages/database/src/index.ts b/packages/database/src/index.ts new file mode 100644 index 0000000..499e188 --- /dev/null +++ b/packages/database/src/index.ts @@ -0,0 +1,19 @@ +import { PrismaClient } from '@prisma/client'; + +declare global { + // Allow global prisma in dev to prevent multiple instances + // eslint-disable-next-line no-var + var prismaGlobal: PrismaClient | undefined; +} + +export const prisma = + globalThis.prismaGlobal ?? + new PrismaClient({ + log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], + }); + +if (process.env.NODE_ENV !== 'production') { + globalThis.prismaGlobal = prisma; +} + +export * from '@prisma/client'; diff --git a/packages/database/tsconfig.json b/packages/database/tsconfig.json new file mode 100644 index 0000000..4b2e1f3 --- /dev/null +++ b/packages/database/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src/**/*"] +} diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000..83df59c --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,21 @@ +{ + "name": "@luxe/shared", + "version": "1.0.0", + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc" + }, + "dependencies": { + "argon2": "^0.41.1", + "fastify": "^5.2.0", + "jsonwebtoken": "^9.0.2", + "pino": "^9.6.0", + "zod": "^3.24.1" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.7", + "typescript": "^5.7.2" + } +} diff --git a/packages/shared/src/index.d.ts b/packages/shared/src/index.d.ts new file mode 100644 index 0000000..668acad --- /dev/null +++ b/packages/shared/src/index.d.ts @@ -0,0 +1,7 @@ +export * from './utils/response.js'; +export * from './utils/argon2.js'; +export * from './utils/jwt.js'; +export * from './logger/index.js'; +export * from './middleware/rbac.js'; +export * from './validators/index.js'; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/shared/src/index.d.ts.map b/packages/shared/src/index.d.ts.map new file mode 100644 index 0000000..8885a59 --- /dev/null +++ b/packages/shared/src/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC"} \ No newline at end of file diff --git a/packages/shared/src/index.js b/packages/shared/src/index.js new file mode 100644 index 0000000..1165851 --- /dev/null +++ b/packages/shared/src/index.js @@ -0,0 +1,23 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +__exportStar(require("./utils/response.js"), exports); +__exportStar(require("./utils/argon2.js"), exports); +__exportStar(require("./utils/jwt.js"), exports); +__exportStar(require("./logger/index.js"), exports); +__exportStar(require("./middleware/rbac.js"), exports); +__exportStar(require("./validators/index.js"), exports); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/packages/shared/src/index.js.map b/packages/shared/src/index.js.map new file mode 100644 index 0000000..dc7cea1 --- /dev/null +++ b/packages/shared/src/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,sDAAoC;AACpC,oDAAkC;AAClC,iDAA+B;AAC/B,oDAAkC;AAClC,uDAAqC;AACrC,wDAAsC"} \ No newline at end of file diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 0000000..191b81d --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,6 @@ +export * from './utils/response.js'; +export * from './utils/argon2.js'; +export * from './utils/jwt.js'; +export * from './logger/index.js'; +export * from './middleware/rbac.js'; +export * from './validators/index.js'; diff --git a/packages/shared/src/logger/index.d.ts b/packages/shared/src/logger/index.d.ts new file mode 100644 index 0000000..5811478 --- /dev/null +++ b/packages/shared/src/logger/index.d.ts @@ -0,0 +1,3 @@ +import pino from 'pino'; +export declare const logger: pino.Logger; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/shared/src/logger/index.d.ts.map b/packages/shared/src/logger/index.d.ts.map new file mode 100644 index 0000000..7e5c7c5 --- /dev/null +++ b/packages/shared/src/logger/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,eAAO,MAAM,MAAM,6BAOjB,CAAC"} \ No newline at end of file diff --git a/packages/shared/src/logger/index.js b/packages/shared/src/logger/index.js new file mode 100644 index 0000000..ded380c --- /dev/null +++ b/packages/shared/src/logger/index.js @@ -0,0 +1,16 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.logger = void 0; +const pino_1 = __importDefault(require("pino")); +exports.logger = (0, pino_1.default)({ + level: process.env.LOG_LEVEL || 'info', + formatters: { + level: (label) => ({ level: label }), + }, + timestamp: pino_1.default.stdTimeFunctions.isoTime, + redact: ['req.headers.authorization', 'password', 'token', 'creditCard'], +}); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/packages/shared/src/logger/index.js.map b/packages/shared/src/logger/index.js.map new file mode 100644 index 0000000..a98a301 --- /dev/null +++ b/packages/shared/src/logger/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";;;;;;AAAA,gDAAwB;AAEX,QAAA,MAAM,GAAG,IAAA,cAAI,EAAC;IACzB,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,MAAM;IACtC,UAAU,EAAE;QACV,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;KACrC;IACD,SAAS,EAAE,cAAI,CAAC,gBAAgB,CAAC,OAAO;IACxC,MAAM,EAAE,CAAC,2BAA2B,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,CAAC;CACzE,CAAC,CAAC"} \ No newline at end of file diff --git a/packages/shared/src/logger/index.ts b/packages/shared/src/logger/index.ts new file mode 100644 index 0000000..e54151b --- /dev/null +++ b/packages/shared/src/logger/index.ts @@ -0,0 +1,10 @@ +import pino from 'pino'; + +export const logger = pino({ + level: process.env.LOG_LEVEL || 'info', + formatters: { + level: (label) => ({ level: label }), + }, + timestamp: pino.stdTimeFunctions.isoTime, + redact: ['req.headers.authorization', 'password', 'token', 'creditCard'], +}); diff --git a/packages/shared/src/middleware/rbac.d.ts b/packages/shared/src/middleware/rbac.d.ts new file mode 100644 index 0000000..a921b09 --- /dev/null +++ b/packages/shared/src/middleware/rbac.d.ts @@ -0,0 +1,3 @@ +import { FastifyRequest, FastifyReply } from 'fastify'; +export declare function authorize(allowedRoles: string[]): (request: FastifyRequest, reply: FastifyReply) => Promise; +//# sourceMappingURL=rbac.d.ts.map \ No newline at end of file diff --git a/packages/shared/src/middleware/rbac.d.ts.map b/packages/shared/src/middleware/rbac.d.ts.map new file mode 100644 index 0000000..8fabf7c --- /dev/null +++ b/packages/shared/src/middleware/rbac.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"rbac.d.ts","sourceRoot":"","sources":["rbac.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAGvD,wBAAgB,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,IAChC,SAAS,cAAc,EAAE,OAAO,YAAY,wBAU3D"} \ No newline at end of file diff --git a/packages/shared/src/middleware/rbac.js b/packages/shared/src/middleware/rbac.js new file mode 100644 index 0000000..2f38237 --- /dev/null +++ b/packages/shared/src/middleware/rbac.js @@ -0,0 +1,16 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.authorize = authorize; +const response_js_1 = require("../utils/response.js"); +function authorize(allowedRoles) { + return async (request, reply) => { + const user = request.user; + if (!user) { + return reply.status(401).send((0, response_js_1.errorResponse)('Unauthorized: Authentication required')); + } + if (!allowedRoles.includes(user.role) && user.role !== 'SUPER_ADMIN') { + return reply.status(403).send((0, response_js_1.errorResponse)('Forbidden: Insufficient privileges')); + } + }; +} +//# sourceMappingURL=rbac.js.map \ No newline at end of file diff --git a/packages/shared/src/middleware/rbac.js.map b/packages/shared/src/middleware/rbac.js.map new file mode 100644 index 0000000..a13abe3 --- /dev/null +++ b/packages/shared/src/middleware/rbac.js.map @@ -0,0 +1 @@ +{"version":3,"file":"rbac.js","sourceRoot":"","sources":["rbac.ts"],"names":[],"mappings":";;AAGA,8BAWC;AAbD,sDAAqD;AAErD,SAAgB,SAAS,CAAC,YAAsB;IAC9C,OAAO,KAAK,EAAE,OAAuB,EAAE,KAAmB,EAAE,EAAE;QAC5D,MAAM,IAAI,GAAI,OAAe,CAAC,IAAI,CAAC;QACnC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAA,2BAAa,EAAC,uCAAuC,CAAC,CAAC,CAAC;QACxF,CAAC;QAED,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;YACrE,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAA,2BAAa,EAAC,oCAAoC,CAAC,CAAC,CAAC;QACrF,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"} \ No newline at end of file diff --git a/packages/shared/src/middleware/rbac.ts b/packages/shared/src/middleware/rbac.ts new file mode 100644 index 0000000..6da9fd5 --- /dev/null +++ b/packages/shared/src/middleware/rbac.ts @@ -0,0 +1,15 @@ +import { FastifyRequest, FastifyReply } from 'fastify'; +import { errorResponse } from '../utils/response.js'; + +export function authorize(allowedRoles: string[]) { + return async (request: FastifyRequest, reply: FastifyReply) => { + const user = (request as any).user; + if (!user) { + return reply.status(401).send(errorResponse('Unauthorized: Authentication required')); + } + + if (!allowedRoles.includes(user.role) && user.role !== 'SUPER_ADMIN') { + return reply.status(403).send(errorResponse('Forbidden: Insufficient privileges')); + } + }; +} diff --git a/packages/shared/src/utils/argon2.d.ts b/packages/shared/src/utils/argon2.d.ts new file mode 100644 index 0000000..038e5f2 --- /dev/null +++ b/packages/shared/src/utils/argon2.d.ts @@ -0,0 +1,3 @@ +export declare function hashPassword(password: string): Promise; +export declare function verifyPassword(hash: string, plainText: string): Promise; +//# sourceMappingURL=argon2.d.ts.map \ No newline at end of file diff --git a/packages/shared/src/utils/argon2.d.ts.map b/packages/shared/src/utils/argon2.d.ts.map new file mode 100644 index 0000000..3c22baf --- /dev/null +++ b/packages/shared/src/utils/argon2.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"argon2.d.ts","sourceRoot":"","sources":["argon2.ts"],"names":[],"mappings":"AAEA,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAOpE;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAMtF"} \ No newline at end of file diff --git a/packages/shared/src/utils/argon2.js b/packages/shared/src/utils/argon2.js new file mode 100644 index 0000000..085fe42 --- /dev/null +++ b/packages/shared/src/utils/argon2.js @@ -0,0 +1,55 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.hashPassword = hashPassword; +exports.verifyPassword = verifyPassword; +const argon2 = __importStar(require("argon2")); +async function hashPassword(password) { + return argon2.hash(password, { + type: argon2.argon2id, + memoryCost: Number(process.env.ARGON_MEMORY_COST) || 65536, + timeCost: Number(process.env.ARGON_TIME_COST) || 3, + parallelism: 4, + }); +} +async function verifyPassword(hash, plainText) { + try { + return await argon2.verify(hash, plainText); + } + catch (err) { + return false; + } +} +//# sourceMappingURL=argon2.js.map \ No newline at end of file diff --git a/packages/shared/src/utils/argon2.js.map b/packages/shared/src/utils/argon2.js.map new file mode 100644 index 0000000..2f11c86 --- /dev/null +++ b/packages/shared/src/utils/argon2.js.map @@ -0,0 +1 @@ +{"version":3,"file":"argon2.js","sourceRoot":"","sources":["argon2.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEA,oCAOC;AAED,wCAMC;AAjBD,+CAAiC;AAE1B,KAAK,UAAU,YAAY,CAAC,QAAgB;IACjD,OAAO,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE;QAC3B,IAAI,EAAE,MAAM,CAAC,QAAQ;QACrB,UAAU,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,IAAI,KAAK;QAC1D,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC;QAClD,WAAW,EAAE,CAAC;KACf,CAAC,CAAC;AACL,CAAC;AAEM,KAAK,UAAU,cAAc,CAAC,IAAY,EAAE,SAAiB;IAClE,IAAI,CAAC;QACH,OAAO,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAC9C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"} \ No newline at end of file diff --git a/packages/shared/src/utils/argon2.ts b/packages/shared/src/utils/argon2.ts new file mode 100644 index 0000000..371f5aa --- /dev/null +++ b/packages/shared/src/utils/argon2.ts @@ -0,0 +1,18 @@ +import * as argon2 from 'argon2'; + +export async function hashPassword(password: string): Promise { + return argon2.hash(password, { + type: argon2.argon2id, + memoryCost: Number(process.env.ARGON_MEMORY_COST) || 65536, + timeCost: Number(process.env.ARGON_TIME_COST) || 3, + parallelism: 4, + }); +} + +export async function verifyPassword(hash: string, plainText: string): Promise { + try { + return await argon2.verify(hash, plainText); + } catch (err) { + return false; + } +} diff --git a/packages/shared/src/utils/jwt.d.ts b/packages/shared/src/utils/jwt.d.ts new file mode 100644 index 0000000..27c17c2 --- /dev/null +++ b/packages/shared/src/utils/jwt.d.ts @@ -0,0 +1,10 @@ +export interface JwtPayload { + userId: string; + email: string; + role: string; + sessionId?: string; +} +export declare function generateAccessToken(payload: JwtPayload): string; +export declare function generateRefreshToken(payload: JwtPayload): string; +export declare function verifyToken(token: string): JwtPayload; +//# sourceMappingURL=jwt.d.ts.map \ No newline at end of file diff --git a/packages/shared/src/utils/jwt.d.ts.map b/packages/shared/src/utils/jwt.d.ts.map new file mode 100644 index 0000000..9124186 --- /dev/null +++ b/packages/shared/src/utils/jwt.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["jwt.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,UAAU,GAAG,MAAM,CAK/D;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,UAAU,GAAG,MAAM,CAKhE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAErD"} \ No newline at end of file diff --git a/packages/shared/src/utils/jwt.js b/packages/shared/src/utils/jwt.js new file mode 100644 index 0000000..f6f11c8 --- /dev/null +++ b/packages/shared/src/utils/jwt.js @@ -0,0 +1,28 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.generateAccessToken = generateAccessToken; +exports.generateRefreshToken = generateRefreshToken; +exports.verifyToken = verifyToken; +const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); +const JWT_SECRET = process.env.JWT_SECRET || 'super_secret_luxe_jwt_signing_key_2026_production_grade'; +const ACCESS_EXPIRATION = process.env.JWT_ACCESS_EXPIRATION || '15m'; +const REFRESH_EXPIRATION = process.env.JWT_REFRESH_EXPIRATION || '7d'; +function generateAccessToken(payload) { + return jsonwebtoken_1.default.sign(payload, JWT_SECRET, { + expiresIn: ACCESS_EXPIRATION, + issuer: 'luxe-api', + }); +} +function generateRefreshToken(payload) { + return jsonwebtoken_1.default.sign({ ...payload, type: 'refresh' }, JWT_SECRET, { + expiresIn: REFRESH_EXPIRATION, + issuer: 'luxe-api', + }); +} +function verifyToken(token) { + return jsonwebtoken_1.default.verify(token, JWT_SECRET, { issuer: 'luxe-api' }); +} +//# sourceMappingURL=jwt.js.map \ No newline at end of file diff --git a/packages/shared/src/utils/jwt.js.map b/packages/shared/src/utils/jwt.js.map new file mode 100644 index 0000000..8c6b835 --- /dev/null +++ b/packages/shared/src/utils/jwt.js.map @@ -0,0 +1 @@ +{"version":3,"file":"jwt.js","sourceRoot":"","sources":["jwt.ts"],"names":[],"mappings":";;;;;AAaA,kDAKC;AAED,oDAKC;AAED,kCAEC;AA7BD,gEAA+B;AAS/B,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,yDAAyD,CAAC;AACvG,MAAM,iBAAiB,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,KAAK,CAAC;AACrE,MAAM,kBAAkB,GAAG,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,IAAI,CAAC;AAEtE,SAAgB,mBAAmB,CAAC,OAAmB;IACrD,OAAO,sBAAG,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE;QACnC,SAAS,EAAE,iBAAwB;QACnC,MAAM,EAAE,UAAU;KACnB,CAAC,CAAC;AACL,CAAC;AAED,SAAgB,oBAAoB,CAAC,OAAmB;IACtD,OAAO,sBAAG,CAAC,IAAI,CAAC,EAAE,GAAG,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,UAAU,EAAE;QAC3D,SAAS,EAAE,kBAAyB;QACpC,MAAM,EAAE,UAAU;KACnB,CAAC,CAAC;AACL,CAAC;AAED,SAAgB,WAAW,CAAC,KAAa;IACvC,OAAO,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,CAAe,CAAC;AAC7E,CAAC"} \ No newline at end of file diff --git a/packages/shared/src/utils/jwt.ts b/packages/shared/src/utils/jwt.ts new file mode 100644 index 0000000..8982e60 --- /dev/null +++ b/packages/shared/src/utils/jwt.ts @@ -0,0 +1,30 @@ +import jwt from 'jsonwebtoken'; + +export interface JwtPayload { + userId: string; + email: string; + role: string; + sessionId?: string; +} + +const JWT_SECRET = process.env.JWT_SECRET || 'super_secret_luxe_jwt_signing_key_2026_production_grade'; +const ACCESS_EXPIRATION = process.env.JWT_ACCESS_EXPIRATION || '15m'; +const REFRESH_EXPIRATION = process.env.JWT_REFRESH_EXPIRATION || '7d'; + +export function generateAccessToken(payload: JwtPayload): string { + return jwt.sign(payload, JWT_SECRET, { + expiresIn: ACCESS_EXPIRATION as any, + issuer: 'luxe-api', + }); +} + +export function generateRefreshToken(payload: JwtPayload): string { + return jwt.sign({ ...payload, type: 'refresh' }, JWT_SECRET, { + expiresIn: REFRESH_EXPIRATION as any, + issuer: 'luxe-api', + }); +} + +export function verifyToken(token: string): JwtPayload { + return jwt.verify(token, JWT_SECRET, { issuer: 'luxe-api' }) as JwtPayload; +} diff --git a/packages/shared/src/utils/response.d.ts b/packages/shared/src/utils/response.d.ts new file mode 100644 index 0000000..5b79f51 --- /dev/null +++ b/packages/shared/src/utils/response.d.ts @@ -0,0 +1,22 @@ +export interface ApiResponse { + success: boolean; + message?: string; + data?: T; + pagination?: { + total: number; + page: number; + pageSize: number; + hasMore: boolean; + }; + errors?: Array<{ + field?: string; + message: string; + }>; +} +export declare function successResponse(data: T, message?: string): ApiResponse; +export declare function paginatedResponse(data: T[], total: number, page: number, pageSize: number, message?: string): ApiResponse; +export declare function errorResponse(message?: string, errors?: Array<{ + field?: string; + message: string; +}>): ApiResponse; +//# sourceMappingURL=response.d.ts.map \ No newline at end of file diff --git a/packages/shared/src/utils/response.d.ts.map b/packages/shared/src/utils/response.d.ts.map new file mode 100644 index 0000000..099c0a7 --- /dev/null +++ b/packages/shared/src/utils/response.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"response.d.ts","sourceRoot":"","sources":["response.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW,CAAC,CAAC,GAAG,GAAG;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,CAAC,CAAC;IACT,UAAU,CAAC,EAAE;QACX,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,OAAO,CAAC;KAClB,CAAC;IACF,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrD;AAED,wBAAgB,eAAe,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,SAAoC,GAAG,WAAW,CAAC,CAAC,CAAC,CAMvG;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,IAAI,EAAE,CAAC,EAAE,EACT,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,OAAO,SAAoC,GAC1C,WAAW,CAAC,CAAC,EAAE,CAAC,CAYlB;AAED,wBAAgB,aAAa,CAC3B,OAAO,SAAsB,EAC7B,MAAM,GAAE,KAAK,CAAC;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAM,GACtD,WAAW,CAMb"} \ No newline at end of file diff --git a/packages/shared/src/utils/response.js b/packages/shared/src/utils/response.js new file mode 100644 index 0000000..2b80c20 --- /dev/null +++ b/packages/shared/src/utils/response.js @@ -0,0 +1,33 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.successResponse = successResponse; +exports.paginatedResponse = paginatedResponse; +exports.errorResponse = errorResponse; +function successResponse(data, message = 'Operation executed successfully') { + return { + success: true, + message, + data, + }; +} +function paginatedResponse(data, total, page, pageSize, message = 'Operation executed successfully') { + return { + success: true, + message, + data, + pagination: { + total, + page, + pageSize, + hasMore: page * pageSize < total, + }, + }; +} +function errorResponse(message = 'An error occurred', errors = []) { + return { + success: false, + message, + errors, + }; +} +//# sourceMappingURL=response.js.map \ No newline at end of file diff --git a/packages/shared/src/utils/response.js.map b/packages/shared/src/utils/response.js.map new file mode 100644 index 0000000..538d491 --- /dev/null +++ b/packages/shared/src/utils/response.js.map @@ -0,0 +1 @@ +{"version":3,"file":"response.js","sourceRoot":"","sources":["response.ts"],"names":[],"mappings":";;AAaA,0CAMC;AAED,8CAkBC;AAED,sCASC;AArCD,SAAgB,eAAe,CAAI,IAAO,EAAE,OAAO,GAAG,iCAAiC;IACrF,OAAO;QACL,OAAO,EAAE,IAAI;QACb,OAAO;QACP,IAAI;KACL,CAAC;AACJ,CAAC;AAED,SAAgB,iBAAiB,CAC/B,IAAS,EACT,KAAa,EACb,IAAY,EACZ,QAAgB,EAChB,OAAO,GAAG,iCAAiC;IAE3C,OAAO;QACL,OAAO,EAAE,IAAI;QACb,OAAO;QACP,IAAI;QACJ,UAAU,EAAE;YACV,KAAK;YACL,IAAI;YACJ,QAAQ;YACR,OAAO,EAAE,IAAI,GAAG,QAAQ,GAAG,KAAK;SACjC;KACF,CAAC;AACJ,CAAC;AAED,SAAgB,aAAa,CAC3B,OAAO,GAAG,mBAAmB,EAC7B,SAAqD,EAAE;IAEvD,OAAO;QACL,OAAO,EAAE,KAAK;QACd,OAAO;QACP,MAAM;KACP,CAAC;AACJ,CAAC"} \ No newline at end of file diff --git a/packages/shared/src/utils/response.ts b/packages/shared/src/utils/response.ts new file mode 100644 index 0000000..dd70f57 --- /dev/null +++ b/packages/shared/src/utils/response.ts @@ -0,0 +1,51 @@ +export interface ApiResponse { + success: boolean; + message?: string; + data?: T; + pagination?: { + total: number; + page: number; + pageSize: number; + hasMore: boolean; + }; + errors?: Array<{ field?: string; message: string }>; +} + +export function successResponse(data: T, message = 'Operation executed successfully'): ApiResponse { + return { + success: true, + message, + data, + }; +} + +export function paginatedResponse( + data: T[], + total: number, + page: number, + pageSize: number, + message = 'Operation executed successfully' +): ApiResponse { + return { + success: true, + message, + data, + pagination: { + total, + page, + pageSize, + hasMore: page * pageSize < total, + }, + }; +} + +export function errorResponse( + message = 'An error occurred', + errors: Array<{ field?: string; message: string }> = [] +): ApiResponse { + return { + success: false, + message, + errors, + }; +} diff --git a/packages/shared/src/validators/index.d.ts b/packages/shared/src/validators/index.d.ts new file mode 100644 index 0000000..95e4ffe --- /dev/null +++ b/packages/shared/src/validators/index.d.ts @@ -0,0 +1,91 @@ +import { z } from 'zod'; +export declare const registerSchema: z.ZodObject<{ + email: z.ZodString; + password: z.ZodString; + username: z.ZodString; + role: z.ZodDefault>; + phone: z.ZodOptional; +}, "strip", z.ZodTypeAny, { + role: "USER" | "ADVERTISER" | "AGENCY"; + email: string; + username: string; + password: string; + phone?: string | undefined; +}, { + email: string; + username: string; + password: string; + role?: "USER" | "ADVERTISER" | "AGENCY" | undefined; + phone?: string | undefined; +}>; +export declare const loginSchema: z.ZodObject<{ + email: z.ZodString; + password: z.ZodString; +}, "strip", z.ZodTypeAny, { + email: string; + password: string; +}, { + email: string; + password: string; +}>; +export declare const forgotPasswordSchema: z.ZodObject<{ + email: z.ZodString; +}, "strip", z.ZodTypeAny, { + email: string; +}, { + email: string; +}>; +export declare const resetPasswordSchema: z.ZodObject<{ + email: z.ZodString; + otp: z.ZodString; + newPassword: z.ZodString; +}, "strip", z.ZodTypeAny, { + email: string; + otp: string; + newPassword: string; +}, { + email: string; + otp: string; + newPassword: string; +}>; +export declare const createListingSchema: z.ZodObject<{ + title: z.ZodString; + description: z.ZodString; + categoryId: z.ZodString; + cityId: z.ZodString; + areaId: z.ZodOptional; + price: z.ZodNumber; + currency: z.ZodDefault; + age: z.ZodOptional; + height: z.ZodOptional; + tagline: z.ZodOptional; + languages: z.ZodDefault>; + tags: z.ZodDefault>; +}, "strip", z.ZodTypeAny, { + description: string; + currency: string; + title: string; + categoryId: string; + cityId: string; + price: number; + languages: string[]; + tags: string[]; + areaId?: string | undefined; + age?: number | undefined; + height?: string | undefined; + tagline?: string | undefined; +}, { + description: string; + title: string; + categoryId: string; + cityId: string; + price: number; + currency?: string | undefined; + areaId?: string | undefined; + age?: number | undefined; + height?: string | undefined; + tagline?: string | undefined; + languages?: string[] | undefined; + tags?: string[] | undefined; +}>; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/shared/src/validators/index.d.ts.map b/packages/shared/src/validators/index.d.ts.map new file mode 100644 index 0000000..4d306e8 --- /dev/null +++ b/packages/shared/src/validators/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;EAMzB,CAAC;AAEH,eAAO,MAAM,WAAW;;;;;;;;;EAGtB,CAAC;AAEH,eAAO,MAAM,oBAAoB;;;;;;EAE/B,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;EAI9B,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAa9B,CAAC"} \ No newline at end of file diff --git a/packages/shared/src/validators/index.js b/packages/shared/src/validators/index.js new file mode 100644 index 0000000..f858cf8 --- /dev/null +++ b/packages/shared/src/validators/index.js @@ -0,0 +1,38 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createListingSchema = exports.resetPasswordSchema = exports.forgotPasswordSchema = exports.loginSchema = exports.registerSchema = void 0; +const zod_1 = require("zod"); +exports.registerSchema = zod_1.z.object({ + email: zod_1.z.string().email(), + password: zod_1.z.string().min(8, 'Password must be at least 8 characters'), + username: zod_1.z.string().min(3).max(30), + role: zod_1.z.enum(['USER', 'ADVERTISER', 'AGENCY']).default('USER'), + phone: zod_1.z.string().optional(), +}); +exports.loginSchema = zod_1.z.object({ + email: zod_1.z.string().email(), + password: zod_1.z.string().min(1, 'Password is required'), +}); +exports.forgotPasswordSchema = zod_1.z.object({ + email: zod_1.z.string().email(), +}); +exports.resetPasswordSchema = zod_1.z.object({ + email: zod_1.z.string().email(), + otp: zod_1.z.string().length(6), + newPassword: zod_1.z.string().min(8), +}); +exports.createListingSchema = zod_1.z.object({ + title: zod_1.z.string().min(5).max(100), + description: zod_1.z.string().min(20), + categoryId: zod_1.z.string().uuid(), + cityId: zod_1.z.string().uuid(), + areaId: zod_1.z.string().uuid().optional(), + price: zod_1.z.number().positive(), + currency: zod_1.z.string().default('USD'), + age: zod_1.z.number().int().min(18).max(99).optional(), + height: zod_1.z.string().optional(), + tagline: zod_1.z.string().max(150).optional(), + languages: zod_1.z.array(zod_1.z.string()).default([]), + tags: zod_1.z.array(zod_1.z.string()).default([]), +}); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/packages/shared/src/validators/index.js.map b/packages/shared/src/validators/index.js.map new file mode 100644 index 0000000..d7db93c --- /dev/null +++ b/packages/shared/src/validators/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AAEX,QAAA,cAAc,GAAG,OAAC,CAAC,MAAM,CAAC;IACrC,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;IACzB,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,wCAAwC,CAAC;IACrE,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;IACnC,IAAI,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;IAC9D,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC7B,CAAC,CAAC;AAEU,QAAA,WAAW,GAAG,OAAC,CAAC,MAAM,CAAC;IAClC,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;IACzB,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,sBAAsB,CAAC;CACpD,CAAC,CAAC;AAEU,QAAA,oBAAoB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC3C,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;CAC1B,CAAC,CAAC;AAEU,QAAA,mBAAmB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC1C,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;IACzB,GAAG,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IACzB,WAAW,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAC/B,CAAC,CAAC;AAEU,QAAA,mBAAmB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC1C,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IACjC,WAAW,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;IAC/B,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;IAC7B,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;IACzB,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;IACpC,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IACnC,GAAG,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE;IAChD,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC7B,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;IACvC,SAAS,EAAE,OAAC,CAAC,KAAK,CAAC,OAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC1C,IAAI,EAAE,OAAC,CAAC,KAAK,CAAC,OAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;CACtC,CAAC,CAAC"} \ No newline at end of file diff --git a/packages/shared/src/validators/index.ts b/packages/shared/src/validators/index.ts new file mode 100644 index 0000000..b5b2803 --- /dev/null +++ b/packages/shared/src/validators/index.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; + +export const registerSchema = z.object({ + email: z.string().email(), + password: z.string().min(8, 'Password must be at least 8 characters'), + username: z.string().min(3).max(30), + role: z.enum(['USER', 'ADVERTISER', 'AGENCY']).default('USER'), + phone: z.string().optional(), +}); + +export const loginSchema = z.object({ + email: z.string().email(), + password: z.string().min(1, 'Password is required'), +}); + +export const forgotPasswordSchema = z.object({ + email: z.string().email(), +}); + +export const resetPasswordSchema = z.object({ + email: z.string().email(), + otp: z.string().length(6), + newPassword: z.string().min(8), +}); + +export const createListingSchema = z.object({ + title: z.string().min(5).max(100), + description: z.string().min(20), + categoryId: z.string().uuid(), + cityId: z.string().uuid(), + areaId: z.string().uuid().optional(), + price: z.number().positive(), + currency: z.string().default('USD'), + age: z.number().int().min(18).max(99).optional(), + height: z.string().optional(), + tagline: z.string().max(150).optional(), + languages: z.array(z.string()).default([]), + tags: z.array(z.string()).default([]), +}); diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000..4b2e1f3 --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src/**/*"] +} diff --git a/scripts/dev.mjs b/scripts/dev.mjs new file mode 100644 index 0000000..253ab59 --- /dev/null +++ b/scripts/dev.mjs @@ -0,0 +1,21 @@ +import { spawn } from 'node:child_process'; +import path from 'node:path'; + +console.log('🚀 Starting Luxe Monorepo Development Environment...'); + +const services = [ + { name: 'Gateway', command: 'npx', args: ['tsx', 'watch', 'services/gateway/src/index.ts'] }, + { name: 'Notifications', command: 'npx', args: ['tsx', 'watch', 'services/notification-service/src/index.ts'] }, +]; + +services.forEach((service) => { + const proc = spawn(service.command, service.args, { + stdio: 'inherit', + shell: true, + env: { ...process.env }, + }); + + proc.on('close', (code) => { + console.log(`[${service.name}] process exited with code ${code}`); + }); +}); diff --git a/services/gateway/package.json b/services/gateway/package.json new file mode 100644 index 0000000..243362c --- /dev/null +++ b/services/gateway/package.json @@ -0,0 +1,34 @@ +{ + "name": "@luxe/gateway", + "version": "1.0.0", + "private": true, + "type": "module", + "main": "dist/index.js", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js" + }, + "dependencies": { + "@fastify/cookie": "^10.0.1", + "@fastify/cors": "^10.0.1", + "@fastify/formbody": "^8.0.0", + "@fastify/helmet": "^12.0.1", + "@fastify/multipart": "^9.0.2", + "@fastify/rate-limit": "^10.2.1", + "@fastify/swagger": "^9.4.0", + "@fastify/swagger-ui": "^5.2.0", + "@luxe/database": "*", + "@luxe/shared": "*", + "bullmq": "^5.34.8", + "fastify": "^5.2.0", + "ioredis": "^5.4.2", + "socket.io": "^4.8.1", + "zod": "^3.24.1" + }, + "devDependencies": { + "@types/node": "^22.10.1", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + } +} diff --git a/services/gateway/src/index.d.ts b/services/gateway/src/index.d.ts new file mode 100644 index 0000000..e26a57a --- /dev/null +++ b/services/gateway/src/index.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/services/gateway/src/index.d.ts.map b/services/gateway/src/index.d.ts.map new file mode 100644 index 0000000..82335e7 --- /dev/null +++ b/services/gateway/src/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/services/gateway/src/index.js b/services/gateway/src/index.js new file mode 100644 index 0000000..6e7afd1 --- /dev/null +++ b/services/gateway/src/index.js @@ -0,0 +1,106 @@ +import Fastify from 'fastify'; +import cors from '@fastify/cors'; +import helmet from '@fastify/helmet'; +import rateLimit from '@fastify/rate-limit'; +import cookie from '@fastify/cookie'; +import swagger from '@fastify/swagger'; +import swaggerUi from '@fastify/swagger-ui'; +import { Server } from 'socket.io'; +import { createServer } from 'node:http'; +import { authRoutes } from './routes/auth.routes.js'; +import { userRoutes } from './routes/user.routes.js'; +import { clientRoutes } from './routes/client.routes.js'; +import { listingRoutes } from './routes/listing.routes.js'; +import { walletRoutes } from './routes/wallet.routes.js'; +import { blogRoutes } from './routes/blog.routes.js'; +import { adminRoutes } from './routes/admin.routes.js'; +import { mediaRoutes } from './routes/media.routes.js'; +const PORT = Number(process.env.PORT) || 3000; +const HOST = process.env.HOST || '0.0.0.0'; +async function bootstrap() { + const app = Fastify({ + logger: { + level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', + }, + }); + // 1. Security & Core Middleware + await app.register(helmet, { contentSecurityPolicy: false }); + await app.register(cors, { + origin: true, + credentials: true, + }); + await app.register(rateLimit, { + max: 100, + timeWindow: '15 minutes', + }); + await app.register(cookie, { + secret: process.env.JWT_SECRET || 'luxe_cookie_secret_key_2026', + }); + // 2. Swagger Documentation Setup + await app.register(swagger, { + openapi: { + info: { + title: 'Luxe Platform REST API Specification', + description: 'Production-ready microservices API specification for Luxe classified marketplace.', + version: '1.0.0', + }, + servers: [{ url: `http://localhost:${PORT}` }], + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + }, + }, + }, + }); + await app.register(swaggerUi, { + routePrefix: '/documentation', + uiConfig: { + docExpansion: 'list', + deepLinking: false, + }, + }); + // 3. Health Check Endpoints + app.get('/health', async () => ({ status: 'UP', timestamp: new Date().toISOString() })); + app.get('/ready', async () => ({ status: 'READY', service: 'luxe-gateway' })); + // 4. API Route Modules Registration + await app.register(authRoutes, { prefix: '/api/v1/auth' }); + await app.register(userRoutes, { prefix: '/api/v1/users' }); + await app.register(clientRoutes, { prefix: '/api/v1/clients' }); + await app.register(listingRoutes, { prefix: '/api/v1' }); + await app.register(walletRoutes, { prefix: '/api/v1/wallet' }); + await app.register(blogRoutes, { prefix: '/api/v1' }); + await app.register(adminRoutes, { prefix: '/api/v1/admin' }); + await app.register(mediaRoutes, { prefix: '/api/v1/media' }); + // 5. Initialize Server & WebSockets (Socket.IO) + const httpServer = createServer(app.server); + const io = new Server(httpServer, { + cors: { origin: '*' }, + }); + io.on('connection', (socket) => { + app.log.info(`Socket connected: ${socket.id}`); + socket.on('join_room', (room) => { + socket.join(room); + }); + socket.on('send_message', (data) => { + io.to(data.roomId).emit('new_message', data); + }); + socket.on('disconnect', () => { + app.log.info(`Socket disconnected: ${socket.id}`); + }); + }); + try { + await app.listen({ port: PORT, host: HOST }); + app.log.info(`🚀 Luxe API Gateway server running on http://${HOST}:${PORT}`); + app.log.info(`📖 Swagger OpenAPI documentation available at http://localhost:${PORT}/documentation`); + } + catch (err) { + app.log.error(err); + process.exit(1); + } +} +bootstrap(); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/services/gateway/src/index.js.map b/services/gateway/src/index.js.map new file mode 100644 index 0000000..ecf6b4d --- /dev/null +++ b/services/gateway/src/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/services/gateway/src/index.ts b/services/gateway/src/index.ts new file mode 100644 index 0000000..f5893ea --- /dev/null +++ b/services/gateway/src/index.ts @@ -0,0 +1,119 @@ +import Fastify from 'fastify'; +import cors from '@fastify/cors'; +import helmet from '@fastify/helmet'; +import rateLimit from '@fastify/rate-limit'; +import cookie from '@fastify/cookie'; +import swagger from '@fastify/swagger'; +import swaggerUi from '@fastify/swagger-ui'; +import { Server } from 'socket.io'; +import { createServer } from 'node:http'; + +import { authRoutes } from './routes/auth.routes.js'; +import { userRoutes } from './routes/user.routes.js'; +import { clientRoutes } from './routes/client.routes.js'; +import { listingRoutes } from './routes/listing.routes.js'; +import { walletRoutes } from './routes/wallet.routes.js'; +import { blogRoutes } from './routes/blog.routes.js'; +import { adminRoutes } from './routes/admin.routes.js'; +import { mediaRoutes } from './routes/media.routes.js'; + +const PORT = Number(process.env.PORT) || 3000; +const HOST = process.env.HOST || '0.0.0.0'; + +async function bootstrap() { + const app = Fastify({ + logger: { + level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', + }, + }); + + // 1. Security & Core Middleware + await app.register(helmet, { contentSecurityPolicy: false }); + await app.register(cors, { + origin: true, + credentials: true, + }); + await app.register(rateLimit, { + max: 100, + timeWindow: '15 minutes', + }); + await app.register(cookie, { + secret: process.env.JWT_SECRET || 'luxe_cookie_secret_key_2026', + }); + + // 2. Swagger Documentation Setup + await app.register(swagger, { + openapi: { + info: { + title: 'Luxe Platform REST API Specification', + description: 'Production-ready microservices API specification for Luxe classified marketplace.', + version: '1.0.0', + }, + servers: [{ url: `http://localhost:${PORT}` }], + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + }, + }, + }, + }); + + await app.register(swaggerUi, { + routePrefix: '/documentation', + uiConfig: { + docExpansion: 'list', + deepLinking: false, + }, + }); + + // 3. Health Check Endpoints + app.get('/health', async () => ({ status: 'UP', timestamp: new Date().toISOString() })); + app.get('/ready', async () => ({ status: 'READY', service: 'luxe-gateway' })); + + // 4. API Route Modules Registration + await app.register(authRoutes, { prefix: '/api/v1/auth' }); + await app.register(userRoutes, { prefix: '/api/v1/users' }); + await app.register(clientRoutes, { prefix: '/api/v1/clients' }); + await app.register(listingRoutes, { prefix: '/api/v1' }); + await app.register(walletRoutes, { prefix: '/api/v1/wallet' }); + await app.register(blogRoutes, { prefix: '/api/v1' }); + await app.register(adminRoutes, { prefix: '/api/v1/admin' }); + await app.register(mediaRoutes, { prefix: '/api/v1/media' }); + + // 5. Initialize Server & WebSockets (Socket.IO) + const httpServer = createServer(app.server); + const io = new Server(httpServer, { + cors: { origin: '*' }, + }); + + io.on('connection', (socket) => { + app.log.info(`Socket connected: ${socket.id}`); + + socket.on('join_room', (room) => { + socket.join(room); + }); + + socket.on('send_message', (data) => { + io.to(data.roomId).emit('new_message', data); + }); + + socket.on('disconnect', () => { + app.log.info(`Socket disconnected: ${socket.id}`); + }); + }); + + try { + await app.listen({ port: PORT, host: HOST }); + app.log.info(`🚀 Luxe API Gateway server running on http://${HOST}:${PORT}`); + app.log.info(`📖 Swagger OpenAPI documentation available at http://localhost:${PORT}/documentation`); + } catch (err) { + app.log.error(err); + process.exit(1); + } +} + +bootstrap(); diff --git a/services/gateway/src/routes/admin.routes.d.ts b/services/gateway/src/routes/admin.routes.d.ts new file mode 100644 index 0000000..3028a7f --- /dev/null +++ b/services/gateway/src/routes/admin.routes.d.ts @@ -0,0 +1,3 @@ +import { FastifyInstance } from 'fastify'; +export declare function adminRoutes(fastify: FastifyInstance): Promise; +//# sourceMappingURL=admin.routes.d.ts.map \ No newline at end of file diff --git a/services/gateway/src/routes/admin.routes.d.ts.map b/services/gateway/src/routes/admin.routes.d.ts.map new file mode 100644 index 0000000..9353c95 --- /dev/null +++ b/services/gateway/src/routes/admin.routes.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"admin.routes.d.ts","sourceRoot":"","sources":["admin.routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAyBxE,wBAAsB,WAAW,CAAC,OAAO,EAAE,eAAe,iBA6JzD"} \ No newline at end of file diff --git a/services/gateway/src/routes/admin.routes.js b/services/gateway/src/routes/admin.routes.js new file mode 100644 index 0000000..d6b3b07 --- /dev/null +++ b/services/gateway/src/routes/admin.routes.js @@ -0,0 +1,165 @@ +import { prisma } from '@luxe/database'; +import { verifyToken, successResponse, paginatedResponse, errorResponse } from '@luxe/shared'; +async function authenticateAdmin(request, reply) { + const authHeader = request.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + reply.status(401).send(errorResponse('Admin Bearer token required')); + return null; + } + try { + const token = authHeader.split(' ')[1]; + const decoded = verifyToken(token); + if (!['ADMIN', 'SUPER_ADMIN', 'MODERATOR', 'FINANCE', 'SUPPORT'].includes(decoded.role)) { + reply.status(403).send(errorResponse('Forbidden: Admin privilege required')); + return null; + } + return decoded; + } + catch (err) { + reply.status(401).send(errorResponse('Invalid or expired token')); + return null; + } +} +export async function adminRoutes(fastify) { + // GET /api/v1/admin/stats/overview + fastify.get('/stats/overview', async (request, reply) => { + const admin = await authenticateAdmin(request, reply); + if (!admin) + return; + const [totalUsers, activeListings, pendingVerifications, totalRevenue] = await Promise.all([ + prisma.user.count(), + prisma.listing.count({ where: { status: 'ACTIVE' } }), + prisma.verificationRequest.count({ where: { status: 'PENDING' } }), + prisma.transaction.aggregate({ + where: { status: 'SUCCESS' }, + _sum: { amount: true }, + }), + ]); + return reply.send(successResponse({ + totalUsers, + activeListings, + pendingVerifications, + totalRevenue: totalRevenue._sum.amount || 0.0, + })); + }); + // GET /api/v1/admin/users + fastify.get('/users', async (request, reply) => { + const admin = await authenticateAdmin(request, reply); + if (!admin) + return; + const { page = '1', pageSize = '20', role, status, search } = request.query; + const p = parseInt(page); + const ps = parseInt(pageSize); + const where = {}; + if (role) + where.role = role; + if (status) + where.status = status; + if (search) { + where.OR = [ + { email: { contains: search, mode: 'insensitive' } }, + { username: { contains: search, mode: 'insensitive' } }, + ]; + } + const [users, total] = await Promise.all([ + prisma.user.findMany({ + where, + select: { + id: true, + email: true, + username: true, + role: true, + status: true, + createdAt: true, + lastLoginAt: true, + }, + orderBy: { createdAt: 'desc' }, + skip: (p - 1) * ps, + take: ps, + }), + prisma.user.count({ where }), + ]); + return reply.send(paginatedResponse(users, total, p, ps)); + }); + // PATCH /api/v1/admin/users/:id/status + fastify.patch('/users/:id/status', async (request, reply) => { + const admin = await authenticateAdmin(request, reply); + if (!admin) + return; + const { id } = request.params; + const { status } = request.body; // ACTIVE, SUSPENDED, BANNED + const updatedUser = await prisma.user.update({ + where: { id }, + data: { status }, + }); + // Create Audit Log entry + await prisma.auditLog.create({ + data: { + userId: admin.userId, + action: 'USER_STATUS_CHANGE', + resource: 'User', + resourceId: id, + metadata: { newStatus: status }, + }, + }); + return reply.send(successResponse(updatedUser, `User status updated to ${status}`)); + }); + // GET /api/v1/admin/verifications + fastify.get('/verifications', async (request, reply) => { + const admin = await authenticateAdmin(request, reply); + if (!admin) + return; + const verifications = await prisma.verificationRequest.findMany({ + where: { status: 'PENDING' }, + include: { + profile: { + include: { user: { select: { id: true, email: true, username: true } } }, + }, + }, + orderBy: { createdAt: 'asc' }, + }); + return reply.send(successResponse(verifications)); + }); + // POST /api/v1/admin/verifications/:id/approve + fastify.post('/verifications/:id/approve', async (request, reply) => { + const admin = await authenticateAdmin(request, reply); + if (!admin) + return; + const { id } = request.params; + const verification = await prisma.verificationRequest.findUnique({ where: { id } }); + if (!verification) + return reply.status(404).send(errorResponse('Verification request not found')); + await prisma.$transaction([ + prisma.verificationRequest.update({ + where: { id }, + data: { status: 'VERIFIED' }, + }), + prisma.advertiserProfile.update({ + where: { id: verification.profileId }, + data: { verificationStatus: 'VERIFIED' }, + }), + prisma.auditLog.create({ + data: { + userId: admin.userId, + action: 'VERIFICATION_APPROVED', + resource: 'VerificationRequest', + resourceId: id, + }, + }), + ]); + return reply.send(successResponse(null, 'Verification request approved')); + }); + // GET /api/v1/admin/audit-logs + fastify.get('/audit-logs', async (request, reply) => { + const admin = await authenticateAdmin(request, reply); + if (!admin) + return; + const logs = await prisma.auditLog.findMany({ + take: 50, + orderBy: { createdAt: 'desc' }, + include: { user: { select: { username: true, role: true } } }, + }); + return reply.send(successResponse(logs)); + }); +} +//# sourceMappingURL=admin.routes.js.map \ No newline at end of file diff --git a/services/gateway/src/routes/admin.routes.js.map b/services/gateway/src/routes/admin.routes.js.map new file mode 100644 index 0000000..97061fa --- /dev/null +++ b/services/gateway/src/routes/admin.routes.js.map @@ -0,0 +1 @@ +{"version":3,"file":"admin.routes.js","sourceRoot":"","sources":["admin.routes.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/services/gateway/src/routes/admin.routes.ts b/services/gateway/src/routes/admin.routes.ts new file mode 100644 index 0000000..587c555 --- /dev/null +++ b/services/gateway/src/routes/admin.routes.ts @@ -0,0 +1,183 @@ +import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { prisma } from '@luxe/database'; +import { verifyToken, successResponse, paginatedResponse, errorResponse, authorize } from '@luxe/shared'; + +async function authenticateAdmin(request: FastifyRequest, reply: FastifyReply) { + const authHeader = request.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + reply.status(401).send(errorResponse('Admin Bearer token required')); + return null; + } + + try { + const token = authHeader.split(' ')[1]; + const decoded = verifyToken(token); + if (!['ADMIN', 'SUPER_ADMIN', 'MODERATOR', 'FINANCE', 'SUPPORT'].includes(decoded.role)) { + reply.status(403).send(errorResponse('Forbidden: Admin privilege required')); + return null; + } + return decoded; + } catch (err) { + reply.status(401).send(errorResponse('Invalid or expired token')); + return null; + } +} + +export async function adminRoutes(fastify: FastifyInstance) { + // GET /api/v1/admin/stats/overview + fastify.get('/stats/overview', async (request: FastifyRequest, reply: FastifyReply) => { + const admin = await authenticateAdmin(request, reply); + if (!admin) return; + + const [totalUsers, activeListings, pendingVerifications, totalRevenue] = await Promise.all([ + prisma.user.count(), + prisma.listing.count({ where: { status: 'ACTIVE' } }), + prisma.verificationRequest.count({ where: { status: 'PENDING' } }), + prisma.transaction.aggregate({ + where: { status: 'SUCCESS' }, + _sum: { amount: true }, + }), + ]); + + return reply.send( + successResponse({ + totalUsers, + activeListings, + pendingVerifications, + totalRevenue: totalRevenue._sum.amount || 0.0, + }) + ); + }); + + // GET /api/v1/admin/users + fastify.get('/users', async (request: FastifyRequest, reply: FastifyReply) => { + const admin = await authenticateAdmin(request, reply); + if (!admin) return; + + const { page = '1', pageSize = '20', role, status, search } = request.query as any; + const p = parseInt(page); + const ps = parseInt(pageSize); + + const where: any = {}; + if (role) where.role = role; + if (status) where.status = status; + if (search) { + where.OR = [ + { email: { contains: search, mode: 'insensitive' } }, + { username: { contains: search, mode: 'insensitive' } }, + ]; + } + + const [users, total] = await Promise.all([ + prisma.user.findMany({ + where, + select: { + id: true, + email: true, + username: true, + role: true, + status: true, + createdAt: true, + lastLoginAt: true, + }, + orderBy: { createdAt: 'desc' }, + skip: (p - 1) * ps, + take: ps, + }), + prisma.user.count({ where }), + ]); + + return reply.send(paginatedResponse(users, total, p, ps)); + }); + + // PATCH /api/v1/admin/users/:id/status + fastify.patch('/users/:id/status', async (request: FastifyRequest, reply: FastifyReply) => { + const admin = await authenticateAdmin(request, reply); + if (!admin) return; + + const { id } = request.params as any; + const { status } = request.body as any; // ACTIVE, SUSPENDED, BANNED + + const updatedUser = await prisma.user.update({ + where: { id }, + data: { status }, + }); + + // Create Audit Log entry + await prisma.auditLog.create({ + data: { + userId: admin.userId, + action: 'USER_STATUS_CHANGE', + resource: 'User', + resourceId: id, + metadata: { newStatus: status }, + }, + }); + + return reply.send(successResponse(updatedUser, `User status updated to ${status}`)); + }); + + // GET /api/v1/admin/verifications + fastify.get('/verifications', async (request: FastifyRequest, reply: FastifyReply) => { + const admin = await authenticateAdmin(request, reply); + if (!admin) return; + + const verifications = await prisma.verificationRequest.findMany({ + where: { status: 'PENDING' }, + include: { + profile: { + include: { user: { select: { id: true, email: true, username: true } } }, + }, + }, + orderBy: { createdAt: 'asc' }, + }); + + return reply.send(successResponse(verifications)); + }); + + // POST /api/v1/admin/verifications/:id/approve + fastify.post('/verifications/:id/approve', async (request: FastifyRequest, reply: FastifyReply) => { + const admin = await authenticateAdmin(request, reply); + if (!admin) return; + + const { id } = request.params as any; + + const verification = await prisma.verificationRequest.findUnique({ where: { id } }); + if (!verification) return reply.status(404).send(errorResponse('Verification request not found')); + + await prisma.$transaction([ + prisma.verificationRequest.update({ + where: { id }, + data: { status: 'VERIFIED' }, + }), + prisma.advertiserProfile.update({ + where: { id: verification.profileId }, + data: { verificationStatus: 'VERIFIED' }, + }), + prisma.auditLog.create({ + data: { + userId: admin.userId, + action: 'VERIFICATION_APPROVED', + resource: 'VerificationRequest', + resourceId: id, + }, + }), + ]); + + return reply.send(successResponse(null, 'Verification request approved')); + }); + + // GET /api/v1/admin/audit-logs + fastify.get('/audit-logs', async (request: FastifyRequest, reply: FastifyReply) => { + const admin = await authenticateAdmin(request, reply); + if (!admin) return; + + const logs = await prisma.auditLog.findMany({ + take: 50, + orderBy: { createdAt: 'desc' }, + include: { user: { select: { username: true, role: true } } }, + }); + + return reply.send(successResponse(logs)); + }); +} diff --git a/services/gateway/src/routes/auth.routes.d.ts b/services/gateway/src/routes/auth.routes.d.ts new file mode 100644 index 0000000..6d6a481 --- /dev/null +++ b/services/gateway/src/routes/auth.routes.d.ts @@ -0,0 +1,3 @@ +import { FastifyInstance } from 'fastify'; +export declare function authRoutes(fastify: FastifyInstance): Promise; +//# sourceMappingURL=auth.routes.d.ts.map \ No newline at end of file diff --git a/services/gateway/src/routes/auth.routes.d.ts.map b/services/gateway/src/routes/auth.routes.d.ts.map new file mode 100644 index 0000000..6b6b3e1 --- /dev/null +++ b/services/gateway/src/routes/auth.routes.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"auth.routes.d.ts","sourceRoot":"","sources":["auth.routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAgBxE,wBAAsB,UAAU,CAAC,OAAO,EAAE,eAAe,iBAuOxD"} \ No newline at end of file diff --git a/services/gateway/src/routes/auth.routes.js b/services/gateway/src/routes/auth.routes.js new file mode 100644 index 0000000..9e5cac4 --- /dev/null +++ b/services/gateway/src/routes/auth.routes.js @@ -0,0 +1,195 @@ +import { prisma } from '@luxe/database'; +import { hashPassword, verifyPassword, generateAccessToken, generateRefreshToken, verifyToken, successResponse, errorResponse, registerSchema, loginSchema, } from '@luxe/shared'; +export async function authRoutes(fastify) { + // 1. POST /api/v1/auth/register + fastify.post('/register', async (request, reply) => { + const parseResult = registerSchema.safeParse(request.body); + if (!parseResult.success) { + return reply.status(400).send(errorResponse('Validation error', parseResult.error.errors.map(e => ({ field: e.path.join('.'), message: e.message })))); + } + const { email, password, username, role, phone } = parseResult.data; + const existingUser = await prisma.user.findFirst({ + where: { OR: [{ email }, { username }] }, + }); + if (existingUser) { + return reply.status(400).send(errorResponse('User with email or username already exists')); + } + const passwordHash = await hashPassword(password); + const user = await prisma.user.create({ + data: { + email, + username, + passwordHash, + role: role, + phone, + }, + }); + // Create Wallet for new user + await prisma.wallet.create({ + data: { + userId: user.id, + balance: 0.0, + credits: 10.0, // Complimentary bonus credits + }, + }); + // If Advertiser role, create AdvertiserProfile shell + if (role === 'ADVERTISER' || role === 'AGENCY') { + await prisma.advertiserProfile.create({ + data: { + userId: user.id, + type: role, + }, + }); + } + const tokenPayload = { userId: user.id, email: user.email, role: user.role }; + const accessToken = generateAccessToken(tokenPayload); + const refreshToken = generateRefreshToken(tokenPayload); + // Save Session in DB + await prisma.session.create({ + data: { + userId: user.id, + refreshToken, + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + }, + }); + reply.setCookie('refreshToken', refreshToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + path: '/api/v1/auth', + }); + return reply.status(201).send(successResponse({ + accessToken, + user: { + id: user.id, + email: user.email, + username: user.username, + role: user.role, + }, + }, 'User registered successfully')); + }); + // 2. POST /api/v1/auth/login + fastify.post('/login', async (request, reply) => { + const parseResult = loginSchema.safeParse(request.body); + if (!parseResult.success) { + return reply.status(400).send(errorResponse('Invalid payload')); + } + const { email, password } = parseResult.data; + const user = await prisma.user.findUnique({ where: { email } }); + if (!user) { + return reply.status(401).send(errorResponse('Invalid email or password')); + } + if (user.status === 'BANNED' || user.status === 'SUSPENDED') { + return reply.status(403).send(errorResponse(`Account is ${user.status.toLowerCase()}`)); + } + const isValid = await verifyPassword(user.passwordHash, password); + if (!isValid) { + await prisma.user.update({ + where: { id: user.id }, + data: { failedLoginCount: { increment: 1 } }, + }); + return reply.status(401).send(errorResponse('Invalid email or password')); + } + // Reset failed attempts & update last login + await prisma.user.update({ + where: { id: user.id }, + data: { failedLoginCount: 0, lastLoginAt: new Date() }, + }); + const tokenPayload = { userId: user.id, email: user.email, role: user.role }; + const accessToken = generateAccessToken(tokenPayload); + const refreshToken = generateRefreshToken(tokenPayload); + await prisma.session.create({ + data: { + userId: user.id, + refreshToken, + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + }, + }); + reply.setCookie('refreshToken', refreshToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + path: '/api/v1/auth', + }); + return reply.send(successResponse({ + accessToken, + user: { + id: user.id, + email: user.email, + username: user.username, + role: user.role, + avatar: user.avatar, + }, + }, 'Logged in successfully')); + }); + // 3. POST /api/v1/auth/refresh-token + fastify.post('/refresh-token', async (request, reply) => { + const refreshToken = request.cookies.refreshToken; + if (!refreshToken) { + return reply.status(401).send(errorResponse('Refresh token required')); + } + try { + const decoded = verifyToken(refreshToken); + const session = await prisma.session.findUnique({ where: { refreshToken } }); + if (!session || session.isRevoked || session.expiresAt < new Date()) { + return reply.status(401).send(errorResponse('Invalid or expired session')); + } + const newAccessToken = generateAccessToken({ + userId: decoded.userId, + email: decoded.email, + role: decoded.role, + }); + return reply.send(successResponse({ accessToken: newAccessToken })); + } + catch (err) { + return reply.status(401).send(errorResponse('Invalid refresh token')); + } + }); + // 4. POST /api/v1/auth/logout + fastify.post('/logout', async (request, reply) => { + const refreshToken = request.cookies.refreshToken; + if (refreshToken) { + await prisma.session.updateMany({ + where: { refreshToken }, + data: { isRevoked: true }, + }); + } + reply.clearCookie('refreshToken', { path: '/api/v1/auth' }); + return reply.send(successResponse(null, 'Logged out successfully')); + }); + // 5. GET /api/v1/auth/me + fastify.get('/me', async (request, reply) => { + const authHeader = request.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return reply.status(401).send(errorResponse('Bearer token required')); + } + try { + const token = authHeader.split(' ')[1]; + const decoded = verifyToken(token); + const user = await prisma.user.findUnique({ + where: { id: decoded.userId }, + select: { + id: true, + email: true, + username: true, + role: true, + avatar: true, + bio: true, + language: true, + currency: true, + emailVerifiedAt: true, + phoneVerifiedAt: true, + wallet: { select: { balance: true, credits: true } }, + }, + }); + if (!user) { + return reply.status(404).send(errorResponse('User not found')); + } + return reply.send(successResponse(user)); + } + catch (err) { + return reply.status(401).send(errorResponse('Invalid or expired token')); + } + }); +} +//# sourceMappingURL=auth.routes.js.map \ No newline at end of file diff --git a/services/gateway/src/routes/auth.routes.js.map b/services/gateway/src/routes/auth.routes.js.map new file mode 100644 index 0000000..683242e --- /dev/null +++ b/services/gateway/src/routes/auth.routes.js.map @@ -0,0 +1 @@ +{"version":3,"file":"auth.routes.js","sourceRoot":"","sources":["auth.routes.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/services/gateway/src/routes/auth.routes.ts b/services/gateway/src/routes/auth.routes.ts new file mode 100644 index 0000000..b2b5be0 --- /dev/null +++ b/services/gateway/src/routes/auth.routes.ts @@ -0,0 +1,248 @@ +import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { prisma, RoleType } from '@luxe/database'; +import { + hashPassword, + verifyPassword, + generateAccessToken, + generateRefreshToken, + verifyToken, + successResponse, + errorResponse, + registerSchema, + loginSchema, + forgotPasswordSchema, + resetPasswordSchema, +} from '@luxe/shared'; + +export async function authRoutes(fastify: FastifyInstance) { + // 1. POST /api/v1/auth/register + fastify.post('/register', async (request: FastifyRequest, reply: FastifyReply) => { + const parseResult = registerSchema.safeParse(request.body); + if (!parseResult.success) { + return reply.status(400).send(errorResponse('Validation error', parseResult.error.errors.map(e => ({ field: e.path.join('.'), message: e.message })))); + } + + const { email, password, username, role, phone } = parseResult.data; + + const existingUser = await prisma.user.findFirst({ + where: { OR: [{ email }, { username }] }, + }); + + if (existingUser) { + return reply.status(400).send(errorResponse('User with email or username already exists')); + } + + const passwordHash = await hashPassword(password); + + const user = await prisma.user.create({ + data: { + email, + username, + passwordHash, + role: role as RoleType, + phone, + }, + }); + + // Create Wallet for new user + await prisma.wallet.create({ + data: { + userId: user.id, + balance: 0.0, + credits: 10.0, // Complimentary bonus credits + }, + }); + + // If Advertiser role, create AdvertiserProfile shell + if (role === 'ADVERTISER' || role === 'AGENCY') { + await prisma.advertiserProfile.create({ + data: { + userId: user.id, + type: role, + }, + }); + } + + const tokenPayload = { userId: user.id, email: user.email, role: user.role }; + const accessToken = generateAccessToken(tokenPayload); + const refreshToken = generateRefreshToken(tokenPayload); + + // Save Session in DB + await prisma.session.create({ + data: { + userId: user.id, + refreshToken, + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + }, + }); + + reply.setCookie('refreshToken', refreshToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + path: '/api/v1/auth', + }); + + return reply.status(201).send( + successResponse( + { + accessToken, + user: { + id: user.id, + email: user.email, + username: user.username, + role: user.role, + }, + }, + 'User registered successfully' + ) + ); + }); + + // 2. POST /api/v1/auth/login + fastify.post('/login', async (request: FastifyRequest, reply: FastifyReply) => { + const parseResult = loginSchema.safeParse(request.body); + if (!parseResult.success) { + return reply.status(400).send(errorResponse('Invalid payload')); + } + + const { email, password } = parseResult.data; + + const user = await prisma.user.findUnique({ where: { email } }); + if (!user) { + return reply.status(401).send(errorResponse('Invalid email or password')); + } + + if (user.status === 'BANNED' || user.status === 'SUSPENDED') { + return reply.status(403).send(errorResponse(`Account is ${user.status.toLowerCase()}`)); + } + + const isValid = await verifyPassword(user.passwordHash, password); + if (!isValid) { + await prisma.user.update({ + where: { id: user.id }, + data: { failedLoginCount: { increment: 1 } }, + }); + return reply.status(401).send(errorResponse('Invalid email or password')); + } + + // Reset failed attempts & update last login + await prisma.user.update({ + where: { id: user.id }, + data: { failedLoginCount: 0, lastLoginAt: new Date() }, + }); + + const tokenPayload = { userId: user.id, email: user.email, role: user.role }; + const accessToken = generateAccessToken(tokenPayload); + const refreshToken = generateRefreshToken(tokenPayload); + + await prisma.session.create({ + data: { + userId: user.id, + refreshToken, + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + }, + }); + + reply.setCookie('refreshToken', refreshToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + path: '/api/v1/auth', + }); + + return reply.send( + successResponse( + { + accessToken, + user: { + id: user.id, + email: user.email, + username: user.username, + role: user.role, + avatar: user.avatar, + }, + }, + 'Logged in successfully' + ) + ); + }); + + // 3. POST /api/v1/auth/refresh-token + fastify.post('/refresh-token', async (request: FastifyRequest, reply: FastifyReply) => { + const refreshToken = request.cookies.refreshToken; + if (!refreshToken) { + return reply.status(401).send(errorResponse('Refresh token required')); + } + + try { + const decoded = verifyToken(refreshToken); + const session = await prisma.session.findUnique({ where: { refreshToken } }); + + if (!session || session.isRevoked || session.expiresAt < new Date()) { + return reply.status(401).send(errorResponse('Invalid or expired session')); + } + + const newAccessToken = generateAccessToken({ + userId: decoded.userId, + email: decoded.email, + role: decoded.role, + }); + + return reply.send(successResponse({ accessToken: newAccessToken })); + } catch (err) { + return reply.status(401).send(errorResponse('Invalid refresh token')); + } + }); + + // 4. POST /api/v1/auth/logout + fastify.post('/logout', async (request: FastifyRequest, reply: FastifyReply) => { + const refreshToken = request.cookies.refreshToken; + if (refreshToken) { + await prisma.session.updateMany({ + where: { refreshToken }, + data: { isRevoked: true }, + }); + } + + reply.clearCookie('refreshToken', { path: '/api/v1/auth' }); + return reply.send(successResponse(null, 'Logged out successfully')); + }); + + // 5. GET /api/v1/auth/me + fastify.get('/me', async (request: FastifyRequest, reply: FastifyReply) => { + const authHeader = request.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return reply.status(401).send(errorResponse('Bearer token required')); + } + + try { + const token = authHeader.split(' ')[1]; + const decoded = verifyToken(token); + const user = await prisma.user.findUnique({ + where: { id: decoded.userId }, + select: { + id: true, + email: true, + username: true, + role: true, + avatar: true, + bio: true, + language: true, + currency: true, + emailVerifiedAt: true, + phoneVerifiedAt: true, + wallet: { select: { balance: true, credits: true } }, + }, + }); + + if (!user) { + return reply.status(404).send(errorResponse('User not found')); + } + + return reply.send(successResponse(user)); + } catch (err) { + return reply.status(401).send(errorResponse('Invalid or expired token')); + } + }); +} diff --git a/services/gateway/src/routes/blog.routes.d.ts b/services/gateway/src/routes/blog.routes.d.ts new file mode 100644 index 0000000..8aab5ef --- /dev/null +++ b/services/gateway/src/routes/blog.routes.d.ts @@ -0,0 +1,3 @@ +import { FastifyInstance } from 'fastify'; +export declare function blogRoutes(fastify: FastifyInstance): Promise; +//# sourceMappingURL=blog.routes.d.ts.map \ No newline at end of file diff --git a/services/gateway/src/routes/blog.routes.d.ts.map b/services/gateway/src/routes/blog.routes.d.ts.map new file mode 100644 index 0000000..6c4bf97 --- /dev/null +++ b/services/gateway/src/routes/blog.routes.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"blog.routes.d.ts","sourceRoot":"","sources":["blog.routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAIxE,wBAAsB,UAAU,CAAC,OAAO,EAAE,eAAe,iBA2FxD"} \ No newline at end of file diff --git a/services/gateway/src/routes/blog.routes.js b/services/gateway/src/routes/blog.routes.js new file mode 100644 index 0000000..e899440 --- /dev/null +++ b/services/gateway/src/routes/blog.routes.js @@ -0,0 +1,84 @@ +import { prisma } from '@luxe/database'; +import { verifyToken, successResponse, paginatedResponse, errorResponse } from '@luxe/shared'; +export async function blogRoutes(fastify) { + // GET /api/v1/blogs + fastify.get('/blogs', async (request, reply) => { + const { page = '1', pageSize = '10', category } = request.query; + const p = parseInt(page); + const ps = parseInt(pageSize); + const where = { isPublished: true }; + if (category) + where.category = category; + const [posts, total] = await Promise.all([ + prisma.blogPost.findMany({ + where, + orderBy: { publishedAt: 'desc' }, + skip: (p - 1) * ps, + take: ps, + }), + prisma.blogPost.count({ where }), + ]); + return reply.send(paginatedResponse(posts, total, p, ps)); + }); + // GET /api/v1/blogs/:slug + fastify.get('/blogs/:slug', async (request, reply) => { + const { slug } = request.params; + const post = await prisma.blogPost.findUnique({ + where: { slug }, + include: { + comments: { + include: { user: { select: { username: true, avatar: true } } }, + orderBy: { createdAt: 'desc' }, + }, + }, + }); + if (!post) + return reply.status(404).send(errorResponse('Blog post not found')); + return reply.send(successResponse(post)); + }); + // GET /api/v1/forum/topics + fastify.get('/forum/topics', async (request, reply) => { + const { page = '1', pageSize = '15' } = request.query; + const p = parseInt(page); + const ps = parseInt(pageSize); + const [threads, total] = await Promise.all([ + prisma.forumThread.findMany({ + include: { + user: { select: { username: true, avatar: true } }, + _count: { select: { replies: true } }, + }, + orderBy: [{ isPinned: 'desc' }, { createdAt: 'desc' }], + skip: (p - 1) * ps, + take: ps, + }), + prisma.forumThread.count(), + ]); + return reply.send(paginatedResponse(threads, total, p, ps)); + }); + // POST /api/v1/forum/topics + fastify.post('/forum/topics', async (request, reply) => { + const authHeader = request.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return reply.status(401).send(errorResponse('Authentication required')); + } + try { + const userPayload = verifyToken(authHeader.split(' ')[1]); + const { title, content, category } = request.body; + const slug = `${title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-${Date.now().toString(36)}`; + const thread = await prisma.forumThread.create({ + data: { + userId: userPayload.userId, + title, + slug, + content, + category: category || 'General', + }, + }); + return reply.status(201).send(successResponse(thread, 'Forum topic created')); + } + catch (err) { + return reply.status(401).send(errorResponse('Invalid token')); + } + }); +} +//# sourceMappingURL=blog.routes.js.map \ No newline at end of file diff --git a/services/gateway/src/routes/blog.routes.js.map b/services/gateway/src/routes/blog.routes.js.map new file mode 100644 index 0000000..84f0d59 --- /dev/null +++ b/services/gateway/src/routes/blog.routes.js.map @@ -0,0 +1 @@ +{"version":3,"file":"blog.routes.js","sourceRoot":"","sources":["blog.routes.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/services/gateway/src/routes/blog.routes.ts b/services/gateway/src/routes/blog.routes.ts new file mode 100644 index 0000000..a4d2dd8 --- /dev/null +++ b/services/gateway/src/routes/blog.routes.ts @@ -0,0 +1,96 @@ +import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { prisma } from '@luxe/database'; +import { verifyToken, successResponse, paginatedResponse, errorResponse } from '@luxe/shared'; + +export async function blogRoutes(fastify: FastifyInstance) { + // GET /api/v1/blogs + fastify.get('/blogs', async (request: FastifyRequest, reply: FastifyReply) => { + const { page = '1', pageSize = '10', category } = request.query as any; + const p = parseInt(page); + const ps = parseInt(pageSize); + + const where: any = { isPublished: true }; + if (category) where.category = category; + + const [posts, total] = await Promise.all([ + prisma.blogPost.findMany({ + where, + orderBy: { publishedAt: 'desc' }, + skip: (p - 1) * ps, + take: ps, + }), + prisma.blogPost.count({ where }), + ]); + + return reply.send(paginatedResponse(posts, total, p, ps)); + }); + + // GET /api/v1/blogs/:slug + fastify.get('/blogs/:slug', async (request: FastifyRequest, reply: FastifyReply) => { + const { slug } = request.params as any; + + const post = await prisma.blogPost.findUnique({ + where: { slug }, + include: { + comments: { + include: { user: { select: { username: true, avatar: true } } }, + orderBy: { createdAt: 'desc' }, + }, + }, + }); + + if (!post) return reply.status(404).send(errorResponse('Blog post not found')); + + return reply.send(successResponse(post)); + }); + + // GET /api/v1/forum/topics + fastify.get('/forum/topics', async (request: FastifyRequest, reply: FastifyReply) => { + const { page = '1', pageSize = '15' } = request.query as any; + const p = parseInt(page); + const ps = parseInt(pageSize); + + const [threads, total] = await Promise.all([ + prisma.forumThread.findMany({ + include: { + user: { select: { username: true, avatar: true } }, + _count: { select: { replies: true } }, + }, + orderBy: [{ isPinned: 'desc' }, { createdAt: 'desc' }], + skip: (p - 1) * ps, + take: ps, + }), + prisma.forumThread.count(), + ]); + + return reply.send(paginatedResponse(threads, total, p, ps)); + }); + + // POST /api/v1/forum/topics + fastify.post('/forum/topics', async (request: FastifyRequest, reply: FastifyReply) => { + const authHeader = request.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return reply.status(401).send(errorResponse('Authentication required')); + } + + try { + const userPayload = verifyToken(authHeader.split(' ')[1]); + const { title, content, category } = request.body as any; + const slug = `${title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-${Date.now().toString(36)}`; + + const thread = await prisma.forumThread.create({ + data: { + userId: userPayload.userId, + title, + slug, + content, + category: category || 'General', + }, + }); + + return reply.status(201).send(successResponse(thread, 'Forum topic created')); + } catch (err) { + return reply.status(401).send(errorResponse('Invalid token')); + } + }); +} diff --git a/services/gateway/src/routes/client.routes.d.ts b/services/gateway/src/routes/client.routes.d.ts new file mode 100644 index 0000000..bb0f596 --- /dev/null +++ b/services/gateway/src/routes/client.routes.d.ts @@ -0,0 +1,3 @@ +import { FastifyInstance } from 'fastify'; +export declare function clientRoutes(fastify: FastifyInstance): Promise; +//# sourceMappingURL=client.routes.d.ts.map \ No newline at end of file diff --git a/services/gateway/src/routes/client.routes.d.ts.map b/services/gateway/src/routes/client.routes.d.ts.map new file mode 100644 index 0000000..5241158 --- /dev/null +++ b/services/gateway/src/routes/client.routes.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"client.routes.d.ts","sourceRoot":"","sources":["client.routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAoBxE,wBAAsB,YAAY,CAAC,OAAO,EAAE,eAAe,iBA4I1D"} \ No newline at end of file diff --git a/services/gateway/src/routes/client.routes.js b/services/gateway/src/routes/client.routes.js new file mode 100644 index 0000000..cdb72c3 --- /dev/null +++ b/services/gateway/src/routes/client.routes.js @@ -0,0 +1,139 @@ +import { prisma } from '@luxe/database'; +import { verifyToken, successResponse, errorResponse, createListingSchema } from '@luxe/shared'; +async function authenticateClient(request, reply) { + const authHeader = request.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + reply.status(401).send(errorResponse('Authorization header missing')); + return null; + } + try { + const token = authHeader.split(' ')[1]; + const decoded = verifyToken(token); + return decoded; + } + catch (err) { + reply.status(401).send(errorResponse('Invalid or expired token')); + return null; + } +} +export async function clientRoutes(fastify) { + // GET /api/v1/clients/my-profile + fastify.get('/my-profile', async (request, reply) => { + const userPayload = await authenticateClient(request, reply); + if (!userPayload) + return; + const advertiser = await prisma.advertiserProfile.findUnique({ + where: { userId: userPayload.userId }, + include: { + listings: { include: { category: true, city: true, gallery: true } }, + verifications: { orderBy: { createdAt: 'desc' }, take: 1 }, + }, + }); + if (!advertiser) { + return reply.status(404).send(errorResponse('Advertiser profile not found')); + } + return reply.send(successResponse(advertiser)); + }); + // POST /api/v1/clients/listings + fastify.post('/listings', async (request, reply) => { + const userPayload = await authenticateClient(request, reply); + if (!userPayload) + return; + const parseResult = createListingSchema.safeParse(request.body); + if (!parseResult.success) { + return reply.status(400).send(errorResponse('Validation error', parseResult.error.errors.map(e => ({ field: e.path.join('.'), message: e.message })))); + } + let advertiser = await prisma.advertiserProfile.findUnique({ + where: { userId: userPayload.userId }, + }); + if (!advertiser) { + advertiser = await prisma.advertiserProfile.create({ + data: { userId: userPayload.userId, type: 'INDIVIDUAL' }, + }); + } + const { title, description, categoryId, cityId, areaId, price, currency, age, height, tagline, languages, tags } = parseResult.data; + const slug = `${title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-${Date.now().toString(36)}`; + const listing = await prisma.listing.create({ + data: { + profileId: advertiser.id, + title, + slug, + description, + categoryId, + cityId, + areaId, + price, + currency, + age, + height, + tagline, + languages, + tags, + status: 'ACTIVE', // Or PENDING_APPROVAL based on system settings + }, + }); + return reply.status(201).send(successResponse(listing, 'Listing created successfully')); + }); + // POST /api/v1/clients/my-profile/gallery + fastify.post('/my-profile/gallery', async (request, reply) => { + const userPayload = await authenticateClient(request, reply); + if (!userPayload) + return; + const { listingId, url, mediaType = 'IMAGE', isPrimary = false } = request.body; + const galleryItem = await prisma.listingGallery.create({ + data: { + listingId, + url, + mediaType, + isPrimary, + }, + }); + return reply.status(201).send(successResponse(galleryItem, 'Media added to gallery')); + }); + // POST /api/v1/clients/my-profile/verification + fastify.post('/my-profile/verification', async (request, reply) => { + const userPayload = await authenticateClient(request, reply); + if (!userPayload) + return; + const { idProofUrl, selfieUrl, notes } = request.body; + const advertiser = await prisma.advertiserProfile.findUnique({ + where: { userId: userPayload.userId }, + }); + if (!advertiser) + return reply.status(404).send(errorResponse('Advertiser profile not found')); + const verification = await prisma.verificationRequest.create({ + data: { + profileId: advertiser.id, + idProofUrl, + selfieUrl, + notes, + status: 'PENDING', + }, + }); + await prisma.advertiserProfile.update({ + where: { id: advertiser.id }, + data: { verificationStatus: 'PENDING' }, + }); + return reply.status(201).send(successResponse(verification, 'Verification submitted successfully')); + }); + // GET /api/v1/clients/analytics + fastify.get('/analytics', async (request, reply) => { + const userPayload = await authenticateClient(request, reply); + if (!userPayload) + return; + const advertiser = await prisma.advertiserProfile.findUnique({ + where: { userId: userPayload.userId }, + include: { listings: { select: { id: true, viewsCount: true, callsCount: true } } }, + }); + if (!advertiser) + return reply.status(404).send(errorResponse('Advertiser profile not found')); + const totalViews = advertiser.listings.reduce((sum, l) => sum + l.viewsCount, 0); + const totalCalls = advertiser.listings.reduce((sum, l) => sum + l.callsCount, 0); + return reply.send(successResponse({ + totalListings: advertiser.listings.length, + totalViews, + totalCalls, + })); + }); +} +//# sourceMappingURL=client.routes.js.map \ No newline at end of file diff --git a/services/gateway/src/routes/client.routes.js.map b/services/gateway/src/routes/client.routes.js.map new file mode 100644 index 0000000..9e677f7 --- /dev/null +++ b/services/gateway/src/routes/client.routes.js.map @@ -0,0 +1 @@ +{"version":3,"file":"client.routes.js","sourceRoot":"","sources":["client.routes.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/services/gateway/src/routes/client.routes.ts b/services/gateway/src/routes/client.routes.ts new file mode 100644 index 0000000..1b62ad8 --- /dev/null +++ b/services/gateway/src/routes/client.routes.ts @@ -0,0 +1,161 @@ +import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { prisma } from '@luxe/database'; +import { verifyToken, successResponse, errorResponse, createListingSchema } from '@luxe/shared'; + +async function authenticateClient(request: FastifyRequest, reply: FastifyReply) { + const authHeader = request.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + reply.status(401).send(errorResponse('Authorization header missing')); + return null; + } + try { + const token = authHeader.split(' ')[1]; + const decoded = verifyToken(token); + return decoded; + } catch (err) { + reply.status(401).send(errorResponse('Invalid or expired token')); + return null; + } +} + +export async function clientRoutes(fastify: FastifyInstance) { + // GET /api/v1/clients/my-profile + fastify.get('/my-profile', async (request: FastifyRequest, reply: FastifyReply) => { + const userPayload = await authenticateClient(request, reply); + if (!userPayload) return; + + const advertiser = await prisma.advertiserProfile.findUnique({ + where: { userId: userPayload.userId }, + include: { + listings: { include: { category: true, city: true, gallery: true } }, + verifications: { orderBy: { createdAt: 'desc' }, take: 1 }, + }, + }); + + if (!advertiser) { + return reply.status(404).send(errorResponse('Advertiser profile not found')); + } + + return reply.send(successResponse(advertiser)); + }); + + // POST /api/v1/clients/listings + fastify.post('/listings', async (request: FastifyRequest, reply: FastifyReply) => { + const userPayload = await authenticateClient(request, reply); + if (!userPayload) return; + + const parseResult = createListingSchema.safeParse(request.body); + if (!parseResult.success) { + return reply.status(400).send(errorResponse('Validation error', parseResult.error.errors.map(e => ({ field: e.path.join('.'), message: e.message })))); + } + + let advertiser = await prisma.advertiserProfile.findUnique({ + where: { userId: userPayload.userId }, + }); + + if (!advertiser) { + advertiser = await prisma.advertiserProfile.create({ + data: { userId: userPayload.userId, type: 'INDIVIDUAL' }, + }); + } + + const { title, description, categoryId, cityId, areaId, price, currency, age, height, tagline, languages, tags } = parseResult.data; + const slug = `${title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-${Date.now().toString(36)}`; + + const listing = await prisma.listing.create({ + data: { + profileId: advertiser.id, + title, + slug, + description, + categoryId, + cityId, + areaId, + price, + currency, + age, + height, + tagline, + languages, + tags, + status: 'ACTIVE', // Or PENDING_APPROVAL based on system settings + }, + }); + + return reply.status(201).send(successResponse(listing, 'Listing created successfully')); + }); + + // POST /api/v1/clients/my-profile/gallery + fastify.post('/my-profile/gallery', async (request: FastifyRequest, reply: FastifyReply) => { + const userPayload = await authenticateClient(request, reply); + if (!userPayload) return; + + const { listingId, url, mediaType = 'IMAGE', isPrimary = false } = request.body as any; + + const galleryItem = await prisma.listingGallery.create({ + data: { + listingId, + url, + mediaType, + isPrimary, + }, + }); + + return reply.status(201).send(successResponse(galleryItem, 'Media added to gallery')); + }); + + // POST /api/v1/clients/my-profile/verification + fastify.post('/my-profile/verification', async (request: FastifyRequest, reply: FastifyReply) => { + const userPayload = await authenticateClient(request, reply); + if (!userPayload) return; + + const { idProofUrl, selfieUrl, notes } = request.body as any; + + const advertiser = await prisma.advertiserProfile.findUnique({ + where: { userId: userPayload.userId }, + }); + + if (!advertiser) return reply.status(404).send(errorResponse('Advertiser profile not found')); + + const verification = await prisma.verificationRequest.create({ + data: { + profileId: advertiser.id, + idProofUrl, + selfieUrl, + notes, + status: 'PENDING', + }, + }); + + await prisma.advertiserProfile.update({ + where: { id: advertiser.id }, + data: { verificationStatus: 'PENDING' }, + }); + + return reply.status(201).send(successResponse(verification, 'Verification submitted successfully')); + }); + + // GET /api/v1/clients/analytics + fastify.get('/analytics', async (request: FastifyRequest, reply: FastifyReply) => { + const userPayload = await authenticateClient(request, reply); + if (!userPayload) return; + + const advertiser = await prisma.advertiserProfile.findUnique({ + where: { userId: userPayload.userId }, + include: { listings: { select: { id: true, viewsCount: true, callsCount: true } } }, + }); + + if (!advertiser) return reply.status(404).send(errorResponse('Advertiser profile not found')); + + const totalViews = advertiser.listings.reduce((sum: number, l: { viewsCount: number }) => sum + l.viewsCount, 0); + const totalCalls = advertiser.listings.reduce((sum: number, l: { callsCount: number }) => sum + l.callsCount, 0); + + return reply.send( + successResponse({ + totalListings: advertiser.listings.length, + totalViews, + totalCalls, + }) + ); + }); +} diff --git a/services/gateway/src/routes/listing.routes.d.ts b/services/gateway/src/routes/listing.routes.d.ts new file mode 100644 index 0000000..1bc60b9 --- /dev/null +++ b/services/gateway/src/routes/listing.routes.d.ts @@ -0,0 +1,3 @@ +import { FastifyInstance } from 'fastify'; +export declare function listingRoutes(fastify: FastifyInstance): Promise; +//# sourceMappingURL=listing.routes.d.ts.map \ No newline at end of file diff --git a/services/gateway/src/routes/listing.routes.d.ts.map b/services/gateway/src/routes/listing.routes.d.ts.map new file mode 100644 index 0000000..7722711 --- /dev/null +++ b/services/gateway/src/routes/listing.routes.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"listing.routes.d.ts","sourceRoot":"","sources":["listing.routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAIxE,wBAAsB,aAAa,CAAC,OAAO,EAAE,eAAe,iBA0I3D"} \ No newline at end of file diff --git a/services/gateway/src/routes/listing.routes.js b/services/gateway/src/routes/listing.routes.js new file mode 100644 index 0000000..fdb15e8 --- /dev/null +++ b/services/gateway/src/routes/listing.routes.js @@ -0,0 +1,115 @@ +import { prisma } from '@luxe/database'; +import { successResponse, paginatedResponse, errorResponse } from '@luxe/shared'; +export async function listingRoutes(fastify) { + // GET /api/v1/search/profiles + fastify.get('/search/profiles', async (request, reply) => { + const { query, city, category, ageMin, ageMax, priceMin, priceMax, verified, featured, page = '1', pageSize = '20', sortBy = 'createdAt', } = request.query; + const p = parseInt(page); + const ps = parseInt(pageSize); + const whereClause = { + status: 'ACTIVE', + deletedAt: null, + }; + if (query) { + whereClause.OR = [ + { title: { contains: query, mode: 'insensitive' } }, + { description: { contains: query, mode: 'insensitive' } }, + { tagline: { contains: query, mode: 'insensitive' } }, + ]; + } + if (city) { + whereClause.city = { slug: city }; + } + if (category) { + whereClause.category = { slug: category }; + } + if (ageMin || ageMax) { + whereClause.age = {}; + if (ageMin) + whereClause.age.gte = parseInt(ageMin); + if (ageMax) + whereClause.age.lte = parseInt(ageMax); + } + if (priceMin || priceMax) { + whereClause.price = {}; + if (priceMin) + whereClause.price.gte = parseFloat(priceMin); + if (priceMax) + whereClause.price.lte = parseFloat(priceMax); + } + if (verified === 'true') + whereClause.isVerified = true; + if (featured === 'true') + whereClause.isFeatured = true; + const [listings, total] = await Promise.all([ + prisma.listing.findMany({ + where: whereClause, + include: { + category: { select: { name: true, slug: true } }, + city: { select: { name: true, slug: true } }, + area: { select: { name: true, slug: true } }, + gallery: { where: { isPrimary: true }, take: 1 }, + advertiser: { select: { rating: true, reviewCount: true, verificationStatus: true } }, + }, + orderBy: { [sortBy]: 'desc' }, + skip: (p - 1) * ps, + take: ps, + }), + prisma.listing.count({ where: whereClause }), + ]); + return reply.send(paginatedResponse(listings, total, p, ps)); + }); + // GET /api/v1/profiles/featured + fastify.get('/profiles/featured', async (_request, reply) => { + const featured = await prisma.listing.findMany({ + where: { isFeatured: true, status: 'ACTIVE', deletedAt: null }, + include: { + category: true, + city: true, + gallery: true, + }, + take: 10, + }); + return reply.send(successResponse(featured)); + }); + // GET /api/v1/profiles/:slug + fastify.get('/profiles/:slug', async (request, reply) => { + const { slug } = request.params; + const listing = await prisma.listing.findUnique({ + where: { slug }, + include: { + category: true, + city: true, + area: true, + gallery: true, + advertiser: true, + reviews: { take: 5, orderBy: { createdAt: 'desc' }, include: { user: { select: { username: true, avatar: true } } } }, + }, + }); + if (!listing) + return reply.status(404).send(errorResponse('Listing profile not found')); + // Increment view count asynchronously + prisma.listing.update({ + where: { id: listing.id }, + data: { viewsCount: { increment: 1 } }, + }).catch(() => { }); + return reply.send(successResponse(listing)); + }); + // GET /api/v1/cities + fastify.get('/cities', async (_request, reply) => { + const cities = await prisma.city.findMany({ + include: { _count: { select: { listings: true } } }, + orderBy: { name: 'asc' }, + }); + return reply.send(successResponse(cities)); + }); + // GET /api/v1/categories + fastify.get('/categories', async (_request, reply) => { + const categories = await prisma.category.findMany({ + include: { _count: { select: { listings: true } } }, + orderBy: { name: 'asc' }, + }); + return reply.send(successResponse(categories)); + }); +} +//# sourceMappingURL=listing.routes.js.map \ No newline at end of file diff --git a/services/gateway/src/routes/listing.routes.js.map b/services/gateway/src/routes/listing.routes.js.map new file mode 100644 index 0000000..db37fd3 --- /dev/null +++ b/services/gateway/src/routes/listing.routes.js.map @@ -0,0 +1 @@ +{"version":3,"file":"listing.routes.js","sourceRoot":"","sources":["listing.routes.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/services/gateway/src/routes/listing.routes.ts b/services/gateway/src/routes/listing.routes.ts new file mode 100644 index 0000000..ded1f01 --- /dev/null +++ b/services/gateway/src/routes/listing.routes.ts @@ -0,0 +1,143 @@ +import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { prisma } from '@luxe/database'; +import { successResponse, paginatedResponse, errorResponse } from '@luxe/shared'; + +export async function listingRoutes(fastify: FastifyInstance) { + // GET /api/v1/search/profiles + fastify.get('/search/profiles', async (request: FastifyRequest, reply: FastifyReply) => { + const { + query, + city, + category, + ageMin, + ageMax, + priceMin, + priceMax, + verified, + featured, + page = '1', + pageSize = '20', + sortBy = 'createdAt', + } = request.query as any; + + const p = parseInt(page); + const ps = parseInt(pageSize); + + const whereClause: any = { + status: 'ACTIVE', + deletedAt: null, + }; + + if (query) { + whereClause.OR = [ + { title: { contains: query, mode: 'insensitive' } }, + { description: { contains: query, mode: 'insensitive' } }, + { tagline: { contains: query, mode: 'insensitive' } }, + ]; + } + + if (city) { + whereClause.city = { slug: city }; + } + + if (category) { + whereClause.category = { slug: category }; + } + + if (ageMin || ageMax) { + whereClause.age = {}; + if (ageMin) whereClause.age.gte = parseInt(ageMin); + if (ageMax) whereClause.age.lte = parseInt(ageMax); + } + + if (priceMin || priceMax) { + whereClause.price = {}; + if (priceMin) whereClause.price.gte = parseFloat(priceMin); + if (priceMax) whereClause.price.lte = parseFloat(priceMax); + } + + if (verified === 'true') whereClause.isVerified = true; + if (featured === 'true') whereClause.isFeatured = true; + + const [listings, total] = await Promise.all([ + prisma.listing.findMany({ + where: whereClause, + include: { + category: { select: { name: true, slug: true } }, + city: { select: { name: true, slug: true } }, + area: { select: { name: true, slug: true } }, + gallery: { where: { isPrimary: true }, take: 1 }, + advertiser: { select: { rating: true, reviewCount: true, verificationStatus: true } }, + }, + orderBy: { [sortBy]: 'desc' }, + skip: (p - 1) * ps, + take: ps, + }), + prisma.listing.count({ where: whereClause }), + ]); + + return reply.send(paginatedResponse(listings, total, p, ps)); + }); + + // GET /api/v1/profiles/featured + fastify.get('/profiles/featured', async (_request: FastifyRequest, reply: FastifyReply) => { + const featured = await prisma.listing.findMany({ + where: { isFeatured: true, status: 'ACTIVE', deletedAt: null }, + include: { + category: true, + city: true, + gallery: true, + }, + take: 10, + }); + + return reply.send(successResponse(featured)); + }); + + // GET /api/v1/profiles/:slug + fastify.get('/profiles/:slug', async (request: FastifyRequest, reply: FastifyReply) => { + const { slug } = request.params as any; + + const listing = await prisma.listing.findUnique({ + where: { slug }, + include: { + category: true, + city: true, + area: true, + gallery: true, + advertiser: true, + reviews: { take: 5, orderBy: { createdAt: 'desc' }, include: { user: { select: { username: true, avatar: true } } } }, + }, + }); + + if (!listing) return reply.status(404).send(errorResponse('Listing profile not found')); + + // Increment view count asynchronously + prisma.listing.update({ + where: { id: listing.id }, + data: { viewsCount: { increment: 1 } }, + }).catch(() => {}); + + return reply.send(successResponse(listing)); + }); + + // GET /api/v1/cities + fastify.get('/cities', async (_request: FastifyRequest, reply: FastifyReply) => { + const cities = await prisma.city.findMany({ + include: { _count: { select: { listings: true } } }, + orderBy: { name: 'asc' }, + }); + + return reply.send(successResponse(cities)); + }); + + // GET /api/v1/categories + fastify.get('/categories', async (_request: FastifyRequest, reply: FastifyReply) => { + const categories = await prisma.category.findMany({ + include: { _count: { select: { listings: true } } }, + orderBy: { name: 'asc' }, + }); + + return reply.send(successResponse(categories)); + }); +} diff --git a/services/gateway/src/routes/media.routes.d.ts b/services/gateway/src/routes/media.routes.d.ts new file mode 100644 index 0000000..e48a515 --- /dev/null +++ b/services/gateway/src/routes/media.routes.d.ts @@ -0,0 +1,3 @@ +import { FastifyInstance } from 'fastify'; +export declare function mediaRoutes(fastify: FastifyInstance): Promise; +//# sourceMappingURL=media.routes.d.ts.map \ No newline at end of file diff --git a/services/gateway/src/routes/media.routes.d.ts.map b/services/gateway/src/routes/media.routes.d.ts.map new file mode 100644 index 0000000..dce5281 --- /dev/null +++ b/services/gateway/src/routes/media.routes.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"media.routes.d.ts","sourceRoot":"","sources":["media.routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAGxE,wBAAsB,WAAW,CAAC,OAAO,EAAE,eAAe,iBA8BzD"} \ No newline at end of file diff --git a/services/gateway/src/routes/media.routes.js b/services/gateway/src/routes/media.routes.js new file mode 100644 index 0000000..e7cc93b --- /dev/null +++ b/services/gateway/src/routes/media.routes.js @@ -0,0 +1,28 @@ +import { verifyToken, successResponse, errorResponse } from '@luxe/shared'; +export async function mediaRoutes(fastify) { + // POST /api/v1/media/presigned-url + fastify.post('/presigned-url', async (request, reply) => { + const authHeader = request.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return reply.status(401).send(errorResponse('Bearer token required')); + } + try { + const userPayload = verifyToken(authHeader.split(' ')[1]); + const { fileName, fileType, folder = 'listings' } = request.body; + if (!fileName || !fileType) { + return reply.status(400).send(errorResponse('fileName and fileType are required')); + } + const key = `${folder}/${userPayload.userId}/${Date.now()}-${fileName}`; + const uploadUrl = `${process.env.S3_ENDPOINT || 'http://localhost:9000'}/${process.env.S3_BUCKET || 'luxe-media'}/${key}`; + return reply.send(successResponse({ + uploadUrl, + key, + publicUrl: uploadUrl, + }, 'Presigned URL generated')); + } + catch (err) { + return reply.status(401).send(errorResponse('Invalid token')); + } + }); +} +//# sourceMappingURL=media.routes.js.map \ No newline at end of file diff --git a/services/gateway/src/routes/media.routes.js.map b/services/gateway/src/routes/media.routes.js.map new file mode 100644 index 0000000..f7495ed --- /dev/null +++ b/services/gateway/src/routes/media.routes.js.map @@ -0,0 +1 @@ +{"version":3,"file":"media.routes.js","sourceRoot":"","sources":["media.routes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE3E,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAwB;IACxD,mCAAmC;IACnC,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,KAAK,EAAE,OAAuB,EAAE,KAAmB,EAAE,EAAE;QACpF,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC;QACjD,IAAI,CAAC,UAAU,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YACrD,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,uBAAuB,CAAC,CAAC,CAAC;QACxE,CAAC;QAED,IAAI,CAAC;YACH,MAAM,WAAW,GAAG,WAAW,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC1D,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU,EAAE,GAAG,OAAO,CAAC,IAAW,CAAC;YAExE,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAC3B,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,oCAAoC,CAAC,CAAC,CAAC;YACrF,CAAC;YAED,MAAM,GAAG,GAAG,GAAG,MAAM,IAAI,WAAW,CAAC,MAAM,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,QAAQ,EAAE,CAAC;YACxE,MAAM,SAAS,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,uBAAuB,IAAI,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,YAAY,IAAI,GAAG,EAAE,CAAC;YAE1H,OAAO,KAAK,CAAC,IAAI,CACf,eAAe,CAAC;gBACd,SAAS;gBACT,GAAG;gBACH,SAAS,EAAE,SAAS;aACrB,EAAE,yBAAyB,CAAC,CAC9B,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC;QAChE,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"} \ No newline at end of file diff --git a/services/gateway/src/routes/media.routes.ts b/services/gateway/src/routes/media.routes.ts new file mode 100644 index 0000000..d5244ad --- /dev/null +++ b/services/gateway/src/routes/media.routes.ts @@ -0,0 +1,34 @@ +import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { verifyToken, successResponse, errorResponse } from '@luxe/shared'; + +export async function mediaRoutes(fastify: FastifyInstance) { + // POST /api/v1/media/presigned-url + fastify.post('/presigned-url', async (request: FastifyRequest, reply: FastifyReply) => { + const authHeader = request.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return reply.status(401).send(errorResponse('Bearer token required')); + } + + try { + const userPayload = verifyToken(authHeader.split(' ')[1]); + const { fileName, fileType, folder = 'listings' } = request.body as any; + + if (!fileName || !fileType) { + return reply.status(400).send(errorResponse('fileName and fileType are required')); + } + + const key = `${folder}/${userPayload.userId}/${Date.now()}-${fileName}`; + const uploadUrl = `${process.env.S3_ENDPOINT || 'http://localhost:9000'}/${process.env.S3_BUCKET || 'luxe-media'}/${key}`; + + return reply.send( + successResponse({ + uploadUrl, + key, + publicUrl: uploadUrl, + }, 'Presigned URL generated') + ); + } catch (err) { + return reply.status(401).send(errorResponse('Invalid token')); + } + }); +} diff --git a/services/gateway/src/routes/user.routes.d.ts b/services/gateway/src/routes/user.routes.d.ts new file mode 100644 index 0000000..ba0073b --- /dev/null +++ b/services/gateway/src/routes/user.routes.d.ts @@ -0,0 +1,3 @@ +import { FastifyInstance } from 'fastify'; +export declare function userRoutes(fastify: FastifyInstance): Promise; +//# sourceMappingURL=user.routes.d.ts.map \ No newline at end of file diff --git a/services/gateway/src/routes/user.routes.d.ts.map b/services/gateway/src/routes/user.routes.d.ts.map new file mode 100644 index 0000000..fa306e8 --- /dev/null +++ b/services/gateway/src/routes/user.routes.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"user.routes.d.ts","sourceRoot":"","sources":["user.routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAqBxE,wBAAsB,UAAU,CAAC,OAAO,EAAE,eAAe,iBAoHxD"} \ No newline at end of file diff --git a/services/gateway/src/routes/user.routes.js b/services/gateway/src/routes/user.routes.js new file mode 100644 index 0000000..53ec618 --- /dev/null +++ b/services/gateway/src/routes/user.routes.js @@ -0,0 +1,119 @@ +import { prisma } from '@luxe/database'; +import { verifyToken, successResponse, paginatedResponse, errorResponse, hashPassword, verifyPassword } from '@luxe/shared'; +async function authenticateUser(request, reply) { + const authHeader = request.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + reply.status(401).send(errorResponse('Authorization header missing')); + return null; + } + try { + const token = authHeader.split(' ')[1]; + const decoded = verifyToken(token); + request.user = decoded; + return decoded; + } + catch (err) { + reply.status(401).send(errorResponse('Invalid or expired token')); + return null; + } +} +export async function userRoutes(fastify) { + // GET /api/v1/users/profile + fastify.get('/profile', async (request, reply) => { + const userPayload = await authenticateUser(request, reply); + if (!userPayload) + return; + const user = await prisma.user.findUnique({ + where: { id: userPayload.userId }, + include: { wallet: true, profile: true }, + }); + return reply.send(successResponse(user)); + }); + // PUT /api/v1/users/profile + fastify.put('/profile', async (request, reply) => { + const userPayload = await authenticateUser(request, reply); + if (!userPayload) + return; + const { bio, avatar, language, currency } = request.body; + const updatedUser = await prisma.user.update({ + where: { id: userPayload.userId }, + data: { bio, avatar, language, currency }, + }); + return reply.send(successResponse(updatedUser, 'Profile updated successfully')); + }); + // GET /api/v1/users/favorites + fastify.get('/favorites', async (request, reply) => { + const userPayload = await authenticateUser(request, reply); + if (!userPayload) + return; + const { page = '1', pageSize = '10' } = request.query; + const p = parseInt(page); + const ps = parseInt(pageSize); + const [favorites, total] = await Promise.all([ + prisma.favorite.findMany({ + where: { userId: userPayload.userId }, + include: { listing: { include: { category: true, city: true, gallery: true } } }, + skip: (p - 1) * ps, + take: ps, + }), + prisma.favorite.count({ where: { userId: userPayload.userId } }), + ]); + return reply.send(paginatedResponse(favorites, total, p, ps)); + }); + // POST /api/v1/users/favorites/:listingId + fastify.post('/favorites/:listingId', async (request, reply) => { + const userPayload = await authenticateUser(request, reply); + if (!userPayload) + return; + const { listingId } = request.params; + const favorite = await prisma.favorite.upsert({ + where: { userId_listingId: { userId: userPayload.userId, listingId } }, + update: {}, + create: { userId: userPayload.userId, listingId }, + }); + return reply.send(successResponse(favorite, 'Added to favorites')); + }); + // DELETE /api/v1/users/favorites/:listingId + fastify.delete('/favorites/:listingId', async (request, reply) => { + const userPayload = await authenticateUser(request, reply); + if (!userPayload) + return; + const { listingId } = request.params; + await prisma.favorite.deleteMany({ + where: { userId: userPayload.userId, listingId }, + }); + return reply.send(successResponse(null, 'Removed from favorites')); + }); + // PUT /api/v1/users/change-password + fastify.put('/change-password', async (request, reply) => { + const userPayload = await authenticateUser(request, reply); + if (!userPayload) + return; + const { oldPassword, newPassword } = request.body; + const user = await prisma.user.findUnique({ where: { id: userPayload.userId } }); + if (!user) + return reply.status(404).send(errorResponse('User not found')); + const isValid = await verifyPassword(user.passwordHash, oldPassword); + if (!isValid) + return reply.status(400).send(errorResponse('Incorrect old password')); + const newHash = await hashPassword(newPassword); + await prisma.user.update({ + where: { id: user.id }, + data: { passwordHash: newHash }, + }); + return reply.send(successResponse(null, 'Password changed successfully')); + }); + // GET /api/v1/users/notifications + fastify.get('/notifications', async (request, reply) => { + const userPayload = await authenticateUser(request, reply); + if (!userPayload) + return; + const notifications = await prisma.notification.findMany({ + where: { userId: userPayload.userId }, + orderBy: { createdAt: 'desc' }, + take: 20, + }); + return reply.send(successResponse(notifications)); + }); +} +//# sourceMappingURL=user.routes.js.map \ No newline at end of file diff --git a/services/gateway/src/routes/user.routes.js.map b/services/gateway/src/routes/user.routes.js.map new file mode 100644 index 0000000..437a71b --- /dev/null +++ b/services/gateway/src/routes/user.routes.js.map @@ -0,0 +1 @@ +{"version":3,"file":"user.routes.js","sourceRoot":"","sources":["user.routes.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/services/gateway/src/routes/user.routes.ts b/services/gateway/src/routes/user.routes.ts new file mode 100644 index 0000000..392d162 --- /dev/null +++ b/services/gateway/src/routes/user.routes.ts @@ -0,0 +1,138 @@ +import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { prisma } from '@luxe/database'; +import { verifyToken, successResponse, paginatedResponse, errorResponse, hashPassword, verifyPassword } from '@luxe/shared'; + +async function authenticateUser(request: FastifyRequest, reply: FastifyReply) { + const authHeader = request.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + reply.status(401).send(errorResponse('Authorization header missing')); + return null; + } + try { + const token = authHeader.split(' ')[1]; + const decoded = verifyToken(token); + (request as any).user = decoded; + return decoded; + } catch (err) { + reply.status(401).send(errorResponse('Invalid or expired token')); + return null; + } +} + +export async function userRoutes(fastify: FastifyInstance) { + // GET /api/v1/users/profile + fastify.get('/profile', async (request: FastifyRequest, reply: FastifyReply) => { + const userPayload = await authenticateUser(request, reply); + if (!userPayload) return; + + const user = await prisma.user.findUnique({ + where: { id: userPayload.userId }, + include: { wallet: true, profile: true }, + }); + + return reply.send(successResponse(user)); + }); + + // PUT /api/v1/users/profile + fastify.put('/profile', async (request: FastifyRequest, reply: FastifyReply) => { + const userPayload = await authenticateUser(request, reply); + if (!userPayload) return; + + const { bio, avatar, language, currency } = request.body as any; + + const updatedUser = await prisma.user.update({ + where: { id: userPayload.userId }, + data: { bio, avatar, language, currency }, + }); + + return reply.send(successResponse(updatedUser, 'Profile updated successfully')); + }); + + // GET /api/v1/users/favorites + fastify.get('/favorites', async (request: FastifyRequest, reply: FastifyReply) => { + const userPayload = await authenticateUser(request, reply); + if (!userPayload) return; + + const { page = '1', pageSize = '10' } = request.query as any; + const p = parseInt(page); + const ps = parseInt(pageSize); + + const [favorites, total] = await Promise.all([ + prisma.favorite.findMany({ + where: { userId: userPayload.userId }, + include: { listing: { include: { category: true, city: true, gallery: true } } }, + skip: (p - 1) * ps, + take: ps, + }), + prisma.favorite.count({ where: { userId: userPayload.userId } }), + ]); + + return reply.send(paginatedResponse(favorites, total, p, ps)); + }); + + // POST /api/v1/users/favorites/:listingId + fastify.post('/favorites/:listingId', async (request: FastifyRequest, reply: FastifyReply) => { + const userPayload = await authenticateUser(request, reply); + if (!userPayload) return; + + const { listingId } = request.params as any; + + const favorite = await prisma.favorite.upsert({ + where: { userId_listingId: { userId: userPayload.userId, listingId } }, + update: {}, + create: { userId: userPayload.userId, listingId }, + }); + + return reply.send(successResponse(favorite, 'Added to favorites')); + }); + + // DELETE /api/v1/users/favorites/:listingId + fastify.delete('/favorites/:listingId', async (request: FastifyRequest, reply: FastifyReply) => { + const userPayload = await authenticateUser(request, reply); + if (!userPayload) return; + + const { listingId } = request.params as any; + + await prisma.favorite.deleteMany({ + where: { userId: userPayload.userId, listingId }, + }); + + return reply.send(successResponse(null, 'Removed from favorites')); + }); + + // PUT /api/v1/users/change-password + fastify.put('/change-password', async (request: FastifyRequest, reply: FastifyReply) => { + const userPayload = await authenticateUser(request, reply); + if (!userPayload) return; + + const { oldPassword, newPassword } = request.body as any; + + const user = await prisma.user.findUnique({ where: { id: userPayload.userId } }); + if (!user) return reply.status(404).send(errorResponse('User not found')); + + const isValid = await verifyPassword(user.passwordHash, oldPassword); + if (!isValid) return reply.status(400).send(errorResponse('Incorrect old password')); + + const newHash = await hashPassword(newPassword); + await prisma.user.update({ + where: { id: user.id }, + data: { passwordHash: newHash }, + }); + + return reply.send(successResponse(null, 'Password changed successfully')); + }); + + // GET /api/v1/users/notifications + fastify.get('/notifications', async (request: FastifyRequest, reply: FastifyReply) => { + const userPayload = await authenticateUser(request, reply); + if (!userPayload) return; + + const notifications = await prisma.notification.findMany({ + where: { userId: userPayload.userId }, + orderBy: { createdAt: 'desc' }, + take: 20, + }); + + return reply.send(successResponse(notifications)); + }); +} diff --git a/services/gateway/src/routes/wallet.routes.d.ts b/services/gateway/src/routes/wallet.routes.d.ts new file mode 100644 index 0000000..a124974 --- /dev/null +++ b/services/gateway/src/routes/wallet.routes.d.ts @@ -0,0 +1,3 @@ +import { FastifyInstance } from 'fastify'; +export declare function walletRoutes(fastify: FastifyInstance): Promise; +//# sourceMappingURL=wallet.routes.d.ts.map \ No newline at end of file diff --git a/services/gateway/src/routes/wallet.routes.d.ts.map b/services/gateway/src/routes/wallet.routes.d.ts.map new file mode 100644 index 0000000..007e786 --- /dev/null +++ b/services/gateway/src/routes/wallet.routes.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"wallet.routes.d.ts","sourceRoot":"","sources":["wallet.routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAmBxE,wBAAsB,YAAY,CAAC,OAAO,EAAE,eAAe,iBAiI1D"} \ No newline at end of file diff --git a/services/gateway/src/routes/wallet.routes.js b/services/gateway/src/routes/wallet.routes.js new file mode 100644 index 0000000..215ab57 --- /dev/null +++ b/services/gateway/src/routes/wallet.routes.js @@ -0,0 +1,131 @@ +import { prisma } from '@luxe/database'; +import { verifyToken, successResponse, paginatedResponse, errorResponse } from '@luxe/shared'; +async function authenticateWalletUser(request, reply) { + const authHeader = request.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + reply.status(401).send(errorResponse('Authorization token required')); + return null; + } + try { + const token = authHeader.split(' ')[1]; + return verifyToken(token); + } + catch (err) { + reply.status(401).send(errorResponse('Invalid or expired token')); + return null; + } +} +export async function walletRoutes(fastify) { + // GET /api/v1/wallet/balance + fastify.get('/balance', async (request, reply) => { + const userPayload = await authenticateWalletUser(request, reply); + if (!userPayload) + return; + let wallet = await prisma.wallet.findUnique({ + where: { userId: userPayload.userId }, + }); + if (!wallet) { + wallet = await prisma.wallet.create({ + data: { userId: userPayload.userId, balance: 0.0, credits: 0.0 }, + }); + } + return reply.send(successResponse(wallet)); + }); + // GET /api/v1/wallet/transactions + fastify.get('/transactions', async (request, reply) => { + const userPayload = await authenticateWalletUser(request, reply); + if (!userPayload) + return; + const { page = '1', pageSize = '15' } = request.query; + const p = parseInt(page); + const ps = parseInt(pageSize); + const wallet = await prisma.wallet.findUnique({ + where: { userId: userPayload.userId }, + }); + if (!wallet) + return reply.status(404).send(errorResponse('Wallet not found')); + const [ledgers, total] = await Promise.all([ + prisma.walletLedger.findMany({ + where: { walletId: wallet.id }, + orderBy: { createdAt: 'desc' }, + skip: (p - 1) * ps, + take: ps, + }), + prisma.walletLedger.count({ where: { walletId: wallet.id } }), + ]); + return reply.send(paginatedResponse(ledgers, total, p, ps)); + }); + // POST /api/v1/wallet/recharge + fastify.post('/recharge', async (request, reply) => { + const userPayload = await authenticateWalletUser(request, reply); + if (!userPayload) + return; + const { amount, provider = 'STRIPE' } = request.body; + if (!amount || amount <= 0) { + return reply.status(400).send(errorResponse('Valid recharge amount required')); + } + // Create pending payment transaction + const transaction = await prisma.transaction.create({ + data: { + userId: userPayload.userId, + provider, + type: 'RECHARGE', + amount, + status: 'PENDING', + metadata: { clientRequestedAt: new Date().toISOString() }, + }, + }); + // Mock Gateway Checkout Session / Payment Intent response + return reply.send(successResponse({ + transactionId: transaction.id, + amount, + currency: 'USD', + clientSecret: `mock_client_secret_${transaction.id}`, + checkoutUrl: `https://checkout.luxe.com/pay/${transaction.id}`, + }, 'Recharge session initiated')); + }); + // POST /api/v1/wallet/coupon/redeem + fastify.post('/coupon/redeem', async (request, reply) => { + const userPayload = await authenticateWalletUser(request, reply); + if (!userPayload) + return; + const { code } = request.body; + const coupon = await prisma.coupon.findUnique({ where: { code } }); + if (!coupon) + return reply.status(404).send(errorResponse('Invalid coupon code')); + if (coupon.usedCount >= coupon.maxUses) { + return reply.status(400).send(errorResponse('Coupon max usage reached')); + } + if (coupon.expiresAt && coupon.expiresAt < new Date()) { + return reply.status(400).send(errorResponse('Coupon code has expired')); + } + const wallet = await prisma.wallet.findUnique({ where: { userId: userPayload.userId } }); + if (!wallet) + return reply.status(404).send(errorResponse('Wallet not found')); + const bonusCredits = coupon.fixedAmount || 50.0; + const balanceBefore = wallet.credits; + const balanceAfter = balanceBefore + bonusCredits; + await prisma.$transaction([ + prisma.wallet.update({ + where: { id: wallet.id }, + data: { credits: { increment: bonusCredits } }, + }), + prisma.walletLedger.create({ + data: { + walletId: wallet.id, + type: 'BONUS', + amount: bonusCredits, + balanceBefore, + balanceAfter, + description: `Coupon redemption: ${code}`, + }, + }), + prisma.coupon.update({ + where: { id: coupon.id }, + data: { usedCount: { increment: 1 } }, + }), + ]); + return reply.send(successResponse({ bonusCredits, newCreditBalance: balanceAfter }, 'Coupon redeemed successfully')); + }); +} +//# sourceMappingURL=wallet.routes.js.map \ No newline at end of file diff --git a/services/gateway/src/routes/wallet.routes.js.map b/services/gateway/src/routes/wallet.routes.js.map new file mode 100644 index 0000000..33b1547 --- /dev/null +++ b/services/gateway/src/routes/wallet.routes.js.map @@ -0,0 +1 @@ +{"version":3,"file":"wallet.routes.js","sourceRoot":"","sources":["wallet.routes.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/services/gateway/src/routes/wallet.routes.ts b/services/gateway/src/routes/wallet.routes.ts new file mode 100644 index 0000000..1a9704a --- /dev/null +++ b/services/gateway/src/routes/wallet.routes.ts @@ -0,0 +1,149 @@ +import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { prisma } from '@luxe/database'; +import { verifyToken, successResponse, paginatedResponse, errorResponse } from '@luxe/shared'; + +async function authenticateWalletUser(request: FastifyRequest, reply: FastifyReply) { + const authHeader = request.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + reply.status(401).send(errorResponse('Authorization token required')); + return null; + } + try { + const token = authHeader.split(' ')[1]; + return verifyToken(token); + } catch (err) { + reply.status(401).send(errorResponse('Invalid or expired token')); + return null; + } +} + +export async function walletRoutes(fastify: FastifyInstance) { + // GET /api/v1/wallet/balance + fastify.get('/balance', async (request: FastifyRequest, reply: FastifyReply) => { + const userPayload = await authenticateWalletUser(request, reply); + if (!userPayload) return; + + let wallet = await prisma.wallet.findUnique({ + where: { userId: userPayload.userId }, + }); + + if (!wallet) { + wallet = await prisma.wallet.create({ + data: { userId: userPayload.userId, balance: 0.0, credits: 0.0 }, + }); + } + + return reply.send(successResponse(wallet)); + }); + + // GET /api/v1/wallet/transactions + fastify.get('/transactions', async (request: FastifyRequest, reply: FastifyReply) => { + const userPayload = await authenticateWalletUser(request, reply); + if (!userPayload) return; + + const { page = '1', pageSize = '15' } = request.query as any; + const p = parseInt(page); + const ps = parseInt(pageSize); + + const wallet = await prisma.wallet.findUnique({ + where: { userId: userPayload.userId }, + }); + + if (!wallet) return reply.status(404).send(errorResponse('Wallet not found')); + + const [ledgers, total] = await Promise.all([ + prisma.walletLedger.findMany({ + where: { walletId: wallet.id }, + orderBy: { createdAt: 'desc' }, + skip: (p - 1) * ps, + take: ps, + }), + prisma.walletLedger.count({ where: { walletId: wallet.id } }), + ]); + + return reply.send(paginatedResponse(ledgers, total, p, ps)); + }); + + // POST /api/v1/wallet/recharge + fastify.post('/recharge', async (request: FastifyRequest, reply: FastifyReply) => { + const userPayload = await authenticateWalletUser(request, reply); + if (!userPayload) return; + + const { amount, provider = 'STRIPE' } = request.body as any; + if (!amount || amount <= 0) { + return reply.status(400).send(errorResponse('Valid recharge amount required')); + } + + // Create pending payment transaction + const transaction = await prisma.transaction.create({ + data: { + userId: userPayload.userId, + provider, + type: 'RECHARGE', + amount, + status: 'PENDING', + metadata: { clientRequestedAt: new Date().toISOString() }, + }, + }); + + // Mock Gateway Checkout Session / Payment Intent response + return reply.send( + successResponse({ + transactionId: transaction.id, + amount, + currency: 'USD', + clientSecret: `mock_client_secret_${transaction.id}`, + checkoutUrl: `https://checkout.luxe.com/pay/${transaction.id}`, + }, 'Recharge session initiated') + ); + }); + + // POST /api/v1/wallet/coupon/redeem + fastify.post('/coupon/redeem', async (request: FastifyRequest, reply: FastifyReply) => { + const userPayload = await authenticateWalletUser(request, reply); + if (!userPayload) return; + + const { code } = request.body as any; + + const coupon = await prisma.coupon.findUnique({ where: { code } }); + if (!coupon) return reply.status(404).send(errorResponse('Invalid coupon code')); + + if (coupon.usedCount >= coupon.maxUses) { + return reply.status(400).send(errorResponse('Coupon max usage reached')); + } + + if (coupon.expiresAt && coupon.expiresAt < new Date()) { + return reply.status(400).send(errorResponse('Coupon code has expired')); + } + + const wallet = await prisma.wallet.findUnique({ where: { userId: userPayload.userId } }); + if (!wallet) return reply.status(404).send(errorResponse('Wallet not found')); + + const bonusCredits = coupon.fixedAmount || 50.0; + const balanceBefore = wallet.credits; + const balanceAfter = balanceBefore + bonusCredits; + + await prisma.$transaction([ + prisma.wallet.update({ + where: { id: wallet.id }, + data: { credits: { increment: bonusCredits } }, + }), + prisma.walletLedger.create({ + data: { + walletId: wallet.id, + type: 'BONUS', + amount: bonusCredits, + balanceBefore, + balanceAfter, + description: `Coupon redemption: ${code}`, + }, + }), + prisma.coupon.update({ + where: { id: coupon.id }, + data: { usedCount: { increment: 1 } }, + }), + ]); + + return reply.send(successResponse({ bonusCredits, newCreditBalance: balanceAfter }, 'Coupon redeemed successfully')); + }); +} diff --git a/services/gateway/tsconfig.json b/services/gateway/tsconfig.json new file mode 100644 index 0000000..8c65f9b --- /dev/null +++ b/services/gateway/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src/**/*"] +} diff --git a/services/notification-service/package.json b/services/notification-service/package.json new file mode 100644 index 0000000..744d438 --- /dev/null +++ b/services/notification-service/package.json @@ -0,0 +1,25 @@ +{ + "name": "@luxe/notification-service", + "version": "1.0.0", + "private": true, + "type": "module", + "main": "dist/index.js", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js" + }, + "dependencies": { + "@luxe/database": "*", + "@luxe/shared": "*", + "bullmq": "^5.34.8", + "ioredis": "^5.4.2", + "nodemailer": "^6.9.16" + }, + "devDependencies": { + "@types/node": "^22.10.1", + "@types/nodemailer": "^6.4.17", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + } +} diff --git a/services/notification-service/src/index.d.ts b/services/notification-service/src/index.d.ts new file mode 100644 index 0000000..8a67506 --- /dev/null +++ b/services/notification-service/src/index.d.ts @@ -0,0 +1,7 @@ +export interface NotificationJobData { + type: 'EMAIL' | 'SMS' | 'WHATSAPP' | 'TELEGRAM'; + recipient: string; + subject?: string; + body: string; +} +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/services/notification-service/src/index.d.ts.map b/services/notification-service/src/index.d.ts.map new file mode 100644 index 0000000..6799615 --- /dev/null +++ b/services/notification-service/src/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,OAAO,GAAG,KAAK,GAAG,UAAU,GAAG,UAAU,CAAC;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd"} \ No newline at end of file diff --git a/services/notification-service/src/index.js b/services/notification-service/src/index.js new file mode 100644 index 0000000..fcbffd9 --- /dev/null +++ b/services/notification-service/src/index.js @@ -0,0 +1,53 @@ +import { Worker } from 'bullmq'; +import nodemailer from 'nodemailer'; +import { logger } from '@luxe/shared'; +const REDIS_HOST = process.env.REDIS_HOST || 'localhost'; +const REDIS_PORT = Number(process.env.REDIS_PORT) || 6379; +const transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST || 'localhost', + port: Number(process.env.SMTP_PORT) || 1025, + secure: false, +}); +logger.info('🔔 Starting Luxe Notification Queue Worker...'); +const worker = new Worker('notification-queue', async (job) => { + logger.info(`Processing notification job ${job.id} of type: ${job.data.type}`); + switch (job.data.type) { + case 'EMAIL': + await transporter.sendMail({ + from: process.env.EMAIL_FROM || '"Luxe Marketplace" ', + to: job.data.recipient, + subject: job.data.subject || 'Luxe Notification', + text: job.data.body, + html: `
+

Luxe Platform

+

${job.data.body}

+
`, + }); + logger.info(`Email successfully dispatched to ${job.data.recipient}`); + break; + case 'SMS': + logger.info(`[SMS STUB] Sent SMS to ${job.data.recipient}: "${job.data.body}"`); + break; + case 'WHATSAPP': + logger.info(`[WHATSAPP STUB] Sent WhatsApp message to ${job.data.recipient}`); + break; + case 'TELEGRAM': + logger.info(`[TELEGRAM STUB] Sent Telegram message to ${job.data.recipient}`); + break; + default: + logger.warn(`Unknown notification channel type: ${job.data.type}`); + } +}, { + connection: { + host: REDIS_HOST, + port: REDIS_PORT, + }, + concurrency: 5, +}); +worker.on('completed', (job) => { + logger.info(`Job ${job.id} completed successfully`); +}); +worker.on('failed', (job, err) => { + logger.error(`Job ${job?.id} failed with error: ${err.message}`); +}); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/services/notification-service/src/index.js.map b/services/notification-service/src/index.js.map new file mode 100644 index 0000000..9e49b57 --- /dev/null +++ b/services/notification-service/src/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAO,MAAM,QAAQ,CAAC;AACrC,OAAO,UAAU,MAAM,YAAY,CAAC;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEtC,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,WAAW,CAAC;AACzD,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC;AAE1D,MAAM,WAAW,GAAG,UAAU,CAAC,eAAe,CAAC;IAC7C,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,WAAW;IAC1C,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,IAAI;IAC3C,MAAM,EAAE,KAAK;CACd,CAAC,CAAC;AASH,MAAM,CAAC,IAAI,CAAC,+CAA+C,CAAC,CAAC;AAE7D,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB,oBAAoB,EACpB,KAAK,EAAE,GAA6B,EAAE,EAAE;IACtC,MAAM,CAAC,IAAI,CAAC,+BAA+B,GAAG,CAAC,EAAE,aAAa,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IAE/E,QAAQ,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACtB,KAAK,OAAO;YACV,MAAM,WAAW,CAAC,QAAQ,CAAC;gBACzB,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,wCAAwC;gBACxE,EAAE,EAAE,GAAG,CAAC,IAAI,CAAC,SAAS;gBACtB,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC,OAAO,IAAI,mBAAmB;gBAChD,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI;gBACnB,IAAI,EAAE;;uBAEO,GAAG,CAAC,IAAI,CAAC,IAAI;wBACZ;aACf,CAAC,CAAC;YACH,MAAM,CAAC,IAAI,CAAC,oCAAoC,GAAG,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;YACtE,MAAM;QAER,KAAK,KAAK;YACR,MAAM,CAAC,IAAI,CAAC,0BAA0B,GAAG,CAAC,IAAI,CAAC,SAAS,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;YAChF,MAAM;QAER,KAAK,UAAU;YACb,MAAM,CAAC,IAAI,CAAC,4CAA4C,GAAG,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;YAC9E,MAAM;QAER,KAAK,UAAU;YACb,MAAM,CAAC,IAAI,CAAC,4CAA4C,GAAG,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;YAC9E,MAAM;QAER;YACE,MAAM,CAAC,IAAI,CAAC,sCAAuC,GAAG,CAAC,IAAY,CAAC,IAAI,EAAE,CAAC,CAAC;IAChF,CAAC;AACH,CAAC,EACD;IACE,UAAU,EAAE;QACV,IAAI,EAAE,UAAU;QAChB,IAAI,EAAE,UAAU;KACjB;IACD,WAAW,EAAE,CAAC;CACf,CACF,CAAC;AAEF,MAAM,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC,GAAG,EAAE,EAAE;IAC7B,MAAM,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,EAAE,yBAAyB,CAAC,CAAC;AACtD,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IAC/B,MAAM,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,EAAE,uBAAuB,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;AACnE,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/services/notification-service/src/index.ts b/services/notification-service/src/index.ts new file mode 100644 index 0000000..785fc24 --- /dev/null +++ b/services/notification-service/src/index.ts @@ -0,0 +1,74 @@ +import { Worker, Job } from 'bullmq'; +import nodemailer from 'nodemailer'; +import { logger } from '@luxe/shared'; + +const REDIS_HOST = process.env.REDIS_HOST || 'localhost'; +const REDIS_PORT = Number(process.env.REDIS_PORT) || 6379; + +const transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST || 'localhost', + port: Number(process.env.SMTP_PORT) || 1025, + secure: false, +}); + +export interface NotificationJobData { + type: 'EMAIL' | 'SMS' | 'WHATSAPP' | 'TELEGRAM'; + recipient: string; + subject?: string; + body: string; +} + +logger.info('🔔 Starting Luxe Notification Queue Worker...'); + +const worker = new Worker( + 'notification-queue', + async (job: Job) => { + logger.info(`Processing notification job ${job.id} of type: ${job.data.type}`); + + switch (job.data.type) { + case 'EMAIL': + await transporter.sendMail({ + from: process.env.EMAIL_FROM || '"Luxe Marketplace" ', + to: job.data.recipient, + subject: job.data.subject || 'Luxe Notification', + text: job.data.body, + html: `
+

Luxe Platform

+

${job.data.body}

+
`, + }); + logger.info(`Email successfully dispatched to ${job.data.recipient}`); + break; + + case 'SMS': + logger.info(`[SMS STUB] Sent SMS to ${job.data.recipient}: "${job.data.body}"`); + break; + + case 'WHATSAPP': + logger.info(`[WHATSAPP STUB] Sent WhatsApp message to ${job.data.recipient}`); + break; + + case 'TELEGRAM': + logger.info(`[TELEGRAM STUB] Sent Telegram message to ${job.data.recipient}`); + break; + + default: + logger.warn(`Unknown notification channel type: ${(job.data as any).type}`); + } + }, + { + connection: { + host: REDIS_HOST, + port: REDIS_PORT, + }, + concurrency: 5, + } +); + +worker.on('completed', (job) => { + logger.info(`Job ${job.id} completed successfully`); +}); + +worker.on('failed', (job, err) => { + logger.error(`Job ${job?.id} failed with error: ${err.message}`); +}); diff --git a/services/notification-service/tsconfig.json b/services/notification-service/tsconfig.json new file mode 100644 index 0000000..8c65f9b --- /dev/null +++ b/services/notification-service/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src/**/*"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c96e08b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "composite": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "baseUrl": ".", + "paths": { + "@luxe/database": ["./packages/database/src/index.ts"], + "@luxe/shared": ["./packages/shared/src/index.ts"] + } + }, + "exclude": ["node_modules", "dist", "build", "coverage"] +}