Final production build for proselitigant.tech
This commit is contained in:
parent
e3c7ff9758
commit
3e51d6222f
@ -2,11 +2,10 @@
|
|||||||
POSTGRES_USER=myuser
|
POSTGRES_USER=myuser
|
||||||
POSTGRES_PASSWORD=mypassword
|
POSTGRES_PASSWORD=mypassword
|
||||||
POSTGRES_DB=pro_se_litigant
|
POSTGRES_DB=pro_se_litigant
|
||||||
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?schema=public
|
|
||||||
|
|
||||||
# API Configuration
|
# API Configuration
|
||||||
PORT=4000
|
PORT=4000
|
||||||
CORS_ORIGIN=https://app.proselitigant.com
|
CORS_ORIGIN=https://proselitigant.tech
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
|
|
||||||
# Rate Limiting
|
# Rate Limiting
|
||||||
|
|||||||
BIN
.github/README.md
vendored
Normal file
BIN
.github/README.md
vendored
Normal file
Binary file not shown.
BIN
.github/workflows/README.md
vendored
Normal file
BIN
.github/workflows/README.md
vendored
Normal file
Binary file not shown.
56
.github/workflows/deploy-api.yml
vendored
Normal file
56
.github/workflows/deploy-api.yml
vendored
Normal 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
25
.github/workflows/deploy-vps.yml
vendored
Normal 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
56
.github/workflows/deploy-web.yml
vendored
Normal 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
37
.gitignore
vendored
Normal 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
|
||||||
63
README.md
63
README.md
@ -1,2 +1,61 @@
|
|||||||
# pro-se-litigant
|
# Pro Se Litigant - Deployment Guide
|
||||||
Pro Se Litigant Respository
|
|
||||||
|
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.
|
||||||
|
|||||||
@ -23,13 +23,7 @@
|
|||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository for the Pro Se Litigant API.
|
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
## Project setup
|
## Project setup
|
||||||
|
|
||||||
|
|||||||
554
apps/api/package-lock.json
generated
554
apps/api/package-lock.json
generated
@ -15,27 +15,19 @@
|
|||||||
"@nestjs/jwt": "^11.0.2",
|
"@nestjs/jwt": "^11.0.2",
|
||||||
"@nestjs/passport": "^11.0.5",
|
"@nestjs/passport": "^11.0.5",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@nestjs/terminus": "^11.1.1",
|
|
||||||
"@nestjs/throttler": "^6.5.0",
|
|
||||||
"@prisma/client": "^5.13.0",
|
"@prisma/client": "^5.13.0",
|
||||||
"aws-sdk": "^2.1693.0",
|
"aws-sdk": "^2.1693.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"bullmq": "^5.69.3",
|
"bullmq": "^5.69.3",
|
||||||
"class-transformer": "^0.5.1",
|
|
||||||
"class-validator": "^0.14.3",
|
|
||||||
"helmet": "^8.1.0",
|
|
||||||
"ioredis": "^5.9.3",
|
"ioredis": "^5.9.3",
|
||||||
"nestjs-pino": "^4.6.0",
|
|
||||||
"openai": "^6.22.0",
|
"openai": "^6.22.0",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"pino-http": "^11.0.0",
|
|
||||||
"prisma": "^5.13.0",
|
"prisma": "^5.13.0",
|
||||||
"prisma-generator-typescript-interfaces": "^3.1.0",
|
"prisma-generator-typescript-interfaces": "^3.1.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"stripe": "^20.3.1",
|
"stripe": "^20.3.1"
|
||||||
"uuid": "^11.0.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@elastic/elasticsearch": "^9.3.1",
|
"@elastic/elasticsearch": "^9.3.1",
|
||||||
@ -50,7 +42,6 @@
|
|||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"@types/uuid": "^10.0.0",
|
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-prettier": "^5.2.2",
|
"eslint-plugin-prettier": "^5.2.2",
|
||||||
@ -58,7 +49,6 @@
|
|||||||
"install": "^0.13.0",
|
"install": "^0.13.0",
|
||||||
"jest": "^30.0.0",
|
"jest": "^30.0.0",
|
||||||
"npm": "^11.10.0",
|
"npm": "^11.10.0",
|
||||||
"pino-pretty": "^13.1.3",
|
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
@ -2659,76 +2649,6 @@
|
|||||||
"tslib": "^2.1.0"
|
"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": {
|
"node_modules/@nestjs/testing": {
|
||||||
"version": "11.1.14",
|
"version": "11.1.14",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.14.tgz",
|
"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": {
|
"node_modules/@noble/hashes": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||||
@ -2843,12 +2752,6 @@
|
|||||||
"@noble/hashes": "^1.1.5"
|
"@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": {
|
"node_modules/@pkgjs/parseargs": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
@ -3397,19 +3300,6 @@
|
|||||||
"@types/superagent": "^8.1.0"
|
"@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": {
|
"node_modules/@types/yargs": {
|
||||||
"version": "17.0.35",
|
"version": "17.0.35",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
|
||||||
@ -4278,15 +4168,6 @@
|
|||||||
"ajv": "^6.9.1"
|
"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": {
|
"node_modules/ansi-colors": {
|
||||||
"version": "4.1.3",
|
"version": "4.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
||||||
@ -4317,6 +4198,7 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@ -4326,6 +4208,7 @@
|
|||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"color-convert": "^2.0.1"
|
"color-convert": "^2.0.1"
|
||||||
@ -4463,15 +4346,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/available-typed-arrays": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||||
@ -4509,15 +4383,6 @@
|
|||||||
"node": ">= 10.0.0"
|
"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": {
|
"node_modules/babel-jest": {
|
||||||
"version": "30.2.0",
|
"version": "30.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz",
|
||||||
@ -4729,69 +4594,6 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"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": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.12",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
@ -4935,6 +4737,19 @@
|
|||||||
"url": "https://opencollective.com/ioredis"
|
"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": {
|
"node_modules/busboy": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||||
@ -5047,6 +4862,7 @@
|
|||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-styles": "^4.1.0",
|
"ansi-styles": "^4.1.0",
|
||||||
@ -5092,15 +4908,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/chokidar": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
@ -5150,35 +4957,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/cli-cursor": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||||
@ -5305,6 +5083,7 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"color-name": "~1.1.4"
|
"color-name": "~1.1.4"
|
||||||
@ -5317,12 +5096,6 @@
|
|||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"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,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@ -5584,16 +5357,6 @@
|
|||||||
"node": ">= 8"
|
"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": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@ -5841,6 +5604,7 @@
|
|||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/encodeurl": {
|
"node_modules/encodeurl": {
|
||||||
@ -5852,16 +5616,6 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.19.0",
|
"version": "5.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
|
||||||
@ -6303,13 +6057,6 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"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": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@ -6728,6 +6475,7 @@
|
|||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "6.* || 8.* || >= 10.*"
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
@ -6938,6 +6686,7 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@ -6994,22 +6743,6 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/hpagent": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz",
|
||||||
@ -7246,6 +6979,7 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@ -8241,16 +7975,6 @@
|
|||||||
"node": ">= 0.6.0"
|
"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": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@ -8431,12 +8155,6 @@
|
|||||||
"node": ">= 0.8.0"
|
"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": {
|
"node_modules/lines-and-columns": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||||
@ -8979,21 +8697,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/node-abort-controller": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
|
"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"
|
"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": {
|
"node_modules/on-finished": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
@ -11530,93 +11224,6 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"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": {
|
"node_modules/pirates": {
|
||||||
"version": "4.0.7",
|
"version": "4.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
|
||||||
@ -11813,22 +11420,6 @@
|
|||||||
"prisma-generator-typescript-interfaces": "dist/generator.js"
|
"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": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
@ -11842,17 +11433,6 @@
|
|||||||
"node": ">= 0.10"
|
"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": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
@ -11904,12 +11484,6 @@
|
|||||||
"node": ">=0.4.x"
|
"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": {
|
"node_modules/randombytes": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||||
@ -11979,15 +11553,6 @@
|
|||||||
"url": "https://paulmillr.com/funding/"
|
"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": {
|
"node_modules/redis-errors": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
||||||
@ -12151,15 +11716,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
@ -12416,15 +11972,6 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/source-map": {
|
||||||
"version": "0.7.4",
|
"version": "0.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
|
||||||
@ -12456,15 +12003,6 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/sprintf-js": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||||
@ -12545,6 +12083,7 @@
|
|||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"emoji-regex": "^8.0.0",
|
"emoji-regex": "^8.0.0",
|
||||||
@ -12575,6 +12114,7 @@
|
|||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^5.0.1"
|
"ansi-regex": "^5.0.1"
|
||||||
@ -12703,6 +12243,7 @@
|
|||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-flag": "^4.0.0"
|
"has-flag": "^4.0.0"
|
||||||
@ -12969,18 +12510,6 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"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": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.15",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
@ -13567,16 +13096,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uuid": {
|
"node_modules/uuid": {
|
||||||
"version": "11.1.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz",
|
||||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
"integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==",
|
||||||
"funding": [
|
|
||||||
"https://github.com/sponsors/broofa",
|
|
||||||
"https://github.com/sponsors/ctavan"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"uuid": "dist/esm/bin/uuid"
|
"uuid": "dist/bin/uuid"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/v8-compile-cache-lib": {
|
"node_modules/v8-compile-cache-lib": {
|
||||||
@ -13601,15 +13126,6 @@
|
|||||||
"node": ">=10.12.0"
|
"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": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
@ -13902,18 +13418,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||||
|
|||||||
@ -26,27 +26,19 @@
|
|||||||
"@nestjs/jwt": "^11.0.2",
|
"@nestjs/jwt": "^11.0.2",
|
||||||
"@nestjs/passport": "^11.0.5",
|
"@nestjs/passport": "^11.0.5",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@nestjs/terminus": "^11.1.1",
|
|
||||||
"@nestjs/throttler": "^6.5.0",
|
|
||||||
"@prisma/client": "^5.13.0",
|
"@prisma/client": "^5.13.0",
|
||||||
"aws-sdk": "^2.1693.0",
|
"aws-sdk": "^2.1693.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"bullmq": "^5.69.3",
|
"bullmq": "^5.69.3",
|
||||||
"class-transformer": "^0.5.1",
|
|
||||||
"class-validator": "^0.14.3",
|
|
||||||
"helmet": "^8.1.0",
|
|
||||||
"ioredis": "^5.9.3",
|
"ioredis": "^5.9.3",
|
||||||
"nestjs-pino": "^4.6.0",
|
|
||||||
"openai": "^6.22.0",
|
"openai": "^6.22.0",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"pino-http": "^11.0.0",
|
|
||||||
"prisma": "^5.13.0",
|
"prisma": "^5.13.0",
|
||||||
"prisma-generator-typescript-interfaces": "^3.1.0",
|
"prisma-generator-typescript-interfaces": "^3.1.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"stripe": "^20.3.1",
|
"stripe": "^20.3.1"
|
||||||
"uuid": "^11.0.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@elastic/elasticsearch": "^9.3.1",
|
"@elastic/elasticsearch": "^9.3.1",
|
||||||
@ -61,7 +53,6 @@
|
|||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"@types/uuid": "^10.0.0",
|
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-prettier": "^5.2.2",
|
"eslint-plugin-prettier": "^5.2.2",
|
||||||
@ -69,7 +60,6 @@
|
|||||||
"install": "^0.13.0",
|
"install": "^0.13.0",
|
||||||
"jest": "^30.0.0",
|
"jest": "^30.0.0",
|
||||||
"npm": "^11.10.0",
|
"npm": "^11.10.0",
|
||||||
"pino-pretty": "^13.1.3",
|
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
@ -9,24 +8,141 @@ datasource db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum Role {
|
enum Role {
|
||||||
USER
|
FREE_USER
|
||||||
|
PREMIUM_USER
|
||||||
ADMIN
|
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 {
|
model User {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
email String @unique
|
email String @unique
|
||||||
password String
|
password String
|
||||||
role Role @default(USER)
|
role Role @default(FREE_USER)
|
||||||
matters Matter[]
|
matters Matter[]
|
||||||
|
subscription Subscription?
|
||||||
createdAt DateTime @default(now())
|
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 {
|
model Matter {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
title String
|
title String
|
||||||
description String?
|
description String?
|
||||||
|
status MatterStatus @default(OPEN)
|
||||||
userId String
|
userId String
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
documents Document[]
|
||||||
|
chats ChatSession[]
|
||||||
|
mockTrials MockTrial[]
|
||||||
|
chronologies MedicalChronology[]
|
||||||
createdAt DateTime @default(now())
|
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
|
||||||
|
}
|
||||||
|
|||||||
22
apps/api/src/ai/ai.controller.ts
Normal file
22
apps/api/src/ai/ai.controller.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
apps/api/src/ai/ai.module.ts
Normal file
11
apps/api/src/ai/ai.module.ts
Normal 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 {}
|
||||||
89
apps/api/src/ai/ai.service.ts
Normal file
89
apps/api/src/ai/ai.service.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,55 +1,23 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule } 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 { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { MattersModule } from './matters/matters.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 { PrismaService } from './prisma.service';
|
||||||
import { HealthModule } from './health/health.module';
|
|
||||||
import { RolesGuard } from './auth/roles.guard';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
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,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
AuthModule,
|
AuthModule,
|
||||||
MattersModule,
|
MattersModule,
|
||||||
HealthModule,
|
AiModule,
|
||||||
],
|
DocumentsModule,
|
||||||
providers: [
|
MockTrialModule,
|
||||||
PrismaService,
|
MedicalChronologyModule,
|
||||||
{
|
|
||||||
provide: APP_GUARD,
|
|
||||||
useClass: ThrottlerGuard,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: APP_GUARD,
|
|
||||||
useClass: RolesGuard,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
|
providers: [PrismaService],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@ -8,19 +8,18 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { JwtAuthGuard } from './jwt.guard';
|
import { JwtAuthGuard } from './jwt.guard';
|
||||||
import { RegisterDto, LoginDto } from './dto/auth.dto';
|
|
||||||
|
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(private authService: AuthService) {}
|
constructor(private authService: AuthService) {}
|
||||||
|
|
||||||
@Post('register')
|
@Post('register')
|
||||||
register(@Body() body: RegisterDto) {
|
register(@Body() body: { email: string; password: string }) {
|
||||||
return this.authService.register(body.email, body.password);
|
return this.authService.register(body.email, body.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('login')
|
@Post('login')
|
||||||
login(@Body() body: LoginDto) {
|
login(@Body() body: { email: string; password: string }) {
|
||||||
return this.authService.login(body.email, body.password);
|
return this.authService.login(body.email, body.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@ export class AuthService {
|
|||||||
data: { email, password: hashed },
|
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) {
|
async login(email: string, password: string) {
|
||||||
@ -31,11 +31,11 @@ export class AuthService {
|
|||||||
|
|
||||||
if (!valid) throw new UnauthorizedException();
|
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) {
|
private async signToken(userId: string, email: string) {
|
||||||
const payload = { sub: userId, email, role };
|
const payload = { sub: userId, email };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
access_token: await this.jwtService.signAsync(payload),
|
access_token: await this.jwtService.signAsync(payload),
|
||||||
|
|||||||
41
apps/api/src/auth/subscription.guard.ts
Normal file
41
apps/api/src/auth/subscription.guard.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
apps/api/src/documents/documents.controller.ts
Normal file
25
apps/api/src/documents/documents.controller.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
apps/api/src/documents/documents.module.ts
Normal file
10
apps/api/src/documents/documents.module.ts
Normal 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 {}
|
||||||
102
apps/api/src/documents/documents.service.ts
Normal file
102
apps/api/src/documents/documents.service.ts
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,44 +1,15 @@
|
|||||||
import { NestFactory, HttpAdapterHost } from '@nestjs/core';
|
import { NestFactory } 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 { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { AllExceptionsFilter } from './common/filters/http-exception.filter';
|
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule, { bufferLogs: true });
|
const app = await NestFactory.create(AppModule);
|
||||||
const configService = app.get(ConfigService);
|
|
||||||
const logger = app.get(PinoLogger);
|
|
||||||
const httpAdapterHost = app.get(HttpAdapterHost);
|
|
||||||
|
|
||||||
// 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({
|
app.enableCors({
|
||||||
origin: configService.get<string>('cors.origin'),
|
origin: 'http://localhost:3000',
|
||||||
credentials: true,
|
credentials: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Centralized Exception Filter
|
await app.listen(4000);
|
||||||
app.useGlobalFilters(new AllExceptionsFilter(httpAdapterHost));
|
console.log('API running on http://localhost:4000');
|
||||||
|
|
||||||
const port = configService.get<number>('port') || 4000;
|
|
||||||
await app.listen(port);
|
|
||||||
|
|
||||||
new Logger('Bootstrap').log(`API running on http://localhost:${port}`);
|
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
@ -1,46 +1,34 @@
|
|||||||
import {
|
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, Request } from '@nestjs/common';
|
||||||
Controller,
|
|
||||||
Post,
|
|
||||||
Get,
|
|
||||||
Delete,
|
|
||||||
Param,
|
|
||||||
Body,
|
|
||||||
UseGuards,
|
|
||||||
Request,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { MattersService } from './matters.service';
|
import { MattersService } from './matters.service';
|
||||||
import { JwtAuthGuard } from '../auth/jwt.guard';
|
import { JwtAuthGuard } from '../auth/jwt.guard';
|
||||||
import { CreateMatterDto } from './dto/matter.dto';
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@Controller('matters')
|
@Controller('matters')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
export class MattersController {
|
export class MattersController {
|
||||||
constructor(private mattersService: MattersService) {}
|
constructor(private readonly mattersService: MattersService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
create(
|
create(@Request() req, @Body() createMatterDto: { title: string; description?: string }) {
|
||||||
@Request() req: any,
|
return this.mattersService.create(req.user.id, createMatterDto);
|
||||||
@Body() body: CreateMatterDto,
|
|
||||||
) {
|
|
||||||
return this.mattersService.create(
|
|
||||||
req.user.sub,
|
|
||||||
body.title,
|
|
||||||
body.description,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
findAll(@Request() req: any) {
|
findAll(@Request() req) {
|
||||||
return this.mattersService.findAll(req.user.sub);
|
return this.mattersService.findAll(req.user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
findOne(@Request() req: any, @Param('id') id: string) {
|
findOne(@Request() req, @Param('id') id: string) {
|
||||||
return this.mattersService.findOne(req.user.sub, id);
|
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(':id')
|
||||||
delete(@Request() req: any, @Param('id') id: string) {
|
remove(@Request() req, @Param('id') id: string) {
|
||||||
return this.mattersService.delete(req.user.sub, id);
|
return this.mattersService.remove(req.user.id, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,5 +6,6 @@ import { PrismaService } from '../prisma.service';
|
|||||||
@Module({
|
@Module({
|
||||||
controllers: [MattersController],
|
controllers: [MattersController],
|
||||||
providers: [MattersService, PrismaService],
|
providers: [MattersService, PrismaService],
|
||||||
|
exports: [MattersService],
|
||||||
})
|
})
|
||||||
export class MattersModule {}
|
export class MattersModule {}
|
||||||
|
|||||||
@ -1,15 +1,32 @@
|
|||||||
import { Injectable, ForbiddenException } from '@nestjs/common';
|
import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma.service';
|
import { PrismaService } from '../prisma.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MattersService {
|
export class MattersService {
|
||||||
constructor(private prisma: PrismaService) {}
|
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({
|
return this.prisma.matter.create({
|
||||||
data: {
|
data: {
|
||||||
title,
|
...data,
|
||||||
description,
|
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -18,33 +35,39 @@ export class MattersService {
|
|||||||
async findAll(userId: string) {
|
async findAll(userId: string) {
|
||||||
return this.prisma.matter.findMany({
|
return this.prisma.matter.findMany({
|
||||||
where: { userId },
|
where: { userId },
|
||||||
orderBy: { createdAt: 'desc' },
|
include: {
|
||||||
|
_count: {
|
||||||
|
select: { documents: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(userId: string, id: string) {
|
async findOne(userId: string, id: string) {
|
||||||
const matter = await this.prisma.matter.findUnique({
|
const matter = await this.prisma.matter.findFirst({
|
||||||
where: { id },
|
where: { id, userId },
|
||||||
|
include: {
|
||||||
|
documents: true,
|
||||||
|
chats: {
|
||||||
|
take: 5,
|
||||||
|
orderBy: { updatedAt: 'desc' },
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
if (!matter) throw new NotFoundException('Matter not found');
|
||||||
if (!matter || matter.userId !== userId) {
|
|
||||||
throw new ForbiddenException('Access denied');
|
|
||||||
}
|
|
||||||
|
|
||||||
return matter;
|
return matter;
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(userId: string, id: string) {
|
async update(userId: string, id: string, data: { title?: string; description?: string; status?: any }) {
|
||||||
const matter = await this.prisma.matter.findUnique({
|
return this.prisma.matter.updateMany({
|
||||||
where: { id },
|
where: { id, userId },
|
||||||
});
|
data,
|
||||||
|
|
||||||
if (!matter || matter.userId !== userId) {
|
|
||||||
throw new ForbiddenException('Access denied');
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.prisma.matter.delete({
|
|
||||||
where: { id },
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
async remove(userId: string, id: string) {
|
||||||
|
return this.prisma.matter.deleteMany({
|
||||||
|
where: { id, userId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
apps/api/src/medical-chronology/medical-chronology.module.ts
Normal file
10
apps/api/src/medical-chronology/medical-chronology.module.ts
Normal 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 {}
|
||||||
@ -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 } });
|
||||||
|
}
|
||||||
|
}
|
||||||
20
apps/api/src/mock-trial/mock-trial.controller.ts
Normal file
20
apps/api/src/mock-trial/mock-trial.controller.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
apps/api/src/mock-trial/mock-trial.module.ts
Normal file
10
apps/api/src/mock-trial/mock-trial.module.ts
Normal 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 {}
|
||||||
17
apps/api/src/mock-trial/mock-trial.service.ts
Normal file
17
apps/api/src/mock-trial/mock-trial.service.ts
Normal 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 } });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,22 +1,11 @@
|
|||||||
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PrismaService
|
export class PrismaService
|
||||||
extends PrismaClient
|
extends PrismaClient
|
||||||
implements OnModuleInit, OnModuleDestroy
|
implements OnModuleInit, OnModuleDestroy
|
||||||
{
|
{
|
||||||
constructor(config: ConfigService) {
|
|
||||||
super({
|
|
||||||
datasources: {
|
|
||||||
db: {
|
|
||||||
url: config.get<string>('database.url'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
await this.$connect();
|
await this.$connect();
|
||||||
}
|
}
|
||||||
|
|||||||
139
apps/web/app/ai-assistant/page.tsx
Normal file
139
apps/web/app/ai-assistant/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import Sidebar from "@/components/Sidebar";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@ -13,8 +14,8 @@ const geistMono = Geist_Mono({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "Pro Se Litigant - AI Legal Assistant",
|
||||||
description: "Generated by create next app",
|
description: "Your AI Legal Partner for Smarter Drafting, Research, and Case Preparation.",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@ -24,10 +25,29 @@ export default function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body
|
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
<div className="flex h-screen bg-slate-50 dark:bg-slate-950 overflow-hidden">
|
||||||
>
|
<Sidebar />
|
||||||
{children}
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
89
apps/web/app/matters/page.tsx
Normal file
89
apps/web/app/matters/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
95
apps/web/app/mock-trial/page.tsx
Normal file
95
apps/web/app/mock-trial/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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 (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen text-center bg-gray-50">
|
<div className="space-y-8">
|
||||||
<Image
|
<div>
|
||||||
src="/logo.png"
|
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">Your Legal Command Center</h1>
|
||||||
alt="Pro Se Litigant Logo"
|
<p className="text-slate-500">Overview of your ongoing cases and legal research.</p>
|
||||||
width={180}
|
</div>
|
||||||
height={180}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
|
|
||||||
<h1 className="text-4xl font-bold mt-6">
|
{/* Stats Grid */}
|
||||||
Pro Se Litigant
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
</h1>
|
{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">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
Your AI Legal Partner for Smarter Drafting, Research, and Case Preparation.
|
{/* Recent Matters */}
|
||||||
</p>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
124
apps/web/app/subscription/page.tsx
Normal file
124
apps/web/app/subscription/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
221
apps/web/app/transcribe/page.tsx
Normal file
221
apps/web/app/transcribe/page.tsx
Normal 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: <100ms</p>
|
||||||
|
<p>Autosave Active: 12:45 PM</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
80
apps/web/components/Sidebar.tsx
Normal file
80
apps/web/components/Sidebar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
apps/web/package-lock.json
generated
33
apps/web/package-lock.json
generated
@ -8,9 +8,12 @@
|
|||||||
"name": "pro_se_litigant",
|
"name": "pro_se_litigant",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.575.0",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3",
|
||||||
|
"tailwind-merge": "^3.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
@ -1331,6 +1334,15 @@
|
|||||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@ -2064,6 +2076,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"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": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.1.18",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||||
|
|||||||
@ -9,9 +9,12 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.575.0",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3",
|
||||||
|
"tailwind-merge": "^3.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
|||||||
23
deploy.sh
Normal file
23
deploy.sh
Normal 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
55
docker-compose.prod.yml
Normal 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:
|
||||||
@ -39,7 +39,7 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
NEXT_PUBLIC_API_URL: https://api.proselitigant.com # Replace with your domain
|
NEXT_PUBLIC_API_URL: https://api.proselitigant.tech
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
expose:
|
expose:
|
||||||
- 3000
|
- 3000
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name api.proselitigant.com localhost;
|
server_name api.proselitigant.tech localhost;
|
||||||
|
|
||||||
# Security headers
|
# Security headers
|
||||||
add_header X-Frame-Options DENY;
|
add_header X-Frame-Options DENY;
|
||||||
@ -17,6 +17,5 @@ server {
|
|||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_set_header X-Request-Id $request_id;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
infrastructure/nginx/conf.d/default.conf
Normal file
23
infrastructure/nginx/conf.d/default.conf
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
server {
|
server {
|
||||||
listen 8080;
|
listen 8080;
|
||||||
server_name app.proselitigant.com localhost;
|
server_name proselitigant.tech localhost;
|
||||||
|
|
||||||
# Security headers
|
# Security headers
|
||||||
add_header X-Frame-Options SAMEORIGIN;
|
add_header X-Frame-Options SAMEORIGIN;
|
||||||
@ -17,6 +17,5 @@ server {
|
|||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_set_header X-Request-Id $request_id;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user