first commit

This commit is contained in:
hardik 2026-07-24 14:47:09 +05:30
commit 146ef59847
118 changed files with 8055 additions and 0 deletions

61
.env.example Normal file
View File

@ -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 <no-reply@luxe.com>"
# 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

32
.gitignore vendored Normal file
View File

@ -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

View File

@ -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>`
* Access Token expiry: 1560 minutes
* Refresh Token expiry: 730 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.

36
docker/Dockerfile.gateway Normal file
View File

@ -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"]

79
docker/docker-compose.yml Normal file
View File

@ -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:

8
docker/prometheus.yml Normal file
View File

@ -0,0 +1,8 @@
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: "luxe-gateway"
static_configs:
- targets: ["host.docker.internal:3000"]

50
docs/ARCHITECTURE.md Normal file
View File

@ -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.

55
docs/DEPLOYMENT.md Normal file
View File

@ -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%.

20
docs/SECURITY.md Normal file
View File

@ -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.

102
k8s/deployment.yaml Normal file
View File

@ -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

2969
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
package.json Normal file
View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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
}

2
packages/database/prisma/seed.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=seed.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"seed.d.ts","sourceRoot":"","sources":["seed.ts"],"names":[],"mappings":""}

View File

@ -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

File diff suppressed because one or more lines are too long

View File

@ -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();
});

7
packages/database/src/index.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
import { PrismaClient } from '@prisma/client';
declare global {
var prismaGlobal: PrismaClient | undefined;
}
export declare const prisma: PrismaClient<import("@prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
export * from '@prisma/client';
//# sourceMappingURL=index.d.ts.map

View File

@ -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"}

View File

@ -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

View File

@ -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"}

View File

@ -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';

View File

@ -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/**/*"]
}

View File

@ -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"
}
}

7
packages/shared/src/index.d.ts vendored Normal file
View File

@ -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

View File

@ -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"}

View File

@ -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

View File

@ -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"}

View File

@ -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';

3
packages/shared/src/logger/index.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
import pino from 'pino';
export declare const logger: pino.Logger<never, boolean>;
//# sourceMappingURL=index.d.ts.map

View File

@ -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"}

View File

@ -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

View File

@ -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"}

View File

@ -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'],
});

View File

@ -0,0 +1,3 @@
import { FastifyRequest, FastifyReply } from 'fastify';
export declare function authorize(allowedRoles: string[]): (request: FastifyRequest, reply: FastifyReply) => Promise<undefined>;
//# sourceMappingURL=rbac.d.ts.map

View File

@ -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"}

View File

@ -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

View File

@ -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"}

View File

@ -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'));
}
};
}

3
packages/shared/src/utils/argon2.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
export declare function hashPassword(password: string): Promise<string>;
export declare function verifyPassword(hash: string, plainText: string): Promise<boolean>;
//# sourceMappingURL=argon2.d.ts.map

View File

@ -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"}

View File

@ -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

View File

@ -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"}

View File

@ -0,0 +1,18 @@
import * as argon2 from 'argon2';
export async function hashPassword(password: string): Promise<string> {
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<boolean> {
try {
return await argon2.verify(hash, plainText);
} catch (err) {
return false;
}
}

10
packages/shared/src/utils/jwt.d.ts vendored Normal file
View File

@ -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

View File

@ -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"}

View File

@ -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

View File

@ -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"}

View File

@ -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;
}

22
packages/shared/src/utils/response.d.ts vendored Normal file
View File

@ -0,0 +1,22 @@
export interface ApiResponse<T = any> {
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<T>(data: T, message?: string): ApiResponse<T>;
export declare function paginatedResponse<T>(data: T[], total: number, page: number, pageSize: number, message?: string): ApiResponse<T[]>;
export declare function errorResponse(message?: string, errors?: Array<{
field?: string;
message: string;
}>): ApiResponse;
//# sourceMappingURL=response.d.ts.map

View File

@ -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"}

View File

@ -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

View File

@ -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"}

View File

@ -0,0 +1,51 @@
export interface ApiResponse<T = any> {
success: boolean;
message?: string;
data?: T;
pagination?: {
total: number;
page: number;
pageSize: number;
hasMore: boolean;
};
errors?: Array<{ field?: string; message: string }>;
}
export function successResponse<T>(data: T, message = 'Operation executed successfully'): ApiResponse<T> {
return {
success: true,
message,
data,
};
}
export function paginatedResponse<T>(
data: T[],
total: number,
page: number,
pageSize: number,
message = 'Operation executed successfully'
): ApiResponse<T[]> {
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,
};
}

View File

@ -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<z.ZodEnum<["USER", "ADVERTISER", "AGENCY"]>>;
phone: z.ZodOptional<z.ZodString>;
}, "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<z.ZodString>;
price: z.ZodNumber;
currency: z.ZodDefault<z.ZodString>;
age: z.ZodOptional<z.ZodNumber>;
height: z.ZodOptional<z.ZodString>;
tagline: z.ZodOptional<z.ZodString>;
languages: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
}, "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

View File

@ -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"}

View File

@ -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

View File

@ -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"}

View File

@ -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([]),
});

View File

@ -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/**/*"]
}

21
scripts/dev.mjs Normal file
View File

@ -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}`);
});
});

