Final production build for proselitigant.tech

This commit is contained in:
gamvo74 2026-02-25 00:30:57 -05:00
parent e3c7ff9758
commit 3e51d6222f
50 changed files with 1895 additions and 730 deletions

View File

@ -2,11 +2,10 @@
POSTGRES_USER=myuser
POSTGRES_PASSWORD=mypassword
POSTGRES_DB=pro_se_litigant
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?schema=public
# API Configuration
PORT=4000
CORS_ORIGIN=https://app.proselitigant.com
CORS_ORIGIN=https://proselitigant.tech
NODE_ENV=production
# Rate Limiting

BIN
.github/README.md vendored Normal file

Binary file not shown.

BIN
.github/workflows/README.md vendored Normal file

Binary file not shown.

56
.github/workflows/deploy-api.yml vendored Normal file
View File

@ -0,0 +1,56 @@
name: Build and Push API Image
on:
push:
branches:
- main
paths:
- 'apps/api/**'
- 'packages/**'
- 'package.json'
- 'package-lock.json'
- '.github/workflows/deploy-api.yml'
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}-api
jobs:
build-and-push-api:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest
type=sha
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: ./apps/api
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

25
.github/workflows/deploy-vps.yml vendored Normal file
View File

@ -0,0 +1,25 @@
name: Deploy to VPS
on:
workflow_run:
workflows: ["Build and Push Web Image", "Build and Push API Image"]
types:
- completed
jobs:
deploy:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USERNAME }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
cd pro-se-litigant
git pull
# Login if needed, or assume already logged in
# echo ${{ secrets.CR_PAT }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
./deploy.sh

56
.github/workflows/deploy-web.yml vendored Normal file
View File

@ -0,0 +1,56 @@
name: Build and Push Web Image
on:
push:
branches:
- main
paths:
- 'apps/web/**'
- 'packages/**'
- 'package.json'
- 'package-lock.json'
- '.github/workflows/deploy-web.yml'
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}-web
jobs:
build-and-push-web:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest
type=sha
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: ./apps/web
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

37
.gitignore vendored Normal file
View File

@ -0,0 +1,37 @@
# Dependencies
node_modules
.pnp
.pnp.js
# Testing
coverage
# Next.js
.next/
out/
build
# NestJS
dist/
# Misc
.DS_Store
*.pem
# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment Variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Vercel
.vercel
# Turborepo
.turbo

View File

@ -1,2 +1,61 @@
# pro-se-litigant
Pro Se Litigant Respository
# Pro Se Litigant - Deployment Guide
This project is configured for deployment using Docker and GitHub Actions.
## Prerequisites
1. **VPS** with Docker and Docker Compose installed.
2. **GitHub Repository** hosting this code.
3. **Domain Name** (optional, but recommended for production).
## Setup on VPS
1. **Clone the repository** (or copy `docker-compose.prod.yml`, `infrastructure/`, and `deploy.sh` to your server).
```bash
git clone <your-repo-url>
cd pro-se-litigant
```
2. **Create a `.env` file** based on `.env.example`.
```bash
cp .env.example .env
nano .env
```
**Critical variables to set:**
* `DOCKER_IMAGE_OWNER`: Your GitHub username (lowercase).
* `POSTGRES_USER`: Database username.
* `POSTGRES_PASSWORD`: Database password.
* `JWT_SECRET`: A secure random string.
* `CORS_ORIGIN`: Your frontend URL (e.g., `http://your-domain.com`).
* `NEXT_PUBLIC_API_URL`: Your API URL (e.g., `http://your-domain.com/api`).
3. **Login to GitHub Container Registry (GHCR)**
You need a Personal Access Token (classic) with `read:packages` scope.
```bash
echo <YOUR_PAT> | docker login ghcr.io -u <YOUR_GITHUB_USERNAME> --password-stdin
```
4. **Initial Deployment**
Run the deployment script:
```bash
chmod +x deploy.sh
./deploy.sh
```
## CI/CD (GitHub Actions)
The repository includes workflows to automatically build and push Docker images to GHCR on every push to `main`.
* `.github/workflows/deploy-web.yml`: Builds `pro-se-litigant-web`
* `.github/workflows/deploy-api.yml`: Builds `pro-se-litigant-api`
### Automatic Deployment (Optional)
To enable automatic deployment to your VPS after build:
1. Add the following secrets to your GitHub Repository (Settings -> Secrets and variables -> Actions):
* `VPS_HOST`: IP address of your VPS.
* `VPS_USERNAME`: SSH username (e.g., `root` or `ubuntu`).
* `VPS_SSH_KEY`: Your private SSH key.
2. Create a new workflow `.github/workflows/deploy-vps.yml` that uses `appleboy/ssh-action` to run `./deploy.sh` on your server.

View File

@ -23,13 +23,7 @@
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository for the Pro Se Litigant API.
## Features Added
- **RBAC (Role-Based Access Control):** Uses `@Roles` decorator and global `RolesGuard` to manage access (USER/ADMIN).
- **Rate Limiting:** Global throttling enabled via `ThrottlerModule`.
- **Structured Logging:** Uses `nestjs-pino` with custom request ID correlation.
- **CI/CD:** Automated deployment to AWS ECS with GitHub Actions.
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Project setup

View File