View File

@ -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"
}
}

2
services/gateway/src/index.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=index.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":""}

View File

@ -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

View File

@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"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"}

View File

@ -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();

View File

@ -0,0 +1,3 @@
import { FastifyInstance } from 'fastify';
export declare function adminRoutes(fastify: FastifyInstance): Promise<void>;
//# sourceMappingURL=admin.routes.d.ts.map

View File

@ -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"}

View File

@ -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

File diff suppressed because one or more lines are too long

View File

@ -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));
});
}

View File

@ -0,0 +1,3 @@
import { FastifyInstance } from 'fastify';
export declare function authRoutes(fastify: FastifyInstance): Promise<void>;
//# sourceMappingURL=auth.routes.d.ts.map

View File

@ -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"}

View File

@ -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

File diff suppressed because one or more lines are too long

View File

@ -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'));
}
});
}

View File

@ -0,0 +1,3 @@
import { FastifyInstance } from 'fastify';
export declare function blogRoutes(fastify: FastifyInstance): Promise<void>;
//# sourceMappingURL=blog.routes.d.ts.map

View File

@ -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"}

View File

@ -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

View File

@ -0,0 +1 @@
{"version":3,"file":"blog.routes.js","sourceRoot":"","sources":["blog.routes.ts"],"names":[],"mappings":"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"}

View File

@ -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'));
}
});
}

View File

@ -0,0 +1,3 @@
import { FastifyInstance } from 'fastify';
export declare function clientRoutes(fastify: FastifyInstance): Promise<void>;
//# sourceMappingURL=client.routes.d.ts.map

View File

@ -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"}

View File

@ -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

File diff suppressed because one or more lines are too long

View File

@ -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,
})
);
});
}

View File

@ -0,0 +1,3 @@
import { FastifyInstance } from 'fastify';
export declare function listingRoutes(fastify: FastifyInstance): Promise<void>;
//# sourceMappingURL=listing.routes.d.ts.map

View File

@ -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"}

View File

@ -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

View File

@ -0,0 +1 @@
{"version":3,"file":"listing.routes.js","sourceRoot":"","sources":["listing.routes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAEjF,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAwB;IAC1D,8BAA8B;IAC9B,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,KAAK,EAAE,OAAuB,EAAE,KAAmB,EAAE,EAAE;QACrF,MAAM,EACJ,KAAK,EACL,IAAI,EACJ,QAAQ,EACR,MAAM,EACN,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,IAAI,GAAG,GAAG,EACV,QAAQ,GAAG,IAAI,EACf,MAAM,GAAG,WAAW,GACrB,GAAG,OAAO,CAAC,KAAY,CAAC;QAEzB,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QACzB,MAAM,EAAE,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAE9B,MAAM,WAAW,GAAQ;YACvB,MAAM,EAAE,QAAQ;YAChB,SAAS,EAAE,IAAI;SAChB,CAAC;QAEF,IAAI,KAAK,EAAE,CAAC;YACV,WAAW,CAAC,EAAE,GAAG;gBACf,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE;gBACnD,EAAE,WAAW,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE;gBACzD,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE;aACtD,CAAC;QACJ,CAAC;QAED,IAAI,IAAI,EAAE,CAAC;YACT,WAAW,CAAC,IAAI,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QACpC,CAAC;QAED,IAAI,QAAQ,EAAE,CAAC;YACb,WAAW,CAAC,QAAQ,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;QAC5C,CAAC;QAED,IAAI,MAAM,IAAI,MAAM,EAAE,CAAC;YACrB,WAAW,CAAC,GAAG,GAAG,EAAE,CAAC;YACrB,IAAI,MAAM;gBAAE,WAAW,CAAC,GAAG,CAAC,GAAG,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;YACnD,IAAI,MAAM;gBAAE,WAAW,CAAC,GAAG,CAAC,GAAG,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;QACrD,CAAC;QAED,IAAI,QAAQ,IAAI,QAAQ,EAAE,CAAC;YACzB,WAAW,CAAC,KAAK,GAAG,EAAE,CAAC;YACvB,IAAI,QAAQ;gBAAE,WAAW,CAAC,KAAK,CAAC,GAAG,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;YAC3D,IAAI,QAAQ;gBAAE,WAAW,CAAC,KAAK,CAAC,GAAG,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;QAC7D,CAAC;QAED,IAAI,QAAQ,KAAK,MAAM;YAAE,WAAW,CAAC,UAAU,GAAG,IAAI,CAAC;QACvD,IAAI,QAAQ,KAAK,MAAM;YAAE,WAAW,CAAC,UAAU,GAAG,IAAI,CAAC;QAEvD,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YAC1C,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC;gBACtB,KAAK,EAAE,WAAW;gBAClB,OAAO,EAAE;oBACP,QAAQ,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;oBAChD,IAAI,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;oBAC5C,IAAI,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;oBAC5C,OAAO,EAAE,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE;oBAChD,UAAU,EAAE,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,kBAAkB,EAAE,IAAI,EAAE,EAAE;iBACtF;gBACD,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE;gBAC7B,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE;gBAClB,IAAI,EAAE,EAAE;aACT,CAAC;YACF,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC;SAC7C,CAAC,CAAC;QAEH,OAAO,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,gCAAgC;IAChC,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,KAAK,EAAE,QAAwB,EAAE,KAAmB,EAAE,EAAE;QACxF,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC;YAC7C,KAAK,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE;YAC9D,OAAO,EAAE;gBACP,QAAQ,EAAE,IAAI;gBACd,IAAI,EAAE,IAAI;gBACV,OAAO,EAAE,IAAI;aACd;YACD,IAAI,EAAE,EAAE;SACT,CAAC,CAAC;QAEH,OAAO,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,6BAA6B;IAC7B,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,KAAK,EAAE,OAAuB,EAAE,KAAmB,EAAE,EAAE;QACpF,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,MAAa,CAAC;QAEvC,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC;YAC9C,KAAK,EAAE,EAAE,IAAI,EAAE;YACf,OAAO,EAAE;gBACP,QAAQ,EAAE,IAAI;gBACd,IAAI,EAAE,IAAI;gBACV,IAAI,EAAE,IAAI;gBACV,OAAO,EAAE,IAAI;gBACb,UAAU,EAAE,IAAI;gBAChB,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE;aACtH;SACF,CAAC,CAAC;QAEH,IAAI,CAAC,OAAO;YAAE,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,2BAA2B,CAAC,CAAC,CAAC;QAExF,sCAAsC;QACtC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;YACpB,KAAK,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE;YACzB,IAAI,EAAE,EAAE,UAAU,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,EAAE;SACvC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAEnB,OAAO,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,qBAAqB;IACrB,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,EAAE,QAAwB,EAAE,KAAmB,EAAE,EAAE;QAC7E,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC;YACxC,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE;YACnD,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE;SACzB,CAAC,CAAC;QAEH,OAAO,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,yBAAyB;IACzB,OAAO,CAAC,GAAG,CAAC,aAAa,EAAE,KAAK,EAAE,QAAwB,EAAE,KAAmB,EAAE,EAAE;QACjF,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAChD,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE;YACnD,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE;SACzB,CAAC,CAAC;QAEH,OAAO,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;AACL,CAAC"}

View File

@ -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));
});
}

View File

@ -0,0 +1,3 @@
import { FastifyInstance } from 'fastify';
export declare function mediaRoutes(fastify: FastifyInstance): Promise<void>;
//# sourceMappingURL=media.routes.d.ts.map

View File

@ -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"}

View File

@ -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

View File

@ -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"}

View File

@ -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'));
}
});
}

View File

@ -0,0 +1,3 @@
import { FastifyInstance } from 'fastify';
export declare function userRoutes(fastify: FastifyInstance): Promise<void>;
//# sourceMappingURL=user.routes.d.ts.map

Some files were not shown because too many files have changed in this diff Show More