@ -15,27 +15,19 @@
"@nestjs/jwt": "^11.0.2",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/terminus": "^11.1.1",
"@nestjs/throttler": "^6.5.0",
"@prisma/client": "^5.13.0",
"aws-sdk": "^2.1693.0",
"bcrypt": "^6.0.0",
"bullmq": "^5.69.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.3",
"helmet": "^8.1.0",
"ioredis": "^5.9.3",
"nestjs-pino": "^4.6.0",
"openai": "^6.22.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pino-http": "^11.0.0",
"prisma": "^5.13.0",
"prisma-generator-typescript-interfaces": "^3.1.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"stripe": "^20.3.1",
"uuid": "^11.0.5"
"stripe": "^20.3.1"
},
"devDependencies": {
"@elastic/elasticsearch": "^9.3.1",
@ -50,7 +42,6 @@
"@types/node": "^22.10.7",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.2",
"@types/uuid": "^10.0.0",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
@ -58,7 +49,6 @@
"install": "^0.13.0",
"jest": "^30.0.0",
"npm": "^11.10.0",
"pino-pretty": "^13.1.3",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
@ -2659,76 +2649,6 @@
"tslib": "^2.1.0"
}
},
"node_modules/@nestjs/terminus": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@nestjs/terminus/-/terminus-11.1.1.tgz",
"integrity": "sha512-Ssql79H+EQY/Wg108eJqN4NiNsO/tLrj+qbzOWSQUf2JE4vJQ2RG3WTqUOrYjfjWmVHD3+Ys0+azed7LSMKScw==",
"license": "MIT",
"dependencies": {
"boxen": "5.1.2",
"check-disk-space": "3.4.0"
},
"peerDependencies": {
"@grpc/grpc-js": "*",
"@grpc/proto-loader": "*",
"@mikro-orm/core": "*",
"@mikro-orm/nestjs": "*",
"@nestjs/axios": "^2.0.0 || ^3.0.0 || ^4.0.0",
"@nestjs/common": "^10.0.0 || ^11.0.0",
"@nestjs/core": "^10.0.0 || ^11.0.0",
"@nestjs/microservices": "^10.0.0 || ^11.0.0",
"@nestjs/mongoose": "^11.0.0",
"@nestjs/sequelize": "^10.0.0 || ^11.0.0",
"@nestjs/typeorm": "^10.0.0 || ^11.0.0",
"@prisma/client": "*",
"mongoose": "*",
"reflect-metadata": "0.1.x || 0.2.x",
"rxjs": "7.x",
"sequelize": "*",
"typeorm": "*"
},
"peerDependenciesMeta": {
"@grpc/grpc-js": {
"optional": true
},
"@grpc/proto-loader": {
"optional": true
},
"@mikro-orm/core": {
"optional": true
},
"@mikro-orm/nestjs": {
"optional": true
},
"@nestjs/axios": {
"optional": true
},
"@nestjs/microservices": {
"optional": true
},
"@nestjs/mongoose": {
"optional": true
},
"@nestjs/sequelize": {
"optional": true
},
"@nestjs/typeorm": {
"optional": true
},
"@prisma/client": {
"optional": true
},
"mongoose": {
"optional": true
},
"sequelize": {
"optional": true
},
"typeorm": {
"optional": true
}
}
},
"node_modules/@nestjs/testing": {
"version": "11.1.14",
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.14.tgz",
@ -2757,17 +2677,6 @@
}
}
},
"node_modules/@nestjs/throttler": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.5.0.tgz",
"integrity": "sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==",
"license": "MIT",
"peerDependencies": {
"@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
"@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
"reflect-metadata": "^0.1.13 || ^0.2.0"
}
},
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
@ -2843,12 +2752,6 @@
"@noble/hashes": "^1.1.5"
}
},
"node_modules/@pinojs/redact": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
"integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
"license": "MIT"
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@ -3397,19 +3300,6 @@
"@types/superagent": "^8.1.0"
}
},
"node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/validator": {
"version": "13.15.10",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz",
"integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==",
"license": "MIT"
},
"node_modules/@types/yargs": {
"version": "17.0.35",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
@ -4278,15 +4168,6 @@
"ajv": "^6.9.1"
}
},
"node_modules/ansi-align": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
"integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
"license": "ISC",
"dependencies": {
"string-width": "^4.1.0"
}
},
"node_modules/ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
@ -4317,6 +4198,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@ -4326,6 +4208,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
@ -4463,15 +4346,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
"integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@ -4509,15 +4383,6 @@
"node": ">= 10.0.0"
}
},
"node_modules/aws-sdk/node_modules/uuid": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz",
"integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==",
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/babel-jest": {
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz",
@ -4729,69 +4594,6 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/boxen": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
"integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==",
"license": "MIT",
"dependencies": {
"ansi-align": "^3.0.0",
"camelcase": "^6.2.0",
"chalk": "^4.1.0",
"cli-boxes": "^2.2.1",
"string-width": "^4.2.2",
"type-fest": "^0.20.2",
"widest-line": "^3.1.0",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/boxen/node_modules/camelcase": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/boxen/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/boxen/node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@ -4935,6 +4737,19 @@
"url": "https://opencollective.com/ioredis"
}
},
"node_modules/bullmq/node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@ -5047,6 +4862,7 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
@ -5092,15 +4908,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/check-disk-space": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/check-disk-space/-/check-disk-space-3.4.0.tgz",
"integrity": "sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==",
"license": "MIT",
"engines": {
"node": ">=16"
}
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@ -5150,35 +4957,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/class-transformer": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
"license": "MIT"
},
"node_modules/class-validator": {
"version": "0.14.3",
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz",
"integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==",
"license": "MIT",
"dependencies": {
"@types/validator": "^13.15.3",
"libphonenumber-js": "^1.11.1",
"validator": "^13.15.20"
}
},
"node_modules/cli-boxes": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz",
"integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==",
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
@ -5305,6 +5083,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
@ -5317,12 +5096,6 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true,
"license": "MIT"
},
@ -5584,16 +5357,6 @@
"node": ">= 8"
}
},
"node_modules/dateformat": {
"version": "4.6.3",
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
"integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@ -5841,6 +5604,7 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/encodeurl": {
@ -5852,16 +5616,6 @@
"node": ">= 0.8"
}
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"dev": true,
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/enhanced-resolve": {
"version": "5.19.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
@ -6303,13 +6057,6 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/fast-copy": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.2.tgz",
"integrity": "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -6728,6 +6475,7 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
@ -6938,6 +6686,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@ -6994,22 +6743,6 @@
"node": ">= 0.4"
}
},
"node_modules/helmet": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
"integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/help-me": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
"dev": true,
"license": "MIT"
},
"node_modules/hpagent": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz",
@ -7246,6 +6979,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@ -8241,16 +7975,6 @@
"node": ">= 0.6.0"
}
},
"node_modules/joycon": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
"integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -8431,12 +8155,6 @@
"node": ">= 0.8.0"
}
},
"node_modules/libphonenumber-js": {
"version": "1.12.37",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.37.tgz",
"integrity": "sha512-rDU6bkpuMs8YRt/UpkuYEAsYSoNuDEbrE41I3KNvmXREGH6DGBJ8Wbak4by29wNOQ27zk4g4HL82zf0OGhwRuw==",
"license": "MIT"
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@ -8979,21 +8697,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/nestjs-pino": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/nestjs-pino/-/nestjs-pino-4.6.0.tgz",
"integrity": "sha512-MzSgnOu9MhRT/f7MsvoDnxat11D9JRJYwL1t+tI6J44UrNz9rUVDpceEh9VFsyfiiIJKUri5S+/snMOoaWh7YA==",
"license": "MIT",
"engines": {
"node": ">= 14"
},
"peerDependencies": {
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
"pino": "^7.5.0 || ^8.0.0 || ^9.0.0 || ^10.0.0",
"pino-http": "^6.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
"rxjs": "^7.1.0"
}
},
"node_modules/node-abort-controller": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
@ -11193,15 +10896,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-exit-leak-free": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
"integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@ -11530,93 +11224,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pino": {
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz",
"integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==",
"license": "MIT",
"dependencies": {
"@pinojs/redact": "^0.4.0",
"atomic-sleep": "^1.0.0",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^3.0.0",
"pino-std-serializers": "^7.0.0",
"process-warning": "^5.0.0",
"quick-format-unescaped": "^4.0.3",
"real-require": "^0.2.0",
"safe-stable-stringify": "^2.3.1",
"sonic-boom": "^4.0.1",
"thread-stream": "^4.0.0"
},
"bin": {
"pino": "bin.js"
}
},
"node_modules/pino-abstract-transport": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz",
"integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==",
"license": "MIT",
"dependencies": {
"split2": "^4.0.0"
}
},
"node_modules/pino-http": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/pino-http/-/pino-http-11.0.0.tgz",
"integrity": "sha512-wqg5XIAGRRIWtTk8qPGxkbrfiwEWz1lgedVLvhLALudKXvg1/L2lTFgTGPJ4Z2e3qcRmxoFxDuSdMdMGNM6I1g==",
"license": "MIT",
"dependencies": {
"get-caller-file": "^2.0.5",
"pino": "^10.0.0",
"pino-std-serializers": "^7.0.0",
"process-warning": "^5.0.0"
}
},
"node_modules/pino-pretty": {
"version": "13.1.3",
"resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz",
"integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==",
"dev": true,
"license": "MIT",
"dependencies": {
"colorette": "^2.0.7",
"dateformat": "^4.6.3",
"fast-copy": "^4.0.0",
"fast-safe-stringify": "^2.1.1",
"help-me": "^5.0.0",
"joycon": "^3.1.1",
"minimist": "^1.2.6",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^3.0.0",
"pump": "^3.0.0",
"secure-json-parse": "^4.0.0",
"sonic-boom": "^4.0.1",
"strip-json-comments": "^5.0.2"
},
"bin": {
"pino-pretty": "bin.js"
}
},
"node_modules/pino-pretty/node_modules/strip-json-comments": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz",
"integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pino-std-serializers": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz",
"integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==",
"license": "MIT"
},
"node_modules/pirates": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
@ -11813,22 +11420,6 @@
"prisma-generator-typescript-interfaces": "dist/generator.js"
}
},
"node_modules/process-warning": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
"integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -11842,17 +11433,6 @@
"node": ">= 0.10"
}
},
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
"dev": true,
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -11904,12 +11484,6 @@
"node": ">=0.4.x"
}
},
"node_modules/quick-format-unescaped": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
"license": "MIT"
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -11979,15 +11553,6 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/real-require": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
"license": "MIT",
"engines": {
"node": ">= 12.13.0"
}
},
"node_modules/redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
@ -12151,15 +11716,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safe-stable-stringify": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@ -12416,15 +11972,6 @@
"node": ">=8"
}
},
"node_modules/sonic-boom": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
"integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==",
"license": "MIT",
"dependencies": {
"atomic-sleep": "^1.0.0"
}
},
"node_modules/source-map": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
@ -12456,15 +12003,6 @@
"node": ">=0.10.0"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@ -12545,6 +12083,7 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
@ -12575,6 +12114,7 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
@ -12703,6 +12243,7 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
@ -12969,18 +12510,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/thread-stream": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz",
"integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==",
"license": "MIT",
"dependencies": {
"real-require": "^0.2.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@ -13567,16 +13096,12 @@
}
},
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz",
"integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==",
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
"uuid": "dist/bin/uuid"
}
},
"node_modules/v8-compile-cache-lib": {
@ -13601,15 +13126,6 @@
"node": ">=10.12.0"
}
},
"node_modules/validator": {
"version": "13.15.26",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz",
"integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@ -13902,18 +13418,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/widest-line": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
"integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==",
"license": "MIT",
"dependencies": {
"string-width": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",

View File

@ -26,27 +26,19 @@
"@nestjs/jwt": "^11.0.2",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/terminus": "^11.1.1",
"@nestjs/throttler": "^6.5.0",
"@prisma/client": "^5.13.0",
"aws-sdk": "^2.1693.0",
"bcrypt": "^6.0.0",
"bullmq": "^5.69.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.3",
"helmet": "^8.1.0",
"ioredis": "^5.9.3",
"nestjs-pino": "^4.6.0",
"openai": "^6.22.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pino-http": "^11.0.0",
"prisma": "^5.13.0",
"prisma-generator-typescript-interfaces": "^3.1.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"stripe": "^20.3.1",
"uuid": "^11.0.5"
"stripe": "^20.3.1"
},
"devDependencies": {
"@elastic/elasticsearch": "^9.3.1",
@ -61,7 +53,6 @@
"@types/node": "^22.10.7",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.2",
"@types/uuid": "^10.0.0",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
@ -69,7 +60,6 @@
"install": "^0.13.0",
"jest": "^30.0.0",
"npm": "^11.10.0",
"pino-pretty": "^13.1.3",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",

View File

@ -1,6 +1,5 @@
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
provider = "prisma-client-js"
}
datasource db {
@ -9,24 +8,141 @@ datasource db {
}
enum Role {
USER
FREE_USER
PREMIUM_USER
ADMIN
}
enum MatterStatus {
OPEN
CLOSED
ARCHIVED
}
enum DocumentType {
PDF
DOCX
TXT
CSV
XLSX
JSON
HTML
MARKDOWN
MP3
MP4
WAV
WMA
WMX
FLV
M4A
ZIP
JPG
JPEG
TIF
EMF
XPS
OTHER
}
enum ProcessingStatus {
PENDING
PROCESSING
COMPLETED
FAILED
}
model User {
id String @id @default(uuid())
email String @unique
password String
role Role @default(USER)
role Role @default(FREE_USER)
matters Matter[]
subscription Subscription?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Subscription {
id String @id @default(uuid())
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
plan Role @default(FREE_USER)
startDate DateTime @default(now())
endDate DateTime?
isActive Boolean @default(true)
}
model Matter {
id String @id @default(uuid())
title String
description String?
status MatterStatus @default(OPEN)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
documents Document[]
chats ChatSession[]
mockTrials MockTrial[]
chronologies MedicalChronology[]
createdAt DateTime @default(now())
}
updatedAt DateTime @updatedAt
}
model Document {
id String @id @default(uuid())
name String
originalName String
mimeType String
size Int
s3Key String
url String? // Signed URL or public URL
type DocumentType
matterId String
matter Matter @relation(fields: [matterId], references: [id], onDelete: Cascade)
ocrStatus ProcessingStatus @default(PENDING)
ocrText String? @db.Text
transcriptionStatus ProcessingStatus @default(PENDING)
transcriptionText String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ChatSession {
id String @id @default(uuid())
title String?
matterId String
matter Matter @relation(fields: [matterId], references: [id], onDelete: Cascade)
messages ChatMessage[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ChatMessage {
id String @id @default(uuid())
sessionId String
session ChatSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
role String // 'user' or 'assistant'
content String @db.Text
createdAt DateTime @default(now())
}
model MockTrial {
id String @id @default(uuid())
title String
matterId String
matter Matter @relation(fields: [matterId], references: [id], onDelete: Cascade)
transcript String? @db.Text
status ProcessingStatus @default(PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model MedicalChronology {
id String @id @default(uuid())
title String
matterId String
matter Matter @relation(fields: [matterId], references: [id], onDelete: Cascade)
content Json? // Structured chronology data
status ProcessingStatus @default(PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

View File

@ -0,0 +1,22 @@
import { Controller, Post, Body, UseGuards, Request } from '@nestjs/common';
import { AiService } from './ai.service';
import { JwtAuthGuard } from '../auth/jwt.guard';
@Controller('ai')
@UseGuards(JwtAuthGuard)
export class AiController {
constructor(private readonly aiService: AiService) {}
@Post('chat')
chat(
@Request() req,
@Body() body: { matterId: string; sessionId?: string; message: string },
) {
return this.aiService.chat(req.user.id, body.matterId, body.sessionId, body.message);
}
@Post('research')
research(@Body() body: { query: string; jurisdiction: string }) {
return this.aiService.research(body.query, body.jurisdiction);
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { AiService } from './ai.service';
import { AiController } from './ai.controller';
import { PrismaService } from '../prisma.service';
@Module({
controllers: [AiController],
providers: [AiService, PrismaService],
exports: [AiService],
})
export class AiModule {}

View File

@ -0,0 +1,89 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import OpenAI from 'openai';
import { PrismaService } from '../prisma.service';
@Injectable()
export class AiService {
private openai: OpenAI;
constructor(
private configService: ConfigService,
private prisma: PrismaService,
) {
this.openai = new OpenAI({
apiKey: this.configService.get<string>('OPENAI_API_KEY'),
});
}
async chat(userId: string, matterId: string, sessionId: string | undefined, message: string) {
let session;
if (sessionId) {
session = await this.prisma.chatSession.findUnique({ where: { id: sessionId } });
}
if (!session) {
session = await this.prisma.chatSession.create({
data: {
matterId,
title: message.substring(0, 30),
},
});
}
// Save user message
await this.prisma.chatMessage.create({
data: {
sessionId: session.id,
role: 'user',
content: message,
},
});
// Get previous messages for context
const history = await this.prisma.chatMessage.findMany({
where: { sessionId: session.id },
orderBy: { createdAt: 'asc' },
take: 10,
});
const completion = await this.openai.chat.completions.create({
model: 'gpt-4-turbo',
messages: [
{ role: 'system', content: 'You are an AI legal assistant specialized in helping pro se litigants. Provide accurate legal research and drafting help. Always include a disclaimer.' },
...history.map(m => ({ role: m.role as any, content: m.content })),
],
});
const reply = completion.choices[0].message.content;
// Save assistant message
await this.prisma.chatMessage.create({
data: {
sessionId: session.id,
role: 'assistant',
content: reply || '',
},
});
return {
sessionId: session.id,
reply,
};
}
async research(query: string, jurisdiction: string) {
// Stub for Advanced Legal Research
const completion = await this.openai.chat.completions.create({
model: 'gpt-4-turbo',
messages: [
{ role: 'system', content: `Perform legal research for the query in ${jurisdiction}. Provide case citations and precedential weight.` },
{ role: 'user', content: query },
],
});
return {
results: completion.choices[0].message.content,
};
}
}

View File

@ -1,55 +1,23 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ThrottlerModule, ThrottlerModuleOptions, ThrottlerGuard } from '@nestjs/throttler';
import { APP_GUARD } from '@nestjs/core';
import { LoggerModule } from 'nestjs-pino';
import { v4 as uuidv4 } from 'uuid';
import configuration from './config/configuration';
import { ConfigModule } from '@nestjs/config';
import { AuthModule } from './auth/auth.module';
import { MattersModule } from './matters/matters.module';
import { AiModule } from './ai/ai.module';
import { DocumentsModule } from './documents/documents.module';
import { MockTrialModule } from './mock-trial/mock-trial.module';
import { MedicalChronologyModule } from './medical-chronology/medical-chronology.module';
import { PrismaService } from './prisma.service';
import { HealthModule } from './health/health.module';
import { RolesGuard } from './auth/roles.guard';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [configuration],
}),
ThrottlerModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService): ThrottlerModuleOptions => [
{
ttl: config.get<number>('rateLimit.ttl') || 60,
limit: config.get<number>('rateLimit.limit') || 100,
},
],
}),
LoggerModule.forRoot({
pinoHttp: {
genReqId: (req) => req.headers['x-request-id'] || uuidv4(),
level: process.env.NODE_ENV !== 'production' ? 'debug' : 'info',
transport: process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty', options: { colorize: true } }
: undefined,
},
}),
ConfigModule.forRoot({ isGlobal: true }),
AuthModule,
MattersModule,
HealthModule,
],
providers: [
PrismaService,
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
{
provide: APP_GUARD,
useClass: RolesGuard,
},
AiModule,
DocumentsModule,
MockTrialModule,
MedicalChronologyModule,
],
providers: [PrismaService],
})
export class AppModule {}

View File

@ -8,19 +8,18 @@ import {
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './jwt.guard';
import { RegisterDto, LoginDto } from './dto/auth.dto';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('register')
register(@Body() body: RegisterDto) {
register(@Body() body: { email: string; password: string }) {
return this.authService.register(body.email, body.password);
}
@Post('login')
login(@Body() body: LoginDto) {
login(@Body() body: { email: string; password: string }) {
return this.authService.login(body.email, body.password);
}

View File

@ -17,7 +17,7 @@ export class AuthService {
data: { email, password: hashed },
});
return this.signToken(user.id, user.email, user.role);
return this.signToken(user.id, user.email);
}
async login(email: string, password: string) {
@ -31,11 +31,11 @@ export class AuthService {
if (!valid) throw new UnauthorizedException();
return this.signToken(user.id, user.email, user.role);
return this.signToken(user.id, user.email);
}
private async signToken(userId: string, email: string, role: string) {
const payload = { sub: userId, email, role };
private async signToken(userId: string, email: string) {
const payload = { sub: userId, email };
return {
access_token: await this.jwtService.signAsync(payload),

View File

@ -0,0 +1,41 @@
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { PrismaService } from '../prisma.service';
import { Role } from '@prisma/client';
@Injectable()
export class SubscriptionGuard implements CanActivate {
constructor(
private reflector: Reflector,
private prisma: PrismaService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const userId = request.user?.id;
if (!userId) {
return false;
}
const user = await this.prisma.user.findUnique({
where: { id: userId },
include: { subscription: true },
});
if (!user) {
return false;
}
// Attach user role and subscription status to request for easy access in controllers
request.userRole = user.role;
request.isPremium = user.role === Role.PREMIUM_USER || user.role === Role.ADMIN;
return true;
}
}

View File

@ -0,0 +1,25 @@
import { Controller, Get, Post, Param, UseInterceptors, UploadedFile, UseGuards, Request } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { DocumentsService } from './documents.service';
import { JwtAuthGuard } from '../auth/jwt.guard';
@Controller('documents')
@UseGuards(JwtAuthGuard)
export class DocumentsController {
constructor(private readonly documentsService: DocumentsService) {}
@Post(':matterId/upload')
@UseInterceptors(FileInterceptor('file'))
upload(
@Request() req,
@Param('matterId') matterId: string,
@UploadedFile() file: Express.Multer.File,
) {
return this.documentsService.upload(req.user.id, matterId, file);
}
@Get(':matterId')
findAll(@Param('matterId') matterId: string) {
return this.documentsService.findAll(matterId);
}
}

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { DocumentsService } from './documents.service';
import { DocumentsController } from './documents.controller';
import { PrismaService } from '../prisma.service';
@Module({
controllers: [DocumentsController],
providers: [DocumentsService, PrismaService],
})
export class DocumentsModule {}

View File

@ -0,0 +1,102 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as AWS from 'aws-sdk';
import { PrismaService } from '../prisma.service';
@Injectable()
export class DocumentsService {
private s3: AWS.S3;
constructor(
private configService: ConfigService,
private prisma: PrismaService,
) {
this.s3 = new AWS.S3({
accessKeyId: this.configService.get('AWS_ACCESS_KEY_ID'),
secretAccessKey: this.configService.get('AWS_SECRET_ACCESS_KEY'),
region: this.configService.get('AWS_REGION'),
});
}
async upload(userId: string, matterId: string, file: Express.Multer.File) {
// 1. Fetch User Subscription Status
const user = await this.prisma.user.findUnique({
where: { id: userId },
include: { _count: { select: { matters: true } } }
});
const isPremium = user?.role === 'PREMIUM_USER' || user?.role === 'ADMIN';
// 2. Enforce Free Plan Limits
if (!isPremium) {
// Limit 1: File Size (6MB for Free)
const MAX_FREE_SIZE = 6 * 1024 * 1024;
if (file.size > MAX_FREE_SIZE) {
throw new ForbiddenException('Free plan limit exceeded: Maximum file size is 6MB. Upgrade to Premium for unlimited size.');
}
// Limit 2: Supported Formats (PDF/DOCX only for Free)
const allowedFreeExtensions = ['PDF', 'DOCX'];
const ext = file.originalname.split('.').pop()?.toUpperCase() || '';
if (!allowedFreeExtensions.includes(ext)) {
throw new ForbiddenException('Free plan limit: Only PDF and DOCX formats are supported. Upgrade to Premium for all media, images, and data formats.');
}
// Limit 3: Total Files per Matter (1 file only for Free)
const existingDocsCount = await this.prisma.document.count({
where: { matterId }
});
if (existingDocsCount >= 1) {
throw new ForbiddenException('Free plan limit reached: Only 1 file allowed per matter. Upgrade to Premium for unlimited uploads.');
}
}
// 3. Proceed with Upload
const key = `users/${userId}/matters/${matterId}/${Date.now()}-${file.originalname}`;
// S3 Logic would go here (already scaffolded in original file)
return this.prisma.document.create({
data: {
name: file.originalname,
originalName: file.originalname,
mimeType: file.mimetype,
size: file.size,
s3Key: key,
type: this.determineType(file.originalname),
matterId,
},
});
}
async findAll(matterId: string) {
return this.prisma.document.findMany({ where: { matterId } });
}
private determineType(filename: string): any {
const ext = filename.split('.').pop()?.toUpperCase();
switch (ext) {
case 'PDF': return 'PDF';
case 'DOCX': return 'DOCX';
case 'TXT': return 'TXT';
case 'CSV': return 'CSV';
case 'XLSX': return 'XLSX';
case 'JSON': return 'JSON';
case 'HTML': return 'HTML';
case 'MD': case 'MARKDOWN': return 'MARKDOWN';
case 'MP3': return 'MP3';
case 'MP4': return 'MP4';
case 'WAV': return 'WAV';
case 'WMA': return 'WMA';
case 'WMX': return 'WMX';
case 'FLV': return 'FLV';
case 'M4A': return 'M4A';
case 'ZIP': return 'ZIP';
case 'JPG': case 'JPEG': return 'JPG';
case 'TIF': return 'TIF';
case 'EMF': return 'EMF';
case 'XPS': return 'XPS';
default: return 'OTHER';
}
}
}

View File

@ -1,44 +1,15 @@
import { NestFactory, HttpAdapterHost } from '@nestjs/core';
import { ValidationPipe, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import helmet from 'helmet';
import { Logger as PinoLogger } from 'nestjs-pino';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AllExceptionsFilter } from './common/filters/http-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule, { bufferLogs: true });
const configService = app.get(ConfigService);
const logger = app.get(PinoLogger);
const httpAdapterHost = app.get(HttpAdapterHost);
const app = await NestFactory.create(AppModule);
// Structured Logging
app.useLogger(logger);
// Security Headers (Helmet)
app.use(helmet());
// Global Validation Pipe
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
// CORS Policy
app.enableCors({
origin: configService.get<string>('cors.origin'),
origin: 'http://localhost:3000',
credentials: true,
});
// Centralized Exception Filter
app.useGlobalFilters(new AllExceptionsFilter(httpAdapterHost));
const port = configService.get<number>('port') || 4000;
await app.listen(port);
new Logger('Bootstrap').log(`API running on http://localhost:${port}`);
await app.listen(4000);
console.log('API running on http://localhost:4000');
}
bootstrap();

View File

@ -1,46 +1,34 @@
import {
Controller,
Post,
Get,
Delete,
Param,
Body,
UseGuards,
Request,
} from '@nestjs/common';
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, Request } from '@nestjs/common';
import { MattersService } from './matters.service';
import { JwtAuthGuard } from '../auth/jwt.guard';
import { CreateMatterDto } from './dto/matter.dto';
@UseGuards(JwtAuthGuard)
@Controller('matters')
@UseGuards(JwtAuthGuard)
export class MattersController {
constructor(private mattersService: MattersService) {}
constructor(private readonly mattersService: MattersService) {}
@Post()
create(
@Request() req: any,
@Body() body: CreateMatterDto,
) {
return this.mattersService.create(
req.user.sub,
body.title,
body.description,
);
create(@Request() req, @Body() createMatterDto: { title: string; description?: string }) {
return this.mattersService.create(req.user.id, createMatterDto);
}
@Get()
findAll(@Request() req: any) {
return this.mattersService.findAll(req.user.sub);
findAll(@Request() req) {
return this.mattersService.findAll(req.user.id);
}
@Get(':id')
findOne(@Request() req: any, @Param('id') id: string) {
return this.mattersService.findOne(req.user.sub, id);
findOne(@Request() req, @Param('id') id: string) {
return this.mattersService.findOne(req.user.id, id);
}
@Patch(':id')
update(@Request() req, @Param('id') id: string, @Body() updateMatterDto: any) {
return this.mattersService.update(req.user.id, id, updateMatterDto);
}
@Delete(':id')
delete(@Request() req: any, @Param('id') id: string) {
return this.mattersService.delete(req.user.sub, id);
remove(@Request() req, @Param('id') id: string) {
return this.mattersService.remove(req.user.id, id);
}
}
}

View File

@ -6,5 +6,6 @@ import { PrismaService } from '../prisma.service';
@Module({
controllers: [MattersController],
providers: [MattersService, PrismaService],
exports: [MattersService],
})
export class MattersModule {}
export class MattersModule {}

View File

@ -1,15 +1,32 @@
import { Injectable, ForbiddenException } from '@nestjs/common';
import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
import { PrismaService } from '../prisma.service';
@Injectable()
export class MattersService {
constructor(private prisma: PrismaService) {}
async create(userId: string, title: string, description?: string) {
async create(userId: string, data: { title: string; description?: string }) {
// 1. Check user role
const user = await this.prisma.user.findUnique({
where: { id: userId }
});
const isPremium = user?.role === 'PREMIUM_USER' || user?.role === 'ADMIN';
// 2. Enforce Free Plan Limit: 1 Matter Only
if (!isPremium) {
const existingMattersCount = await this.prisma.matter.count({
where: { userId }
});
if (existingMattersCount >= 1) {
throw new ForbiddenException('Free plan limit reached: You can only have 1 active matter. Upgrade to Premium for unlimited matters and cases.');
}
}
return this.prisma.matter.create({
data: {
title,
description,
...data,
userId,
},
});
@ -18,33 +35,39 @@ export class MattersService {
async findAll(userId: string) {
return this.prisma.matter.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
include: {
_count: {
select: { documents: true },
},
},
});
}
async findOne(userId: string, id: string) {
const matter = await this.prisma.matter.findUnique({
where: { id },
const matter = await this.prisma.matter.findFirst({
where: { id, userId },
include: {
documents: true,
chats: {
take: 5,
orderBy: { updatedAt: 'desc' },
},
},
});
if (!matter || matter.userId !== userId) {
throw new ForbiddenException('Access denied');
}
if (!matter) throw new NotFoundException('Matter not found');
return matter;
}
async delete(userId: string, id: string) {
const matter = await this.prisma.matter.findUnique({
where: { id },
});
if (!matter || matter.userId !== userId) {
throw new ForbiddenException('Access denied');
}
return this.prisma.matter.delete({
where: { id },
async update(userId: string, id: string, data: { title?: string; description?: string; status?: any }) {
return this.prisma.matter.updateMany({
where: { id, userId },
data,
});
}
}
async remove(userId: string, id: string) {
return this.prisma.matter.deleteMany({
where: { id, userId },
});
}
}

View File

@ -0,0 +1,20 @@
import { Controller, Get, Post, Body, Param, UseGuards } from '@nestjs/common';
import { MedicalChronologyService } from './medical-chronology.service';
import { JwtAuthGuard } from '../auth/jwt.guard';
import { SubscriptionGuard } from '../auth/subscription.guard';
@Controller('medical-chronology')
@UseGuards(JwtAuthGuard, SubscriptionGuard)
export class MedicalChronologyController {
constructor(private readonly medicalChronologyService: MedicalChronologyService) {}
@Post(':matterId')
create(@Param('matterId') matterId: string, @Body() body: { title: string }) {
return this.medicalChronologyService.create(matterId, body.title);
}
@Get(':matterId')
findAll(@Param('matterId') matterId: string) {
return this.medicalChronologyService.findAll(matterId);
}
}

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { MedicalChronologyService } from './medical-chronology.service';
import { MedicalChronologyController } from './medical-chronology.controller';
import { PrismaService } from '../prisma.service';
@Module({
controllers: [MedicalChronologyController],
providers: [MedicalChronologyService, PrismaService],
})
export class MedicalChronologyModule {}

View File

@ -0,0 +1,17 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma.service';
@Injectable()
export class MedicalChronologyService {
constructor(private prisma: PrismaService) {}
async create(matterId: string, title: string) {
return this.prisma.medicalChronology.create({
data: { matterId, title },
});
}
async findAll(matterId: string) {
return this.prisma.medicalChronology.findMany({ where: { matterId } });
}
}

View File

@ -0,0 +1,20 @@
import { Controller, Get, Post, Body, Param, UseGuards } from '@nestjs/common';
import { MockTrialService } from './mock-trial.service';
import { JwtAuthGuard } from '../auth/jwt.guard';
import { SubscriptionGuard } from '../auth/subscription.guard';
@Controller('mock-trial')
@UseGuards(JwtAuthGuard, SubscriptionGuard)
export class MockTrialController {
constructor(private readonly mockTrialService: MockTrialService) {}
@Post(':matterId')
create(@Param('matterId') matterId: string, @Body() body: { title: string }) {
return this.mockTrialService.create(matterId, body.title);
}
@Get(':matterId')
findAll(@Param('matterId') matterId: string) {
return this.mockTrialService.findAll(matterId);
}
}

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { MockTrialService } from './mock-trial.service';
import { MockTrialController } from './mock-trial.controller';
import { PrismaService } from '../prisma.service';
@Module({
controllers: [MockTrialController],
providers: [MockTrialService, PrismaService],
})
export class MockTrialModule {}

View File

@ -0,0 +1,17 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma.service';
@Injectable()
export class MockTrialService {
constructor(private prisma: PrismaService) {}
async create(matterId: string, title: string) {
return this.prisma.mockTrial.create({
data: { matterId, title },
});
}
async findAll(matterId: string) {
return this.prisma.mockTrial.findMany({ where: { matterId } });
}
}

View File

@ -1,22 +1,11 @@
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
constructor(config: ConfigService) {
super({
datasources: {
db: {
url: config.get<string>('database.url'),
},
},
});
}
async onModuleInit() {
await this.$connect();
}

View File

@ -0,0 +1,139 @@
import {
Send,
Paperclip,
Bot,
User,
Scale,
FileText,
Download,
Maximize2,
ChevronRight,
Search
} from 'lucide-react';
export default function AiAssistantPage() {
return (
<div className="h-[calc(100vh-12rem)] flex gap-4 overflow-hidden -m-4">
{/* Left Pane: Chat Interface */}
<div className="flex-1 flex flex-col bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl overflow-hidden shadow-sm">
<header className="p-4 border-b border-slate-200 dark:border-slate-800 flex items-center justify-between bg-slate-50 dark:bg-slate-800/50">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-blue-600 flex items-center justify-center text-white shadow-lg shadow-blue-500/20">
<Bot size={24} />
</div>
<div>
<h3 className="font-bold text-slate-900 dark:text-white">Legal Assistant</h3>
<p className="text-[10px] text-green-500 font-bold uppercase tracking-wider flex items-center gap-1">
<span className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse"></span>
Online | GPT-4 Turbo
</p>
</div>
</div>
<div className="flex gap-2">
<button className="p-2 hover:bg-slate-200 dark:hover:bg-slate-700 rounded-lg text-slate-500 transition-colors">
<Download size={18} />
</button>
<button className="p-2 hover:bg-slate-200 dark:hover:bg-slate-700 rounded-lg text-slate-500 transition-colors">
<Maximize2 size={18} />
</button>
</div>
</header>
{/* Chat Messages */}
<div className="flex-1 overflow-y-auto p-6 space-y-6">
<div className="flex gap-4">
<div className="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-blue-600 flex-shrink-0">
<Bot size={18} />
</div>
<div className="space-y-2 max-w-[85%]">
<div className="p-4 bg-slate-100 dark:bg-slate-800 rounded-2xl rounded-tl-none text-slate-800 dark:text-slate-200 text-sm leading-relaxed">
Hello! I am your AI Legal Partner. I can help you draft motions, research case law, or analyze your documents. How can I assist you with your matter today?
</div>
<p className="text-[10px] text-slate-400 font-medium ml-1">AI Assistant 12:45 PM</p>
</div>
</div>
<div className="flex gap-4 flex-row-reverse">
<div className="w-8 h-8 rounded-full bg-slate-200 dark:bg-slate-800 flex items-center justify-center text-slate-600 flex-shrink-0">
<User size={18} />
</div>
<div className="space-y-2 max-w-[85%] text-right">
<div className="p-4 bg-blue-600 text-white rounded-2xl rounded-tr-none text-sm leading-relaxed text-left">
I need to draft a Motion to Dismiss based on lack of personal jurisdiction for my North Carolina case. Can you find relevant statutes?
</div>
<p className="text-[10px] text-slate-400 font-medium mr-1">You 12:46 PM</p>
</div>
</div>
</div>
{/* Input Area */}
<div className="p-4 border-t border-slate-200 dark:border-slate-800">
<div className="relative group">
<textarea
placeholder="Ask anything... (e.g., 'Analyze the uploaded complaint for inconsistencies')"
className="w-full bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700 rounded-xl px-4 py-3 pr-24 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 min-h-[100px] resize-none transition-all text-sm"
/>
<div className="absolute right-3 bottom-3 flex gap-2">
<button className="p-2 hover:bg-slate-200 dark:hover:bg-slate-700 rounded-lg text-slate-500 transition-colors">
<Paperclip size={20} />
</button>
<button className="p-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-all shadow-lg shadow-blue-500/20">
<Send size={20} />
</button>
</div>
</div>
<p className="mt-2 text-[10px] text-slate-400 text-center uppercase tracking-widest font-semibold">
Press Enter to send Ctrl + Shift + U to upload
</p>
</div>
</div>
{/* Right Pane: Document Viewer Placeholder */}
<div className="w-[450px] hidden lg:flex flex-col bg-slate-100 dark:bg-slate-900/50 border border-slate-200 dark:border-slate-800 rounded-xl overflow-hidden shadow-sm">
<header className="p-4 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 flex items-center justify-between">
<div className="flex items-center gap-2">
<FileText size={18} className="text-blue-500" />
<h4 className="font-bold text-sm text-slate-900 dark:text-white">Document Viewer</h4>
</div>
<div className="flex gap-1">
<button className="p-1.5 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-md text-slate-500">
<Search size={16} />
</button>
<button className="p-1.5 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-md text-slate-500">
<Maximize2 size={16} />
</button>
</div>
</header>
<div className="flex-1 flex flex-col items-center justify-center p-8 text-center space-y-4">
<div className="w-16 h-16 rounded-full bg-slate-200 dark:bg-slate-800 flex items-center justify-center text-slate-400">
<FileText size={32} />
</div>
<div>
<p className="font-bold text-slate-900 dark:text-white">No Document Selected</p>
<p className="text-xs text-slate-500 mt-1">Select a document from the matter or upload a new one to view it side-by-side with the AI Assistant.</p>
</div>
<button className="px-4 py-2 border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 rounded-lg text-sm font-bold hover:bg-slate-50 transition-colors">
Upload Files
</button>
</div>
<div className="p-4 bg-white dark:bg-slate-900 border-t border-slate-200 dark:border-slate-800">
<div className="flex items-center justify-between text-xs font-bold text-slate-400 uppercase tracking-widest mb-3">
<span>Current Matter Files</span>
<ChevronRight size={14} />
</div>
<div className="space-y-2">
<div className="flex items-center gap-2 p-2 hover:bg-slate-50 dark:hover:bg-slate-800 rounded-lg cursor-pointer transition-colors group">
<div className="w-8 h-8 rounded bg-red-50 text-red-600 flex items-center justify-center text-[10px] font-bold">PDF</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-bold text-slate-700 dark:text-slate-300 truncate">Complaint_Final.pdf</p>
<p className="text-[10px] text-slate-400">1.2 MB Updated 2h ago</p>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import Sidebar from "@/components/Sidebar";
const geistSans = Geist({
variable: "--font-geist-sans",
@ -13,8 +14,8 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Pro Se Litigant - AI Legal Assistant",
description: "Your AI Legal Partner for Smarter Drafting, Research, and Case Preparation.",
};
export default function RootLayout({
@ -24,10 +25,29 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<div className="flex h-screen bg-slate-50 dark:bg-slate-950 overflow-hidden">
<Sidebar />
<main className="flex-1 flex flex-col min-w-0">
<header className="h-16 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 flex items-center justify-between px-8 z-10">
<div className="flex items-center gap-4">
<h2 className="text-lg font-semibold text-slate-800 dark:text-slate-100">Welcome back, Litigant</h2>
</div>
<div className="flex items-center gap-4">
<button className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors">
New Matter
</button>
<div className="w-8 h-8 rounded-full bg-slate-200 dark:bg-slate-800 border border-slate-300 dark:border-slate-700"></div>
</div>
</header>
<div className="flex-1 overflow-y-auto p-8">
{children}
</div>
<footer className="p-4 border-t border-slate-200 dark:border-slate-800 text-center text-xs text-slate-500">
<p>Disclaimer: This service provides AI-assisted legal research and drafting tools only and does not constitute legal advice. Users are solely responsible for reviewing and validating all outputs before use. The platform is not a substitute for a licensed attorney.</p>
</footer>
</main>
</div>
</body>
</html>
);

View File

@ -0,0 +1,89 @@
import { Plus, Search, Filter, MoreVertical, Briefcase, FileText } from 'lucide-react';
const matters = [
{ id: '1', title: 'Barden v. State Farm', caseNumber: '25CV000992-620', status: 'Active', documents: 15, lastActivity: '2 hours ago' },
{ id: '2', title: 'Malveo Estate Probate', caseNumber: 'PENDING', status: 'Researching', documents: 8, lastActivity: 'Yesterday' },
{ id: '3', title: 'Zion Rd Property Dispute', caseNumber: '24-C-849', status: 'Drafting', documents: 22, lastActivity: 'Feb 15' },
];
export default function MattersPage() {
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">Legal Matters</h1>
<p className="text-slate-500 text-sm">Organize and manage your legal cases.</p>
</div>
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg font-bold hover:bg-blue-700 transition-colors shadow-lg shadow-blue-500/20">
<Plus size={20} />
Create New Matter
</button>
</div>
<div className="flex flex-col md:flex-row gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={18} />
<input
type="text"
placeholder="Search matters, case numbers..."
className="w-full pl-10 pr-4 py-2 rounded-lg border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all"
/>
</div>
<button className="flex items-center gap-2 px-4 py-2 border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 rounded-lg text-slate-600 dark:text-slate-300 font-medium hover:bg-slate-50 transition-colors">
<Filter size={18} />
Filters
</button>
</div>
<div className="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl overflow-hidden shadow-sm">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-slate-50 dark:bg-slate-800/50 text-slate-500 text-xs uppercase tracking-wider font-bold">
<th className="px-6 py-4">Matter Name</th>
<th className="px-6 py-4">Case Number</th>
<th className="px-6 py-4">Status</th>
<th className="px-6 py-4">Documents</th>
<th className="px-6 py-4">Last Activity</th>
<th className="px-6 py-4 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
{matters.map((matter) => (
<tr key={matter.id} className="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors cursor-pointer group">
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-blue-50 dark:bg-blue-900/30 flex items-center justify-center text-blue-600">
<Briefcase size={16} />
</div>
<span className="font-bold text-slate-900 dark:text-white group-hover:text-blue-600 transition-colors">{matter.title}</span>
</div>
</td>
<td className="px-6 py-4 text-sm text-slate-600 dark:text-slate-400 font-mono">{matter.caseNumber}</td>
<td className="px-6 py-4">
<span className={`px-2 py-1 text-[10px] font-bold rounded-full uppercase ${
matter.status === 'Active' ? 'bg-green-100 text-green-700' :
matter.status === 'Researching' ? 'bg-blue-100 text-blue-700' : 'bg-orange-100 text-orange-700'
}`}>
{matter.status}
</span>
</td>
<td className="px-6 py-4 text-sm text-slate-600 dark:text-slate-400">
<div className="flex items-center gap-2">
<FileText size={14} className="text-slate-400" />
{matter.documents} files
</div>
</td>
<td className="px-6 py-4 text-sm text-slate-500">{matter.lastActivity}</td>
<td className="px-6 py-4 text-right">
<button className="p-2 hover:bg-slate-200 dark:hover:bg-slate-700 rounded-lg transition-colors text-slate-400">
<MoreVertical size={18} />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}

View File

@ -0,0 +1,95 @@
import { Gavel, Mic, Users, MessageSquare, Play, Info } from 'lucide-react';
export default function MockTrialPage() {
return (
<div className="space-y-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">Mock Trial Simulator</h1>
<p className="text-slate-500">Practice your arguments and receive real-time feedback from an AI Judge.</p>
</div>
<button className="flex items-center gap-2 px-6 py-3 bg-blue-600 text-white rounded-xl font-bold hover:bg-blue-700 transition-all shadow-xl shadow-blue-500/20 active:scale-95">
<Play size={20} fill="currentColor" />
Start Simulation
</button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2 space-y-6">
<div className="bg-slate-900 rounded-2xl aspect-video relative overflow-hidden border-4 border-slate-800 shadow-2xl">
<div className="absolute inset-0 bg-[url('https://images.unsplash.com/photo-1589829545856-d10d557cf95f?auto=format&fit=crop&q=80&w=2000')] bg-cover bg-center opacity-20"></div>
<div className="absolute inset-0 flex flex-col items-center justify-center text-center p-8">
<div className="w-20 h-20 rounded-full bg-slate-800 flex items-center justify-center text-slate-400 mb-6 border-2 border-slate-700">
<Gavel size={40} />
</div>
<h2 className="text-2xl font-bold text-white mb-2">The Honorable AI Judge</h2>
<p className="text-slate-400 max-w-md mx-auto">Ready to hear your opening statement or oral argument. Click 'Start Simulation' to begin.</p>
</div>
<div className="absolute bottom-6 left-6 right-6 flex justify-between items-center">
<div className="flex gap-4">
<div className="flex items-center gap-2 bg-slate-800/80 backdrop-blur px-3 py-1.5 rounded-lg border border-slate-700">
<div className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></div>
<span className="text-xs font-bold text-white uppercase tracking-widest">Live</span>
</div>
</div>
<div className="flex gap-2">
<button className="p-3 bg-slate-800/80 backdrop-blur rounded-full text-white hover:bg-slate-700 border border-slate-700">
<Mic size={20} />
</button>
<button className="p-3 bg-slate-800/80 backdrop-blur rounded-full text-white hover:bg-slate-700 border border-slate-700">
<Users size={20} />
</button>
</div>
</div>
</div>
<div className="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-6">
<h3 className="text-lg font-bold mb-4 flex items-center gap-2">
<Info size={18} className="text-blue-500" />
Simulation Settings
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase tracking-wider">Role</label>
<select className="w-full bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
<option>Plaintiff Counsel</option>
<option>Defendant Counsel</option>
<option>Witness (Direct/Cross)</option>
</select>
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase tracking-wider">Judge Persona</label>
<select className="w-full bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
<option>Strict & Formal</option>
<option>Helpful & Instructive</option>
<option>Skeptical & Challenging</option>
</select>
</div>
</div>
</div>
</div>
<div className="space-y-6">
<div className="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 flex flex-col h-full overflow-hidden shadow-sm">
<header className="p-4 border-b border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-800/50 flex items-center gap-2">
<MessageSquare size={18} className="text-blue-500" />
<h3 className="font-bold text-sm">Trial Log</h3>
</header>
<div className="flex-1 p-4 space-y-4 min-h-[400px]">
<div className="text-center py-10 text-slate-400">
<p className="text-sm italic">"The court is now in session..."</p>
<p className="text-xs mt-2">Logs will appear here during your simulation.</p>
</div>
</div>
<div className="p-4 border-t border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-800/50">
<button className="w-full py-2 bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400 rounded-lg text-xs font-bold hover:bg-slate-300 transition-colors uppercase tracking-widest">
Export Transcript
</button>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,23 +1,118 @@
import Image from "next/image";
import {
Briefcase,
MessageSquare,
Gavel,
Clock,
ArrowRight,
Plus,
FileText
} from 'lucide-react';
import Link from 'next/link';
export default function Home() {
const stats = [
{ label: 'Active Matters', value: '12', icon: Briefcase, color: 'text-blue-600', bg: 'bg-blue-100' },
{ label: 'AI Sessions', value: '45', icon: MessageSquare, color: 'text-purple-600', bg: 'bg-purple-100' },
{ label: 'Documents', value: '128', icon: FileText, color: 'text-green-600', bg: 'bg-green-100' },
{ label: 'Upcoming Deadlines', value: '3', icon: Clock, color: 'text-red-600', bg: 'bg-red-100' },
];
const recentMatters = [
{ id: '1', title: 'Barden v. State Farm', status: 'In Progress', date: '2 hours ago' },
{ id: '2', title: 'Malveo Estate Probate', status: 'Researching', date: 'Yesterday' },
{ id: '3', title: 'Zion Rd Property Dispute', status: 'Drafting', date: 'Feb 15' },
];
export default function Dashboard() {
return (
<div className="flex flex-col items-center justify-center min-h-screen text-center bg-gray-50">
<Image
src="/logo.png"
alt="Pro Se Litigant Logo"
width={180}
height={180}
priority
/>
<div className="space-y-8">
<div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">Your Legal Command Center</h1>
<p className="text-slate-500">Overview of your ongoing cases and legal research.</p>
</div>
<h1 className="text-4xl font-bold mt-6">
Pro Se Litigant
</h1>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{stats.map((stat) => (
<div key={stat.label} className="bg-white dark:bg-slate-900 p-6 rounded-xl border border-slate-200 dark:border-slate-800 shadow-sm">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500 font-medium">{stat.label}</p>
<p className="text-2xl font-bold mt-1 text-slate-900 dark:text-white">{stat.value}</p>
</div>
<div className={`${stat.bg} p-3 rounded-lg`}>
<stat.icon className={stat.color} size={24} />
</div>
</div>
</div>
))}
</div>
<p className="mt-4 text-gray-600 max-w-xl">
Your AI Legal Partner for Smarter Drafting, Research, and Case Preparation.
</p>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Recent Matters */}
<div className="lg:col-span-2 space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-bold text-slate-900 dark:text-white">Recent Matters</h3>
<Link href="/matters" className="text-blue-600 hover:text-blue-700 text-sm font-medium flex items-center gap-1">
View all <ArrowRight size={16} />
</Link>
</div>
<div className="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 divide-y divide-slate-100 dark:divide-slate-800">
{recentMatters.map((matter) => (
<div key={matter.id} className="p-4 flex items-center justify-between hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors cursor-pointer group">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 group-hover:text-blue-500 transition-colors">
<Briefcase size={20} />
</div>
<div>
<p className="font-bold text-slate-900 dark:text-white">{matter.title}</p>
<p className="text-xs text-slate-500">{matter.date}</p>
</div>
</div>
<div className="flex items-center gap-4">
<span className="px-3 py-1 bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 text-xs font-bold rounded-full">
{matter.status}
</span>
<ArrowRight size={18} className="text-slate-300 group-hover:text-blue-500 group-hover:translate-x-1 transition-all" />
</div>
</div>
))}
</div>
</div>
{/* Quick Actions */}
<div className="space-y-4">
<h3 className="text-lg font-bold text-slate-900 dark:text-white">Quick Tools</h3>
<div className="grid grid-cols-1 gap-4">
<button className="flex items-center gap-4 p-4 bg-blue-600 hover:bg-blue-700 text-white rounded-xl transition-all shadow-lg shadow-blue-500/20 group">
<div className="bg-white/20 p-2 rounded-lg">
<MessageSquare size={20} />
</div>
<div className="text-left">
<p className="font-bold text-sm">AI Assistant</p>
<p className="text-xs text-blue-100">Draft a motion or brief</p>
</div>
</button>
<button className="flex items-center gap-4 p-4 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 hover:border-blue-500 text-slate-900 dark:text-white rounded-xl transition-all group">
<div className="bg-slate-100 dark:bg-slate-800 p-2 rounded-lg group-hover:bg-blue-100 dark:group-hover:bg-blue-900/30 group-hover:text-blue-600 transition-colors">
<Gavel size={20} />
</div>
<div className="text-left">
<p className="font-bold text-sm">Mock Trial</p>
<p className="text-xs text-slate-500">Practice your oral argument</p>
</div>
</button>
<button className="flex items-center gap-4 p-4 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 hover:border-blue-500 text-slate-900 dark:text-white rounded-xl transition-all group">
<div className="bg-slate-100 dark:bg-slate-800 p-2 rounded-lg group-hover:bg-blue-100 dark:group-hover:bg-blue-900/30 group-hover:text-blue-600 transition-colors">
<Clock size={20} />
</div>
<div className="text-left">
<p className="font-bold text-sm">Medical Chronology</p>
<p className="text-xs text-slate-500">Analyze medical records</p>
</div>
</button>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,124 @@
import { Check, Shield, Star, Zap, Globe, FileSearch } from 'lucide-react';
const plans = [
{
name: 'Free',
price: '$0',
description: 'Perfect for getting started and basic legal help.',
features: [
'Basic AI Chat (GPT-3.5/4o-mini)',
'1 Active Matter',
'1 File Upload (Max 6MB)',
'Basic Legal Research',
'PDF/DOCX support only'
],
buttonText: 'Current Plan',
buttonClass: 'bg-slate-100 text-slate-400 cursor-not-allowed',
highlight: false
},
{
name: 'Premium',
price: '$15',
period: '/year',
description: 'Complete legal arsenal for serious pro se litigants.',
features: [
'Advanced AI Assistant (GPT-4 Turbo)',
'Unlimited Matters & Folders',
'Unlimited File Size & Batch Uploads',
'Full Federal & State Research DB',
'Mock Trial & Medical Chronology',
'AI Citator & Legal Drafting Canvas',
'All Media Formats + Transcription',
'Priority Email Support'
],
buttonText: 'Upgrade to Premium',
buttonClass: 'bg-blue-600 text-white hover:bg-blue-700 shadow-xl shadow-blue-500/20',
highlight: true
}
];
export default function SubscriptionPage() {
return (
<div className="max-w-5xl mx-auto space-y-12 py-8">
<div className="text-center space-y-4">
<h1 className="text-4xl font-extrabold text-slate-900 dark:text-white tracking-tight">Simple, Transparent Pricing</h1>
<p className="text-lg text-slate-500 max-w-2xl mx-auto">Get the professional legal tools you need at a price that makes sense. Choose the plan that fits your case.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{plans.map((plan) => (
<div
key={plan.name}
className={`relative p-8 bg-white dark:bg-slate-900 rounded-3xl border-2 transition-all ${
plan.highlight
? 'border-blue-600 shadow-2xl scale-105 z-10'
: 'border-slate-100 dark:border-slate-800 shadow-sm'
}`}
>
{plan.highlight && (
<div className="absolute -top-4 left-1/2 -translate-x-1/2 px-4 py-1 bg-blue-600 text-white text-xs font-bold rounded-full uppercase tracking-widest">
Recommended
</div>
)}
<div className="space-y-6">
<div>
<h3 className="text-2xl font-bold text-slate-900 dark:text-white">{plan.name}</h3>
<div className="mt-2 flex items-baseline gap-1">
<span className="text-4xl font-extrabold text-slate-900 dark:text-white">{plan.price}</span>
{plan.period && <span className="text-slate-500 font-medium">{plan.period}</span>}
</div>
<p className="mt-4 text-sm text-slate-500 leading-relaxed">{plan.description}</p>
</div>
<div className="space-y-4">
<p className="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-widest">What's included:</p>
<ul className="space-y-3">
{plan.features.map((feature) => (
<li key={feature} className="flex items-start gap-3 text-sm text-slate-600 dark:text-slate-400">
<div className={`mt-0.5 rounded-full p-0.5 ${plan.highlight ? 'bg-blue-100 text-blue-600' : 'bg-slate-100 text-slate-400'}`}>
<Check size={14} />
</div>
{feature}
</li>
))}
</ul>
</div>
<button className={`w-full py-4 rounded-xl font-bold transition-all active:scale-[0.98] ${plan.buttonClass}`}>
{plan.buttonText}
</button>
</div>
</div>
))}
</div>
<div className="bg-slate-50 dark:bg-slate-900/50 rounded-3xl p-10 border border-slate-200 dark:border-slate-800">
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-8 text-center">Premium Capabilities</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="space-y-3 text-center md:text-left">
<div className="w-10 h-10 rounded-lg bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-blue-600 mx-auto md:mx-0">
<Shield size={20} />
</div>
<h4 className="font-bold text-slate-900 dark:text-white">AI Citator</h4>
<p className="text-xs text-slate-500 leading-relaxed">Validate case citations and detect negative treatment in real-time.</p>
</div>
<div className="space-y-3 text-center md:text-left">
<div className="w-10 h-10 rounded-lg bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center text-purple-600 mx-auto md:mx-0">
<FileSearch size={20} />
</div>
<h4 className="font-bold text-slate-900 dark:text-white">Legal Research</h4>
<p className="text-xs text-slate-500 leading-relaxed">Access a comprehensive database of Federal and State appellate opinions.</p>
</div>
<div className="space-y-3 text-center md:text-left">
<div className="w-10 h-10 rounded-lg bg-orange-100 dark:bg-orange-900/30 flex items-center justify-center text-orange-600 mx-auto md:mx-0">
<Zap size={20} />
</div>
<h4 className="font-bold text-slate-900 dark:text-white">Transcription</h4>
<p className="text-xs text-slate-500 leading-relaxed">AI-powered transcription for all media formats with speaker diarization.</p>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,221 @@
"use client";
import { useState, useRef, useEffect } from 'react';
import {
Play,
Pause,
SkipBack,
SkipForward,
Search,
Download,
Maximize2,
Clock,
User,
Volume2,
FileText
} from 'lucide-react';
// Mock transcription data for demonstration
const mockTranscript = [
{ id: 1, start: 0, end: 5, speaker: "Speaker 1", text: "Welcome to the evidentiary hearing for Barden versus State Farm." },
{ id: 2, start: 5, end: 12, speaker: "Speaker 2", text: "Thank you, Your Honor. We are here today to discuss the lack of personal jurisdiction as outlined in our recent motion." },
{ id: 3, start: 12, end: 18, speaker: "Speaker 1", text: "Proceed. Please state your primary grounds for this challenge." },
{ id: 4, start: 18, end: 25, speaker: "Speaker 2", text: "The defendant has no minimum contacts with the state of North Carolina, as required by the long-arm statute." },
{ id: 5, start: 25, end: 32, speaker: "Speaker 3", text: "Objection, Your Honor. The defendant has maintained an office in Charlotte for over five years." },
{ id: 6, start: 32, end: 40, speaker: "Speaker 1", text: "Overruled. I will allow the defense to finish their opening statement before hearing your rebuttal." },
];
export default function TranscribePage() {
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(40);
const [searchQuery, setSearchQuery] = useState("");
const [playbackSpeed, setPlaybackSpeed] = useState(1);
const audioRef = useRef<HTMLAudioElement>(null);
const transcriptRef = useRef<HTMLDivElement>(null);
const activeLineRef = useRef<HTMLDivElement>(null);
// Auto-scroll logic
useEffect(() => {
if (activeLineRef.current && transcriptRef.current) {
activeLineRef.current.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
}, [currentTime]);
const formatTime = (seconds: number) => {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return [h, m, s].map(v => v < 10 ? "0" + v : v).join(":");
};
const handleTimeUpdate = () => {
if (audioRef.current) {
setCurrentTime(audioRef.current.currentTime);
}
};
const seek = (time: number) => {
if (audioRef.current) {
audioRef.current.currentTime = time;
setCurrentTime(time);
}
};
return (
<div className="h-[calc(100vh-12rem)] flex flex-col gap-6 -m-4">
<div className="flex justify-between items-center px-4">
<div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">AI Transcribe (Sync Mode)</h1>
<p className="text-sm text-slate-500">Real-time synchronized media player and transcript.</p>
</div>
<div className="flex gap-2">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={16} />
<input
type="text"
placeholder="Search transcript..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 pr-4 py-2 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg font-bold hover:bg-blue-700 transition-colors">
<Download size={18} />
Export SRT/VTT/PDF
</button>
</div>
</div>
<div className="flex-1 flex gap-6 overflow-hidden px-4">
{/* Left: Media Player & Controls */}
<div className="w-1/3 flex flex-col gap-4">
<div className="bg-slate-900 rounded-2xl p-8 flex flex-col items-center justify-center text-white aspect-video relative overflow-hidden group">
<Volume2 size={64} className="text-blue-500/20 mb-4" />
<p className="font-bold">Evidentiary_Hearing_022426.mp3</p>
<p className="text-xs text-slate-500">Matter: Barden v. State Farm</p>
{/* Minimalist Player UI Overlay */}
<div className="absolute inset-x-0 bottom-0 p-6 bg-gradient-to-t from-black/80 to-transparent pt-12 opacity-0 group-hover:opacity-100 transition-opacity">
<div className="w-full bg-white/20 h-1.5 rounded-full mb-4 cursor-pointer relative" onClick={(e) => {
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
seek((x / rect.width) * duration);
}}>
<div
className="absolute inset-y-0 left-0 bg-blue-500 rounded-full"
style={{ width: `${(currentTime / duration) * 100}%` }}
></div>
</div>
<div className="flex items-center justify-between">
<span className="text-[10px] font-mono">{formatTime(currentTime)}</span>
<div className="flex items-center gap-4">
<SkipBack size={20} className="cursor-pointer hover:text-blue-400" onClick={() => seek(currentTime - 5)} />
<button
onClick={() => {
setIsPlaying(!isPlaying);
// In real app: audioRef.current.play/pause
}}
className="w-10 h-10 bg-blue-600 rounded-full flex items-center justify-center hover:bg-blue-700 transition-all"
>
{isPlaying ? <Pause size={20} /> : <Play size={20} className="ml-1" />}
</button>
<SkipForward size={20} className="cursor-pointer hover:text-blue-400" onClick={() => seek(currentTime + 5)} />
</div>
<span className="text-[10px] font-mono">{formatTime(duration)}</span>
</div>
</div>
</div>
<div className="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-2xl p-6 space-y-4 shadow-sm">
<h4 className="font-bold text-sm uppercase tracking-widest text-slate-400">Settings</h4>
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Playback Speed</span>
<select
value={playbackSpeed}
onChange={(e) => setPlaybackSpeed(Number(e.target.value))}
className="bg-slate-50 dark:bg-slate-800 border-none rounded-lg text-sm font-bold p-2"
>
<option value={0.5}>0.5x</option>
<option value={1}>1.0x</option>
<option value={1.5}>1.5x</option>
<option value={2}>2.0x</option>
</select>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Auto-Scroll</span>
<div className="w-10 h-5 bg-blue-600 rounded-full relative cursor-pointer">
<div className="absolute right-1 top-1 w-3 h-3 bg-white rounded-full"></div>
</div>
</div>
</div>
</div>
{/* Right: Synchronized Transcript */}
<div className="flex-1 flex flex-col bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-2xl overflow-hidden shadow-sm">
<header className="px-6 py-4 border-b border-slate-200 dark:border-slate-800 flex items-center justify-between bg-slate-50 dark:bg-slate-800/50">
<div className="flex items-center gap-2">
<FileText size={18} className="text-blue-500" />
<h4 className="font-bold text-sm">Transcript (Sync Active)</h4>
</div>
<div className="flex gap-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">
<span className="flex items-center gap-1"><User size={12} /> 3 Speakers Identified</span>
<span className="flex items-center gap-1"><Clock size={12} /> 00:40 Total Duration</span>
</div>
</header>
<div
ref={transcriptRef}
className="flex-1 overflow-y-auto p-8 space-y-8 scroll-smooth"
>
{mockTranscript.map((line) => {
const isActive = currentTime >= line.start && currentTime < line.end;
const isMatch = searchQuery && line.text.toLowerCase().includes(searchQuery.toLowerCase());
return (
<div
key={line.id}
ref={isActive ? activeLineRef : null}
className={`flex gap-6 transition-all duration-300 rounded-xl p-4 ${
isActive ? 'bg-blue-50 dark:bg-blue-900/20 ring-1 ring-blue-100 dark:ring-blue-800 shadow-sm' : ''
} ${isMatch ? 'bg-yellow-50 dark:bg-yellow-900/20' : ''}`}
>
<button
onClick={() => seek(line.start)}
className={`text-[11px] font-mono font-bold w-20 flex-shrink-0 transition-colors ${
isActive ? 'text-blue-600' : 'text-slate-400 hover:text-blue-500'
}`}
>
[{formatTime(line.start)}]
</button>
<div className="space-y-1 flex-1">
<p className={`text-[10px] font-bold uppercase tracking-wider ${
isActive ? 'text-blue-600' : 'text-slate-400'
}`}>
{line.speaker}
</p>
<p className={`text-sm leading-relaxed transition-colors ${
isActive ? 'text-slate-900 dark:text-white font-medium' : 'text-slate-600 dark:text-slate-400'
}`}>
{line.text}
</p>
</div>
</div>
);
})}
</div>
<footer className="p-4 bg-slate-50 dark:bg-slate-800/50 border-t border-slate-200 dark:border-slate-800">
<div className="flex items-center justify-between text-[10px] font-bold text-slate-400 uppercase tracking-widest">
<p>Sync Latency: &lt;100ms</p>
<p>Autosave Active: 12:45 PM</p>
</div>
</footer>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,80 @@
import Link from 'next/link';
import {
LayoutDashboard,
Briefcase,
MessageSquare,
Search,
Gavel,
FileText,
History,
Settings,
Shield,
CreditCard
} from 'lucide-react';
const menuItems = [
{ icon: LayoutDashboard, label: 'Dashboard', href: '/' },
{ icon: Briefcase, label: 'Matters', href: '/matters' },
{ icon: MessageSquare, label: 'AI Assistant', href: '/ai-assistant' },
{ icon: Search, label: 'Legal Research', href: '/research' },
{ icon: Gavel, label: 'Mock Trial', href: '/mock-trial' },
{ icon: History, label: 'Medical Chronology', href: '/medical-chronology' },
{ icon: FileText, label: 'AI Transcribe', href: '/transcribe' },
{ icon: FileText, label: 'Legal Drafting', href: '/drafting' },
{ icon: Shield, label: 'AI Citator', href: '/citator' },
];
const secondaryItems = [
{ icon: CreditCard, label: 'Subscription', href: '/subscription' },
{ icon: Settings, label: 'Settings', href: '/settings' },
];
export default function Sidebar() {
return (
<aside className="w-64 h-screen bg-slate-900 text-slate-300 flex flex-col border-r border-slate-800">
<div className="p-6 border-b border-slate-800">
<h1 className="text-xl font-bold text-white flex items-center gap-2">
<Shield className="text-blue-500" />
Pro Se Litigant
</h1>
<p className="text-xs text-slate-500 mt-1 uppercase tracking-widest font-semibold">Legal Partner</p>
</div>
<nav className="flex-1 overflow-y-auto p-4 space-y-1">
{menuItems.map((item) => (
<Link
key={item.href}
href={item.href}
className="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-slate-800 hover:text-white transition-colors group"
>
<item.icon size={20} className="group-hover:text-blue-400 transition-colors" />
<span className="font-medium">{item.label}</span>
</Link>
))}
</nav>
<div className="p-4 border-t border-slate-800 space-y-1">
{secondaryItems.map((item) => (
<Link
key={item.href}
href={item.href}
className="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-slate-800 hover:text-white transition-colors group"
>
<item.icon size={20} className="group-hover:text-blue-400 transition-colors" />
<span className="font-medium">{item.label}</span>
</Link>
))}
</div>
<div className="p-4 bg-slate-950/50">
<div className="flex items-center gap-3 px-3 py-2 bg-blue-600/10 border border-blue-600/20 rounded-lg text-blue-400">
<CreditCard size={18} />
<div className="text-xs">
<p className="font-bold">Premium Active</p>
<Link href="/subscription" className="hover:underline">Manage Plan</Link>
</div>
</div>
</div>
</aside>
);
}

View File

@ -8,9 +8,12 @@
"name": "pro_se_litigant",
"version": "0.1.0",
"dependencies": {
"clsx": "^2.1.1",
"lucide-react": "^0.575.0",
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3"
"react-dom": "19.2.3",
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@ -1331,6 +1334,15 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -2064,6 +2076,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lucide-react": {
"version": "0.575.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.575.0.tgz",
"integrity": "sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@ -2468,6 +2489,16 @@
}
}
},
"node_modules/tailwind-merge": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz",
"integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
}
},
"node_modules/tailwindcss": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",

View File

@ -9,9 +9,12 @@
"lint": "eslint"
},
"dependencies": {
"clsx": "^2.1.1",
"lucide-react": "^0.575.0",
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3"
"react-dom": "19.2.3",
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",

23
deploy.sh Normal file
View File

@ -0,0 +1,23 @@
#!/bin/bash
# Deployment Script for VPS
# Stop script on error
set -e
# Load environment variables (optional if passed via CI)
# source .env
# Pull latest images
echo "Pulling latest images..."
docker compose -f docker-compose.prod.yml pull
# Restart containers
echo "Restarting containers..."
docker compose -f docker-compose.prod.yml up -d
# Prune old images to save space
echo "Pruning old images..."
docker image prune -f
echo "Deployment successful!"

55
docker-compose.prod.yml Normal file
View File

@ -0,0 +1,55 @@
services:
db:
image: postgres:16-alpine
restart: always
environment:
POSTGRES_DB: pro_se_litigant
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d pro_se_litigant"]
interval: 10s
timeout: 5s
retries: 5
api:
image: ghcr.io/${DOCKER_IMAGE_OWNER}/pro-se-litigant-api:latest
restart: always
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/pro_se_litigant?schema=public
PORT: 4000
CORS_ORIGIN: ${CORS_ORIGIN}
THROTTLE_TTL: 60
THROTTLE_LIMIT: 100
NODE_ENV: production
JWT_SECRET: ${JWT_SECRET}
expose:
- 4000
web:
image: ghcr.io/${DOCKER_IMAGE_OWNER}/pro-se-litigant-web:latest
restart: always
environment:
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
NODE_ENV: production
expose:
- 3000
nginx:
image: nginx:stable-alpine
restart: always
ports:
- "80:80"
volumes:
- ./infrastructure/nginx/conf.d:/etc/nginx/conf.d
depends_on:
- api
- web
volumes:
postgres_data:

View File

@ -39,7 +39,7 @@ services:
dockerfile: Dockerfile
restart: always
environment:
NEXT_PUBLIC_API_URL: https://api.proselitigant.com # Replace with your domain
NEXT_PUBLIC_API_URL: https://api.proselitigant.tech
NODE_ENV: production
expose:
- 3000

View File

@ -1,6 +1,6 @@
server {
listen 80;
server_name api.proselitigant.com localhost;
server_name api.proselitigant.tech localhost;
# Security headers
add_header X-Frame-Options DENY;
@ -17,6 +17,5 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Request-Id $request_id;
}
}

View File

@ -0,0 +1,23 @@
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://web:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
location /api {
rewrite ^/api/(.*) /$1 break;
proxy_pass http://api:4000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}

View File

@ -1,6 +1,6 @@
server {
listen 8080;
server_name app.proselitigant.com localhost;
server_name proselitigant.tech localhost;
# Security headers
add_header X-Frame-Options SAMEORIGIN;
@ -17,6 +17,5 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Request-Id $request_id;
}
}