Readme
This commit is contained in:
14
.dockerignore
Normal file
14
.dockerignore
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
Dockerfile
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
pnpm-debug.log
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
blockchain/artifacts
|
||||||
|
blockchain/cache
|
||||||
|
blockchain/typechain-types
|
||||||
|
blockchain/node_modules
|
||||||
29
Dockerfile
Normal file
29
Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
|
|
||||||
|
FROM node:20-alpine AS base
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
FROM base AS deps
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
FROM base AS builder
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
RUN npx prisma generate
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM base AS runner
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=3000
|
||||||
|
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
COPY --from=builder /app/.next/standalone ./
|
||||||
|
COPY --from=builder /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node", "server.js"]
|
||||||
247
README.md
247
README.md
@@ -1,110 +1,215 @@
|
|||||||
# 🔗 LexiChain: Professional BFSI Document Intelligence Platform
|
# LexiChain
|
||||||
|
|
||||||
> **Status**: Production-Ready PFE Project
|
<p align="center">
|
||||||
> **Target Audience**: Banking, Financial Services, and Insurance (BFSI) Institutions
|
<img src="public/LexiChain.png" alt="LexiChain logo" width="180" />
|
||||||
> **Key Innovation**: Hybrid integration of Generative AI (Gemini) and Ethereum Blockchain (DocumentRegistry)
|
</p>
|
||||||
|
|
||||||
---
|
<p align="center">
|
||||||
|
BFSI document intelligence platform that combines AI-assisted analysis with blockchain-backed verification.
|
||||||
|
</p>
|
||||||
|
|
||||||
## 🏛️ 1. Project Vision & Mission
|
<p align="center">
|
||||||
|
<img src="https://img.shields.io/badge/Next.js-16-000000?logo=nextdotjs&logoColor=white" alt="Next.js badge" />
|
||||||
|
<img src="https://img.shields.io/badge/React-19-61DAFB?logo=react&logoColor=black" alt="React badge" />
|
||||||
|
<img src="https://img.shields.io/badge/Hardhat-Smart%20Contracts-FFF100?logo=ethereum&logoColor=black" alt="Hardhat badge" />
|
||||||
|
<img src="https://img.shields.io/badge/Prisma-Database%20Layer-2D3748?logo=prisma&logoColor=white" alt="Prisma badge" />
|
||||||
|
<img src="https://img.shields.io/badge/Clerk-Auth%20Ready-3B82F6?logo=clerk&logoColor=white" alt="Clerk badge" />
|
||||||
|
</p>
|
||||||
|
|
||||||
**LexiChain** is a next-generation platform designed to bridge the gap between complex legal documentation and user-centric transparency. In the traditional BFSI sector, contracts are often "black boxes"—static PDFs that are hard to understand and easy to misplace.
|
LexiChain is a team-ready platform for uploading documents, extracting important information, verifying integrity, and tracking records through a polished enterprise interface. It combines a modern Next.js frontend, a Solidity/Hardhat blockchain module, PostgreSQL with Prisma, and Clerk authentication.
|
||||||
|
|
||||||
LexiChain transforms these static documents into **dynamic, searchable, and cryptographically secured assets**. Our mission is to automate document analysis while providing an immutable "Digital Notary" service that guarantees trust between financial institutions and their clients.
|
## Overview
|
||||||
|
|
||||||
---
|
LexiChain is designed for banking, financial services, and insurance workflows where documents must be understandable, traceable, and trustworthy.
|
||||||
|
|
||||||
## 📉 2. Market Problem & Opportunity
|
It addresses four core needs:
|
||||||
|
|
||||||
The project addresses several critical "pain points" in the current financial landscape:
|
- converting dense documents into structured, reviewable insights
|
||||||
|
- reducing repetitive validation and manual review work
|
||||||
|
- preserving tamper-evident proof for uploaded files
|
||||||
|
- giving end users a clearer and more professional contract experience
|
||||||
|
|
||||||
1. **The Transparency Gap**: Clients often sign contracts without understanding specific exclusion clauses or renewal dates.
|
## Product Highlights
|
||||||
2. **Operational Friction**: Insurance agents spend thousands of hours manually checking PDFs for compliance and signature presence.
|
|
||||||
3. **The Integrity Risk**: In legal disputes, proving that a specific version of a document was the one actually signed can be difficult and expensive.
|
|
||||||
4. **Information Overload**: Users are overwhelmed by the volume of fine print in modern banking.
|
|
||||||
|
|
||||||
---
|
- AI document analysis powered by Gemini
|
||||||
|
- OCR-based ingestion for digital and scanned documents
|
||||||
|
- automated extraction of key contract details
|
||||||
|
- chat-style retrieval over document content
|
||||||
|
- blockchain proof-of-existence for uploaded files
|
||||||
|
- document verification against on-chain hashes
|
||||||
|
- secure authentication and per-user access control
|
||||||
|
- dashboard views for contracts, contacts, and claims workflows
|
||||||
|
- responsive UI with a dark, premium visual style
|
||||||
|
|
||||||
## 🚀 3. Core Feature Deep-Dive
|
## Visual Preview
|
||||||
|
|
||||||
### 🧠 A. The AI "Analyst" Module (Intelligence Layer)
|
The screenshots in [public/screens](public/screens) are intentionally included to make the README feel like a product page and to help new contributors understand the interface quickly.
|
||||||
|
|
||||||
LexiChain uses **Google Gemini 2.0 Flash** to act as a virtual legal analyst.
|
| Landing page | Dashboard |
|
||||||
|
| ------------------------------------------------ | ----------------------------------------------- |
|
||||||
|
|  |  |
|
||||||
|
|
||||||
- **Smart Ingestion**: Uses high-fidelity OCR to read digital PDFs and scanned images.
|
| Login | Register |
|
||||||
- **Automated Extraction**: Identifies 15+ key data points (Amount, Interest Rate, Parties, Expiration Dates, Clauses) instantly.
|
| ---------------------------------------------- | ---------------------------------------------------- |
|
||||||
- **RAG (Retrieval-Augmented Generation)**: The most advanced part of the AI. It breaks the contract into "Semantic Chunks" and stores them in a vector index. This allows the user to **Chat with their Contract** in natural language.
|
|  |  |
|
||||||
- **Intelligent Validation**: The AI automatically flags missing signatures, inconsistent dates, or high-risk clauses before the document is finalized.
|
|
||||||
|
|
||||||
### ⛓️ B. The Blockchain "Notary" Module (Security Layer)
|
| Contracts |
|
||||||
|
| ---------------------------------------------------- |
|
||||||
|
|  |
|
||||||
|
|
||||||
LexiChain uses a private/public hybrid blockchain strategy to ensure **Non-Repudiation**.
|
## Architecture
|
||||||
|
|
||||||
- **Proof of Existence (PoE)**: We generate a SHA-256 hash (digital fingerprint) for every file. This hash is sent to a **Solidity Smart Contract** on the Ethereum network.
|
LexiChain follows a practical feature-oriented structure:
|
||||||
- **Immutable Timestamping**: The blockchain records exactly _when_ the document was uploaded. This cannot be changed by any administrator, providing a "Golden Record."
|
|
||||||
- **Metadata Leakage Prevention**: We only store the **Hash** on-chain. No personal data (names, amounts) ever touches the public blockchain, ensuring 100% GDPR compliance.
|
|
||||||
- **The Explorer**: A built-in "Verification Panel" that allows any auditor to verify a file's integrity by comparing its current hash with the on-chain record.
|
|
||||||
|
|
||||||
---
|
- [app](app) contains the Next.js app router, layouts, and pages
|
||||||
|
- [components](components) contains shared UI and layout building blocks
|
||||||
|
- [features](features) contains feature-level business logic
|
||||||
|
- [hooks](hooks) contains reusable client hooks
|
||||||
|
- [lib](lib) contains shared utilities and helpers
|
||||||
|
- [prisma](prisma) contains database schema files
|
||||||
|
- [blockchain](blockchain) contains the Hardhat project and smart contracts
|
||||||
|
- [public/screens](public/screens) contains screenshots and visual assets
|
||||||
|
|
||||||
## 🏗️ 4. Technical Architecture
|
The solution is split across three main concerns:
|
||||||
|
|
||||||
### **Architecture Pattern: Feature-Sliced Design (FSD)**
|
| Layer | Purpose |
|
||||||
|
| ---------- | ----------------------------------------------------- |
|
||||||
|
| Frontend | UI, dashboards, forms, and user interactions |
|
||||||
|
| Backend | Server actions and API routes for business workflows |
|
||||||
|
| Blockchain | Smart contract registration and document verification |
|
||||||
|
|
||||||
The project follows **FSD principles**, which is a modern architectural pattern for scaling large applications.
|
## Tech Stack
|
||||||
|
|
||||||
- **Layers**: App, Pages, Features, Entities, Shared.
|
| Area | Stack |
|
||||||
- **Benefits**: Decouples the Blockchain logic from the AI logic, making the system highly maintainable and ready for enterprise scaling.
|
| -------------- | ------------------------------------ |
|
||||||
|
| Frontend | Next.js 16, React 19, Tailwind CSS |
|
||||||
|
| Backend | Next.js server actions, API routes |
|
||||||
|
| AI | Google Gemini |
|
||||||
|
| Blockchain | Solidity, Hardhat, Ethers.js |
|
||||||
|
| Database | PostgreSQL, Prisma |
|
||||||
|
| Authentication | Clerk |
|
||||||
|
| UI Components | Radix UI, shadcn/ui-style primitives |
|
||||||
|
|
||||||
### **The Stack**
|
## Prerequisites
|
||||||
|
|
||||||
| Layer | Technology | Rationale |
|
- Node.js 20 or later
|
||||||
| :------------- | :----------------------- | :-------------------------------------------------------------------------------------------------- |
|
- npm
|
||||||
| **Frontend** | Next.js 15 (React) | High performance, SEO-friendly, and supports React Server Components. |
|
- PostgreSQL database
|
||||||
| **Backend** | Server Actions (Next.js) | Allows for secure, server-side blockchain signing and API communication without a separate backend.
|
- a local or remote blockchain endpoint for development and deployment
|
||||||
| **LLM** | Gemini | Unbeatable speed and context window size for long legal documents. |
|
- environment variables for Clerk, Gemini, database, and blockchain access
|
||||||
| **Blockchain** | Solidity / Hardhat | Ethereum-compatible smart contracts for industry-standard security. |
|
|
||||||
| **Database** | PostgreSQL + Prisma | Robust relational storage for user data and contract metadata. |
|
|
||||||
| **Identity** | Clerk | Enterprise-grade security for user authentication and session management. |
|
|
||||||
|
|
||||||
---
|
## Environment Variables
|
||||||
|
|
||||||
## 🔒 5. Security & Privacy Philosophy
|
The exact values depend on your deployment target, but the project expects environment configuration for the following areas:
|
||||||
|
|
||||||
LexiChain is built with a **"Security by Design"** approach:
|
| Area | Typical Variables |
|
||||||
|
| -------------- | ---------------------------------------------- |
|
||||||
|
| App | `NODE_ENV`, app URL settings |
|
||||||
|
| Database | `DATABASE_URL` |
|
||||||
|
| Authentication | Clerk publishable and secret keys |
|
||||||
|
| AI | Gemini API key or equivalent model credentials |
|
||||||
|
| Blockchain | RPC URL, contract address, private key |
|
||||||
|
|
||||||
- **Hashing vs. Storage**: We never store the actual document on the blockchain. The blockchain only holds the "Proof," while the "Content" remains in encrypted cloud storage.
|
Keep secrets in environment variables and never commit them to the repository.
|
||||||
- **Server-Side Signing**: Users don't need a crypto wallet (MetaMask). Our backend acts as a **Trusted Custodian**, signing transactions with a secure private key hidden in environment variables.
|
|
||||||
- **Authentication Hooks**: Access to contracts is strictly controlled via Clerk Auth, ensuring users only see their own data.
|
|
||||||
|
|
||||||
---
|
## Installation
|
||||||
|
|
||||||
## 🎨 6. UX/UI & Aesthetics
|
Install the root application dependencies first:
|
||||||
|
|
||||||
The application features a **"Premium Glassmorphism"** design system.
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
- **Why?**: In BFSI, the interface must convey **Trust, Modernity, and Clarity**.
|
Then install the blockchain workspace dependencies:
|
||||||
- **Design Tokens**: We use vibrant gradients, subtle blurs, and micro-animations to make the complex task of contract management feel light and intuitive.
|
|
||||||
- **Theme Awareness**: The UI is optimized for both Light and Dark modes, adapting to the professional environment of the user.
|
|
||||||
|
|
||||||
---
|
```bash
|
||||||
|
cd blockchain
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
## 🌟 7. Why this is a 10/10 PFE Project
|
## Local Development
|
||||||
|
|
||||||
LexiChain isn't just a simple web app; it is a **Multidisciplinary Innovation**:
|
The recommended development command starts the app together with the local chain workflow:
|
||||||
|
|
||||||
1. **AI Innovation**: Moving beyond simple text extraction to a conversational RAG system.
|
```bash
|
||||||
2. **Blockchain Innovation**: Implementing a production-ready Document Registry that solves real-world legal issues.
|
npm run dev
|
||||||
3. **Architectural Integrity**: Using FSD and Clean Code principles usually found in senior-level software engineering.
|
```
|
||||||
4. **Market Readiness**: The solution is directly applicable to banks and insurance companies looking to digitalize their workflow.
|
|
||||||
|
|
||||||
---
|
This runs the bootstrap script defined in the root package and is the best option when you want the frontend and blockchain pieces aligned locally.
|
||||||
|
|
||||||
## 📚 8. Glossary for NotebookLM
|
If you only need the Next.js app without the chain bootstrap, run:
|
||||||
|
|
||||||
- **BFSI**: Banking, Financial Services, and Insurance.
|
```bash
|
||||||
- **RAG (Retrieval-Augmented Generation)**: A technique that allows AI to answer questions based _only_ on the provided documents, preventing "hallucinations."
|
npm run dev:next
|
||||||
- **Smart Contract**: A programmable contract that executes automatically when conditions are met.
|
```
|
||||||
- **SHA-256**: A one-way cryptographic function. If you change 1 bit of a file, the entire hash changes.
|
|
||||||
- **Hardhat**: A professional development environment for Ethereum software.
|
## Scripts
|
||||||
- **Sepolia**: The public test network used to simulate the real Ethereum blockchain.
|
|
||||||
|
### Root workspace
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
| ------------------------ | ------------------------------------------- |
|
||||||
|
| `npm run dev` | Starts the combined development workflow |
|
||||||
|
| `npm run dev:with-chain` | Alias for the combined development workflow |
|
||||||
|
| `npm run dev:next` | Runs only the Next.js dev server |
|
||||||
|
| `npm run build` | Builds the production application |
|
||||||
|
| `npm run start` | Starts the production server |
|
||||||
|
| `npm run lint` | Runs ESLint |
|
||||||
|
|
||||||
|
### Blockchain workspace
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
| ------------------------ | ------------------------------------ |
|
||||||
|
| `npm run compile` | Compiles the smart contracts |
|
||||||
|
| `npm run test` | Runs the Hardhat test suite |
|
||||||
|
| `npm run node` | Starts a local Hardhat node |
|
||||||
|
| `npm run deploy:local` | Deploys to the local Hardhat network |
|
||||||
|
| `npm run deploy:sepolia` | Deploys to Sepolia |
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
This is the clean production path for option 3 in the README.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
Before deploying, verify:
|
||||||
|
|
||||||
|
- environment variables are set correctly
|
||||||
|
- the database is reachable
|
||||||
|
- the blockchain contract address and RPC settings are valid
|
||||||
|
- the code passes linting and any required tests
|
||||||
|
|
||||||
|
## Team Notes
|
||||||
|
|
||||||
|
These are the implementation details future contributors should know before extending the app:
|
||||||
|
|
||||||
|
- Contract records store the internal application user id, not the Clerk user id; resolve the Clerk user first before querying contract data.
|
||||||
|
- The upload flow triggers analysis automatically after saving a contract, so the UI should show analysis-in-progress feedback immediately.
|
||||||
|
- Dashboard and contacts routes live under `/dashboard` and `/contacts`.
|
||||||
|
- Trend charts should be aggregated by day with zero-filled windows so 30-day views stay accurate.
|
||||||
|
- Q&A over contracts uses Gemini embeddings and an in-memory contract vector cache.
|
||||||
|
- Claim status updates are intentionally restricted; the UI should use the allowed next statuses for each claim instead of a global list.
|
||||||
|
|
||||||
|
## Contribution Guidelines
|
||||||
|
|
||||||
|
If you are continuing this project as a team, keep the following practices in mind:
|
||||||
|
|
||||||
|
- make focused changes and avoid mixing unrelated refactors with feature work
|
||||||
|
- keep environment-dependent values out of source control
|
||||||
|
- prefer the existing feature structure when adding new UI or business logic
|
||||||
|
- update screenshots when user-facing screens change significantly
|
||||||
|
- document workflow changes in the README or the relevant docs file
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- If the app does not start, verify your Node.js version and reinstall dependencies.
|
||||||
|
- If blockchain features fail, confirm the local node is running and the contract address points to a deployed contract.
|
||||||
|
- If authentication fails, recheck the Clerk environment variables and callback configuration.
|
||||||
|
- If database access fails, validate the Prisma connection string and database availability.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
LexiChain is a BFSI document intelligence platform built for contract review, verification, and operational clarity. The codebase is structured to be maintainable by a team, with a clear split between app, blockchain, data, and shared UI concerns.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
3
blockchain/.dev-deploy.json
Normal file
3
blockchain/.dev-deploy.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"address": "0x5FbDB2315678afecb367f032d93F642f64180aa3"
|
||||||
|
}
|
||||||
@@ -2,6 +2,9 @@
|
|||||||
"name": "lexichain-blockchain",
|
"name": "lexichain-blockchain",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "LexiChain Document Registry - Solidity Smart Contract",
|
"description": "LexiChain Document Registry - Solidity Smart Contract",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20 <23"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"compile": "hardhat compile",
|
"compile": "hardhat compile",
|
||||||
"test": "hardhat test",
|
"test": "hardhat test",
|
||||||
|
|||||||
103
docs/DEPLOYMENT_OPENSTACK.md
Normal file
103
docs/DEPLOYMENT_OPENSTACK.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# Deployment Guide (OpenStack + Docker)
|
||||||
|
|
||||||
|
This guide deploys the Next.js app as a Docker container on a private OpenStack environment.
|
||||||
|
|
||||||
|
## 1) Prerequisites
|
||||||
|
|
||||||
|
- OpenStack project with a VM (Ubuntu 22.04 or similar)
|
||||||
|
- Docker Engine installed on the VM
|
||||||
|
- A PostgreSQL database reachable from the VM
|
||||||
|
- A blockchain RPC endpoint (Sepolia or private chain)
|
||||||
|
- A deployed DocumentRegistry contract address
|
||||||
|
|
||||||
|
## 2) Build the Docker image (local or CI)
|
||||||
|
|
||||||
|
From the project root:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker build -t lexichain-app:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
Optionally tag and push to your private registry.
|
||||||
|
|
||||||
|
## 3) Runtime environment variables
|
||||||
|
|
||||||
|
Create a file named `lexichain.env` on the VM with the required secrets.
|
||||||
|
Example (fill with your real values):
|
||||||
|
|
||||||
|
```
|
||||||
|
NODE_ENV=production
|
||||||
|
PORT=3000
|
||||||
|
DATABASE_URL=postgresql://USER:PASSWORD@HOST:PORT/DBNAME
|
||||||
|
CLERK_PUBLISHABLE_KEY=...
|
||||||
|
CLERK_SECRET_KEY=...
|
||||||
|
CLERK_WEBHOOK_SECRET=...
|
||||||
|
UPLOADTHING_SECRET=...
|
||||||
|
UPLOADTHING_APP_ID=...
|
||||||
|
GOOGLE_AI_API_KEY=...
|
||||||
|
BLOCKCHAIN_NETWORK=sepolia
|
||||||
|
BLOCKCHAIN_RPC_URL=https://...
|
||||||
|
BLOCKCHAIN_CONTRACT_ADDRESS=0x...
|
||||||
|
BLOCKCHAIN_PRIVATE_KEY=0x...
|
||||||
|
```
|
||||||
|
|
||||||
|
If you use a private chain, set `BLOCKCHAIN_NETWORK` accordingly.
|
||||||
|
|
||||||
|
## 4) Run the container on the VM
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run -d \
|
||||||
|
--name lexichain-app \
|
||||||
|
--restart unless-stopped \
|
||||||
|
--env-file /path/to/lexichain.env \
|
||||||
|
-p 3000:3000 \
|
||||||
|
lexichain-app:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5) Database migration (first deploy)
|
||||||
|
|
||||||
|
Run migrations from the container image (or from CI) before first launch:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run --rm \
|
||||||
|
--env-file /path/to/lexichain.env \
|
||||||
|
lexichain-app:latest \
|
||||||
|
npx prisma migrate deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6) OpenStack security group / firewall
|
||||||
|
|
||||||
|
Allow inbound traffic to port 3000 from your internal network or from the reverse proxy.
|
||||||
|
|
||||||
|
## 7) Optional: reverse proxy
|
||||||
|
|
||||||
|
Place Nginx or HAProxy in front of the app for TLS termination and HTTP/2.
|
||||||
|
|
||||||
|
## 8) Health check
|
||||||
|
|
||||||
|
Open `http://<vm-ip>:3000` and validate:
|
||||||
|
|
||||||
|
- Sign-in flow
|
||||||
|
- Upload + AI analysis
|
||||||
|
- Blockchain explorer stats
|
||||||
|
- Document verification
|
||||||
|
|
||||||
|
## 9) Update / rollout
|
||||||
|
|
||||||
|
- Build a new image and push to your registry.
|
||||||
|
- Pull on the VM and restart the container:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker pull registry.example.com/lexichain-app:latest
|
||||||
|
|
||||||
|
docker stop lexichain-app
|
||||||
|
|
||||||
|
docker rm lexichain-app
|
||||||
|
|
||||||
|
docker run -d \
|
||||||
|
--name lexichain-app \
|
||||||
|
--restart unless-stopped \
|
||||||
|
--env-file /path/to/lexichain.env \
|
||||||
|
-p 3000:3000 \
|
||||||
|
registry.example.com/lexichain-app:latest
|
||||||
|
```
|
||||||
@@ -19,7 +19,12 @@ import { prisma } from "@/lib/db/prisma";
|
|||||||
import { BlockchainService } from "@/lib/services/blockchain.service";
|
import { BlockchainService } from "@/lib/services/blockchain.service";
|
||||||
import { NotificationService } from "@/lib/services/notification.service";
|
import { NotificationService } from "@/lib/services/notification.service";
|
||||||
import { ContractService } from "@/lib/services/contract.service";
|
import { ContractService } from "@/lib/services/contract.service";
|
||||||
import type { BlockchainTransactionView, BlockchainStats } from "@/lib/services/blockchain.types";
|
import { CertificateService } from "@/lib/services/certificate.service";
|
||||||
|
import type {
|
||||||
|
BlockchainTransactionView,
|
||||||
|
BlockchainStats,
|
||||||
|
CryptographicCertificate,
|
||||||
|
} from "@/lib/services/blockchain.types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a contract's document on the blockchain.
|
* Register a contract's document on the blockchain.
|
||||||
@@ -44,7 +49,8 @@ export async function registerContractOnBlockchain(contractId: string) {
|
|||||||
if (!BlockchainService.isConfigured()) {
|
if (!BlockchainService.isConfigured()) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: "Blockchain not configured. Start a Hardhat node and check your .env.",
|
error:
|
||||||
|
"Blockchain not configured. Start a Hardhat node and check your .env.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +80,7 @@ export async function registerContractOnBlockchain(contractId: string) {
|
|||||||
// Hash the document and register on-chain
|
// Hash the document and register on-chain
|
||||||
const proof = await BlockchainService.hashAndRegister(
|
const proof = await BlockchainService.hashAndRegister(
|
||||||
contract.fileUrl,
|
contract.fileUrl,
|
||||||
contract.fileName
|
contract.fileName,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Save proof data to the Contract record
|
// Save proof data to the Contract record
|
||||||
@@ -137,7 +143,8 @@ export async function registerContractOnBlockchain(contractId: string) {
|
|||||||
console.error("❌ Blockchain registration error:", error);
|
console.error("❌ Blockchain registration error:", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : "Unknown blockchain error",
|
error:
|
||||||
|
error instanceof Error ? error.message : "Unknown blockchain error",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,14 +158,18 @@ export async function verifyContractOnBlockchain(contractId: string) {
|
|||||||
if (!clerkId) return { success: false, error: "Unauthorized" };
|
if (!clerkId) return { success: false, error: "Unauthorized" };
|
||||||
|
|
||||||
if (!BlockchainService.isReadConfigured()) {
|
if (!BlockchainService.isReadConfigured()) {
|
||||||
return { success: false, error: "Blockchain not configured for verification" };
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Blockchain not configured for verification",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await ContractService.getUserByClerkId(clerkId);
|
const user = await ContractService.getUserByClerkId(clerkId);
|
||||||
if (!user) return { success: false, error: "User not found" };
|
if (!user) return { success: false, error: "User not found" };
|
||||||
|
|
||||||
const contract = await ContractService.getById(contractId);
|
const contract = await ContractService.getById(contractId);
|
||||||
if (contract.userId !== user.id) return { success: false, error: "Unauthorized" };
|
if (contract.userId !== user.id)
|
||||||
|
return { success: false, error: "Unauthorized" };
|
||||||
|
|
||||||
if (!contract.documentHash) {
|
if (!contract.documentHash) {
|
||||||
return {
|
return {
|
||||||
@@ -167,7 +178,9 @@ export async function verifyContractOnBlockchain(contractId: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const verification = await BlockchainService.verifyOnChain(contract.documentHash);
|
const verification = await BlockchainService.verifyOnChain(
|
||||||
|
contract.documentHash,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -199,7 +212,10 @@ export async function verifyDocumentHashOnBlockchain(documentHash: string) {
|
|||||||
if (!clerkId) return { success: false, error: "Unauthorized" };
|
if (!clerkId) return { success: false, error: "Unauthorized" };
|
||||||
|
|
||||||
if (!BlockchainService.isReadConfigured()) {
|
if (!BlockchainService.isReadConfigured()) {
|
||||||
return { success: false, error: "Blockchain not configured for verification" };
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Blockchain not configured for verification",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure proper format
|
// Ensure proper format
|
||||||
@@ -313,3 +329,97 @@ export async function getBlockchainStats(): Promise<{
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a cryptographic certificate for a blockchain-registered contract.
|
||||||
|
*
|
||||||
|
* The certificate contains:
|
||||||
|
* - Contract metadata
|
||||||
|
* - Blockchain proof (txHash, block, timestamp)
|
||||||
|
* - A digital signature created by the server wallet
|
||||||
|
*
|
||||||
|
* @param contractId - The contract ID
|
||||||
|
* @returns Certificate data (JSON) or error
|
||||||
|
*/
|
||||||
|
export async function generateContractCertificate(contractId: string): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
certificate?: CryptographicCertificate;
|
||||||
|
certificateJson?: string;
|
||||||
|
certificatePdfBase64?: string;
|
||||||
|
certificateFileName?: string;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const { userId: clerkId } = await auth();
|
||||||
|
if (!clerkId) return { success: false, error: "Unauthorized" };
|
||||||
|
|
||||||
|
if (!BlockchainService.isReadConfigured()) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Blockchain not configured for certificate generation",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await ContractService.getUserByClerkId(clerkId);
|
||||||
|
if (!user) return { success: false, error: "User not found" };
|
||||||
|
|
||||||
|
const contract = await ContractService.getById(contractId);
|
||||||
|
if (contract.userId !== user.id) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Unauthorized: Contract does not belong to you",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if contract is registered on blockchain
|
||||||
|
if (!contract.documentHash || !contract.txHash) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error:
|
||||||
|
"Contract is not registered on blockchain. Register it first to generate a certificate.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the certificate
|
||||||
|
const certificate = await CertificateService.generateCertificate(
|
||||||
|
contract.id,
|
||||||
|
contract.title,
|
||||||
|
contract.fileName,
|
||||||
|
contract.documentHash,
|
||||||
|
contract.txHash,
|
||||||
|
contract.blockNumber || 0,
|
||||||
|
contract.blockTimestamp?.toISOString() || new Date().toISOString(),
|
||||||
|
contract.blockchainNetwork || "hardhat",
|
||||||
|
contract.contractAddress || "",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Generate a professional PDF for download
|
||||||
|
const pdfBuffer =
|
||||||
|
await CertificateService.generateCertificatePdf(certificate);
|
||||||
|
const certificatePdfBase64 = pdfBuffer.toString("base64");
|
||||||
|
|
||||||
|
const baseName = (contract.title || contract.fileName || contract.id)
|
||||||
|
.replace(/[^a-zA-Z0-9-_]+/g, "-")
|
||||||
|
.replace(/^-+|-+$/g, "")
|
||||||
|
.slice(0, 64);
|
||||||
|
const dateStamp = new Date().toISOString().slice(0, 10);
|
||||||
|
const certificateFileName = `certificate-${baseName || contract.id}-${dateStamp}.pdf`;
|
||||||
|
|
||||||
|
// Keep JSON available for internal use or auditing if needed
|
||||||
|
const certificateJson = JSON.stringify(certificate, null, 2);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
certificate,
|
||||||
|
certificateJson,
|
||||||
|
certificatePdfBase64,
|
||||||
|
certificateFileName,
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error("❌ Certificate generation error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
116
features/contracts/components/contract-countdown.tsx
Normal file
116
features/contracts/components/contract-countdown.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Clock, AlertCircle } from "lucide-react";
|
||||||
|
|
||||||
|
interface ContractCountdownProps {
|
||||||
|
endDate?: string | null;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ContractCountdown({
|
||||||
|
endDate,
|
||||||
|
className = "",
|
||||||
|
}: ContractCountdownProps) {
|
||||||
|
const [daysLeft, setDaysLeft] = useState<number | null>(null);
|
||||||
|
const [isExpired, setIsExpired] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!endDate) {
|
||||||
|
setDaysLeft(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculateDaysLeft = () => {
|
||||||
|
const end = new Date(endDate);
|
||||||
|
const today = new Date();
|
||||||
|
|
||||||
|
// Reset time for comparison
|
||||||
|
end.setHours(0, 0, 0, 0);
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const timeDiff = end.getTime() - today.getTime();
|
||||||
|
const days = Math.ceil(timeDiff / (1000 * 3600 * 24));
|
||||||
|
|
||||||
|
setIsExpired(days < 0);
|
||||||
|
setDaysLeft(days);
|
||||||
|
};
|
||||||
|
|
||||||
|
calculateDaysLeft();
|
||||||
|
|
||||||
|
// Update every minute to keep countdown fresh
|
||||||
|
const interval = setInterval(calculateDaysLeft, 60000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [endDate]);
|
||||||
|
|
||||||
|
if (daysLeft === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine colors based on urgency
|
||||||
|
const getUrgencyStyle = () => {
|
||||||
|
if (isExpired) {
|
||||||
|
return {
|
||||||
|
bg: "bg-red-500/15",
|
||||||
|
border: "border-red-500/40",
|
||||||
|
text: "text-red-700 dark:text-red-300",
|
||||||
|
icon: "text-red-500",
|
||||||
|
label: "Expired",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (daysLeft < 7) {
|
||||||
|
return {
|
||||||
|
bg: "bg-red-500/15",
|
||||||
|
border: "border-red-500/40",
|
||||||
|
text: "text-red-700 dark:text-red-300",
|
||||||
|
icon: "text-red-500",
|
||||||
|
label: `${daysLeft} day${daysLeft !== 1 ? "s" : ""} left`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (daysLeft < 14) {
|
||||||
|
return {
|
||||||
|
bg: "bg-orange-500/15",
|
||||||
|
border: "border-orange-500/40",
|
||||||
|
text: "text-orange-700 dark:text-orange-300",
|
||||||
|
icon: "text-orange-500",
|
||||||
|
label: `${daysLeft} days left`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (daysLeft < 30) {
|
||||||
|
return {
|
||||||
|
bg: "bg-amber-500/15",
|
||||||
|
border: "border-amber-500/40",
|
||||||
|
text: "text-amber-700 dark:text-amber-300",
|
||||||
|
icon: "text-amber-500",
|
||||||
|
label: `${daysLeft} days left`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
bg: "bg-green-500/15",
|
||||||
|
border: "border-green-500/40",
|
||||||
|
text: "text-green-700 dark:text-green-300",
|
||||||
|
icon: "text-green-500",
|
||||||
|
label: `${daysLeft} days left`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const urgency = getUrgencyStyle();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`inline-flex items-center gap-2 rounded-full border ${urgency.bg} ${urgency.border} px-3 py-1.5 text-[11px] font-bold uppercase tracking-wider ${urgency.text} ${className}`}
|
||||||
|
>
|
||||||
|
{isExpired ? (
|
||||||
|
<AlertCircle className={`h-3.5 w-3.5 ${urgency.icon}`} />
|
||||||
|
) : (
|
||||||
|
<Clock className={`h-3.5 w-3.5 ${urgency.icon} animate-pulse`} />
|
||||||
|
)}
|
||||||
|
{urgency.label}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -60,6 +60,7 @@ import {
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ContractChatModal } from "@/features/contracts/components/modals/contract-chat-modal";
|
import { ContractChatModal } from "@/features/contracts/components/modals/contract-chat-modal";
|
||||||
import { ContractProofModal } from "@/features/contracts/components/modals/contract-proof-modal";
|
import { ContractProofModal } from "@/features/contracts/components/modals/contract-proof-modal";
|
||||||
|
import { ContractCountdown } from "@/features/contracts/components/contract-countdown";
|
||||||
import {
|
import {
|
||||||
stripMarkdown,
|
stripMarkdown,
|
||||||
exportToCSV,
|
exportToCSV,
|
||||||
@@ -1122,6 +1123,10 @@ export function ContractsList({ refreshTrigger }: { refreshTrigger?: number }) {
|
|||||||
/>
|
/>
|
||||||
{status.label}
|
{status.label}
|
||||||
</span>
|
</span>
|
||||||
|
{contract.endDate &&
|
||||||
|
contract.status === "COMPLETED" && (
|
||||||
|
<ContractCountdown endDate={contract.endDate} />
|
||||||
|
)}
|
||||||
{contract.isRagged && (
|
{contract.isRagged && (
|
||||||
<span className="inline-flex items-center gap-1 rounded-full border border-cyan-500/30 bg-cyan-500/10 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-cyan-700 dark:text-cyan-300">
|
<span className="inline-flex items-center gap-1 rounded-full border border-cyan-500/30 bg-cyan-500/10 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-cyan-700 dark:text-cyan-300">
|
||||||
<Network className="h-3 w-3" />
|
<Network className="h-3 w-3" />
|
||||||
|
|||||||
@@ -23,7 +23,11 @@
|
|||||||
|
|
||||||
import { ethers } from "ethers";
|
import { ethers } from "ethers";
|
||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
import type { BlockchainProof, BlockchainVerification, BlockchainStats } from "./blockchain.types";
|
import type {
|
||||||
|
BlockchainProof,
|
||||||
|
BlockchainVerification,
|
||||||
|
BlockchainStats,
|
||||||
|
} from "./blockchain.types";
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────
|
||||||
// Smart Contract ABI (Application Binary Interface)
|
// Smart Contract ABI (Application Binary Interface)
|
||||||
@@ -102,7 +106,7 @@ function getReadContract(): ethers.Contract {
|
|||||||
_readContract = new ethers.Contract(
|
_readContract = new ethers.Contract(
|
||||||
contractAddress,
|
contractAddress,
|
||||||
DOCUMENT_REGISTRY_ABI,
|
DOCUMENT_REGISTRY_ABI,
|
||||||
getProvider()
|
getProvider(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return _readContract;
|
return _readContract;
|
||||||
@@ -117,7 +121,7 @@ function getWriteContract(): ethers.Contract {
|
|||||||
_writeContract = new ethers.Contract(
|
_writeContract = new ethers.Contract(
|
||||||
contractAddress,
|
contractAddress,
|
||||||
DOCUMENT_REGISTRY_ABI,
|
DOCUMENT_REGISTRY_ABI,
|
||||||
getWallet()
|
getWallet(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return _writeContract;
|
return _writeContract;
|
||||||
@@ -214,7 +218,7 @@ export class BlockchainService {
|
|||||||
*/
|
*/
|
||||||
static async registerOnChain(
|
static async registerOnChain(
|
||||||
documentHash: string,
|
documentHash: string,
|
||||||
fileName: string
|
fileName: string,
|
||||||
): Promise<BlockchainProof> {
|
): Promise<BlockchainProof> {
|
||||||
if (!this.isConfigured()) {
|
if (!this.isConfigured()) {
|
||||||
throw new Error("Blockchain not configured. Check your .env variables.");
|
throw new Error("Blockchain not configured. Check your .env variables.");
|
||||||
@@ -307,7 +311,7 @@ export class BlockchainService {
|
|||||||
* @returns Verification result with existence, timestamp, depositor
|
* @returns Verification result with existence, timestamp, depositor
|
||||||
*/
|
*/
|
||||||
static async verifyOnChain(
|
static async verifyOnChain(
|
||||||
documentHash: string
|
documentHash: string,
|
||||||
): Promise<BlockchainVerification> {
|
): Promise<BlockchainVerification> {
|
||||||
if (!this.isReadConfigured()) {
|
if (!this.isReadConfigured()) {
|
||||||
throw new Error("Blockchain read access not configured");
|
throw new Error("Blockchain read access not configured");
|
||||||
@@ -331,7 +335,7 @@ export class BlockchainService {
|
|||||||
*/
|
*/
|
||||||
static async hashAndRegister(
|
static async hashAndRegister(
|
||||||
fileUrl: string,
|
fileUrl: string,
|
||||||
fileName: string
|
fileName: string,
|
||||||
): Promise<BlockchainProof> {
|
): Promise<BlockchainProof> {
|
||||||
const documentHash = await this.hashDocument(fileUrl);
|
const documentHash = await this.hashDocument(fileUrl);
|
||||||
return await this.registerOnChain(documentHash, fileName);
|
return await this.registerOnChain(documentHash, fileName);
|
||||||
@@ -368,13 +372,14 @@ export class BlockchainService {
|
|||||||
const [blockNumber, totalDocs, networkObj] = await Promise.all([
|
const [blockNumber, totalDocs, networkObj] = await Promise.all([
|
||||||
provider.getBlockNumber(),
|
provider.getBlockNumber(),
|
||||||
contract.totalDocuments(),
|
contract.totalDocuments(),
|
||||||
provider.getNetwork()
|
provider.getNetwork(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalVerified: Number(totalDocs),
|
totalVerified: Number(totalDocs),
|
||||||
latestBlockNumber: blockNumber,
|
latestBlockNumber: blockNumber,
|
||||||
networkName: config.network === "sepolia" ? "Ethereum Sepolia" : "Hardhat Local",
|
networkName:
|
||||||
|
config.network === "sepolia" ? "Ethereum Sepolia" : "Hardhat Local",
|
||||||
networkStatus: "connected",
|
networkStatus: "connected",
|
||||||
walletAddress,
|
walletAddress,
|
||||||
chainId: Number(networkObj.chainId),
|
chainId: Number(networkObj.chainId),
|
||||||
@@ -384,7 +389,8 @@ export class BlockchainService {
|
|||||||
return {
|
return {
|
||||||
totalVerified: 0,
|
totalVerified: 0,
|
||||||
latestBlockNumber: null,
|
latestBlockNumber: null,
|
||||||
networkName: config.network === "sepolia" ? "Ethereum Sepolia" : "Hardhat Local",
|
networkName:
|
||||||
|
config.network === "sepolia" ? "Ethereum Sepolia" : "Hardhat Local",
|
||||||
networkStatus: "disconnected",
|
networkStatus: "disconnected",
|
||||||
walletAddress: "",
|
walletAddress: "",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -48,6 +48,37 @@ export interface BlockchainTransactionView {
|
|||||||
explorerUrl: string | null;
|
explorerUrl: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cryptographic certificate for a registered contract.
|
||||||
|
* Contains metadata, blockchain proof, and a digital signature.
|
||||||
|
* Can be downloaded and verified by third parties.
|
||||||
|
*/
|
||||||
|
export interface CryptographicCertificate {
|
||||||
|
// Document metadata
|
||||||
|
contractId: string;
|
||||||
|
contractTitle: string | null;
|
||||||
|
contractFileName: string;
|
||||||
|
documentHash: string;
|
||||||
|
|
||||||
|
// Blockchain proof
|
||||||
|
txHash: string;
|
||||||
|
blockNumber: number;
|
||||||
|
blockTimestamp: string; // ISO string
|
||||||
|
network: string;
|
||||||
|
contractAddress: string;
|
||||||
|
|
||||||
|
// Certificate metadata
|
||||||
|
certificateId: string; // Unique certificate identifier
|
||||||
|
issuedAt: string; // ISO string - when the certificate was generated
|
||||||
|
issuer: string; // Ethereum address that issued the certificate (server wallet)
|
||||||
|
|
||||||
|
// Digital signature (ECDSA)
|
||||||
|
signature: string; // Signed message in hex format (0x...)
|
||||||
|
|
||||||
|
// Version for future compatibility
|
||||||
|
version: "1.0";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stats displayed at the top of the blockchain explorer page.
|
* Stats displayed at the top of the blockchain explorer page.
|
||||||
*/
|
*/
|
||||||
|
|||||||
402
lib/services/certificate.service.ts
Normal file
402
lib/services/certificate.service.ts
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
/**
|
||||||
|
* Certificate Service
|
||||||
|
*
|
||||||
|
* Generates cryptographically signed certificates for blockchain-registered contracts.
|
||||||
|
* These certificates can be downloaded and verified by third parties, proving that
|
||||||
|
* a contract was registered on-chain with specific metadata at a specific timestamp.
|
||||||
|
*
|
||||||
|
* Uses ECDSA (Elliptic Curve Digital Signature Algorithm) via ethers.js Wallet.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ethers } from "ethers";
|
||||||
|
import { createHash } from "crypto";
|
||||||
|
import PDFDocument from "pdfkit";
|
||||||
|
import QRCode from "qrcode";
|
||||||
|
import type { CryptographicCertificate } from "./blockchain.types";
|
||||||
|
import { BlockchainService } from "./blockchain.service";
|
||||||
|
|
||||||
|
export class CertificateService {
|
||||||
|
/**
|
||||||
|
* Generate a digitally-signed certificate for a blockchain-registered contract.
|
||||||
|
*
|
||||||
|
* @param contractId - Contract ID
|
||||||
|
* @param contractTitle - Contract title
|
||||||
|
* @param contractFileName - Original file name
|
||||||
|
* @param documentHash - SHA-256 hash of the document
|
||||||
|
* @param txHash - Blockchain transaction hash
|
||||||
|
* @param blockNumber - Block number
|
||||||
|
* @param blockTimestamp - Block timestamp (ISO string)
|
||||||
|
* @param network - Blockchain network ('hardhat' | 'sepolia')
|
||||||
|
* @param contractAddress - Smart contract address
|
||||||
|
* @returns Digitally-signed certificate
|
||||||
|
*/
|
||||||
|
static async generateCertificate(
|
||||||
|
contractId: string,
|
||||||
|
contractTitle: string | null,
|
||||||
|
contractFileName: string,
|
||||||
|
documentHash: string,
|
||||||
|
txHash: string,
|
||||||
|
blockNumber: number,
|
||||||
|
blockTimestamp: string,
|
||||||
|
network: string,
|
||||||
|
contractAddress: string,
|
||||||
|
): Promise<CryptographicCertificate> {
|
||||||
|
if (!BlockchainService.isReadConfigured()) {
|
||||||
|
throw new Error("Blockchain not configured for certificate generation");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate unique certificate ID
|
||||||
|
const certificateId = this.generateCertificateId(contractId);
|
||||||
|
|
||||||
|
// Get issuer wallet address
|
||||||
|
let issuerAddress = "";
|
||||||
|
try {
|
||||||
|
// Try to get the wallet address if blockchain write is configured
|
||||||
|
if (BlockchainService.isConfigured()) {
|
||||||
|
// We can't directly access the wallet from here, so we'll use the private key env var
|
||||||
|
const privateKey = process.env.BLOCKCHAIN_PRIVATE_KEY;
|
||||||
|
if (privateKey) {
|
||||||
|
const wallet = new ethers.Wallet(privateKey);
|
||||||
|
issuerAddress = wallet.address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If wallet creation fails, use a placeholder
|
||||||
|
issuerAddress = "0x0000000000000000000000000000000000000000";
|
||||||
|
}
|
||||||
|
|
||||||
|
const issuedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
// Prepare the data to sign
|
||||||
|
const certificateData = {
|
||||||
|
contractId,
|
||||||
|
contractTitle,
|
||||||
|
contractFileName,
|
||||||
|
documentHash,
|
||||||
|
txHash,
|
||||||
|
blockNumber,
|
||||||
|
blockTimestamp,
|
||||||
|
network,
|
||||||
|
contractAddress,
|
||||||
|
certificateId,
|
||||||
|
issuedAt,
|
||||||
|
issuer: issuerAddress,
|
||||||
|
version: "1.0",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a message to sign (hash of the certificate data)
|
||||||
|
const messageToSign = this.createSignableMessage(certificateData);
|
||||||
|
const messageHash = ethers.hashMessage(messageToSign);
|
||||||
|
|
||||||
|
let signature = "";
|
||||||
|
try {
|
||||||
|
// Sign the message with the server wallet
|
||||||
|
if (BlockchainService.isConfigured()) {
|
||||||
|
const privateKey = process.env.BLOCKCHAIN_PRIVATE_KEY;
|
||||||
|
if (privateKey) {
|
||||||
|
const wallet = new ethers.Wallet(privateKey);
|
||||||
|
signature = await wallet.signMessage(messageToSign);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to sign certificate:", error);
|
||||||
|
signature = "0x"; // Return empty signature if signing fails
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificate: CryptographicCertificate = {
|
||||||
|
contractId,
|
||||||
|
contractTitle,
|
||||||
|
contractFileName,
|
||||||
|
documentHash,
|
||||||
|
txHash,
|
||||||
|
blockNumber,
|
||||||
|
blockTimestamp,
|
||||||
|
network,
|
||||||
|
contractAddress,
|
||||||
|
certificateId,
|
||||||
|
issuedAt,
|
||||||
|
issuer: issuerAddress,
|
||||||
|
signature,
|
||||||
|
version: "1.0",
|
||||||
|
};
|
||||||
|
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a unique certificate ID based on contract ID and timestamp.
|
||||||
|
*/
|
||||||
|
private static generateCertificateId(contractId: string): string {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const combined = `${contractId}:${timestamp}`;
|
||||||
|
const hash = createHash("sha256").update(combined).digest("hex");
|
||||||
|
return `cert_${hash.slice(0, 16)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a human-readable and machine-verifiable message to sign.
|
||||||
|
*/
|
||||||
|
private static createSignableMessage(data: any): string {
|
||||||
|
const lines = [
|
||||||
|
"═══════════════════════════════════════════",
|
||||||
|
"LexiChain Cryptographic Certificate",
|
||||||
|
"═══════════════════════════════════════════",
|
||||||
|
"",
|
||||||
|
`Certificate ID: ${data.certificateId}`,
|
||||||
|
`Contract: ${data.contractTitle || data.contractFileName}`,
|
||||||
|
`Document Hash: ${data.documentHash}`,
|
||||||
|
"",
|
||||||
|
"Blockchain Proof:",
|
||||||
|
` Transaction Hash: ${data.txHash}`,
|
||||||
|
` Block Number: ${data.blockNumber}`,
|
||||||
|
` Timestamp: ${data.blockTimestamp}`,
|
||||||
|
` Network: ${data.network}`,
|
||||||
|
` Contract Address: ${data.contractAddress}`,
|
||||||
|
"",
|
||||||
|
`Issued At: ${data.issuedAt}`,
|
||||||
|
`Issued By: ${data.issuer}`,
|
||||||
|
"",
|
||||||
|
"This certificate cryptographically proves that the above",
|
||||||
|
"document was registered on the blockchain at the specified",
|
||||||
|
"block and timestamp. The signature on this message can be",
|
||||||
|
"verified using the issuer's public key.",
|
||||||
|
"═══════════════════════════════════════════",
|
||||||
|
];
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify a certificate's signature (useful for third parties).
|
||||||
|
* Returns true if the signature is valid and matches the issuer.
|
||||||
|
*/
|
||||||
|
static verifyCertificateSignature(
|
||||||
|
certificate: CryptographicCertificate,
|
||||||
|
): boolean {
|
||||||
|
try {
|
||||||
|
const messageToSign = this.createSignableMessage(certificate);
|
||||||
|
const recoveredAddress = ethers.verifyMessage(
|
||||||
|
messageToSign,
|
||||||
|
certificate.signature,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
recoveredAddress.toLowerCase() === certificate.issuer.toLowerCase()
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a professional PDF certificate for the given certificate data.
|
||||||
|
*/
|
||||||
|
static async generateCertificatePdf(
|
||||||
|
certificate: CryptographicCertificate,
|
||||||
|
): Promise<Buffer> {
|
||||||
|
const qrPayload = JSON.stringify({
|
||||||
|
certificateId: certificate.certificateId,
|
||||||
|
contractId: certificate.contractId,
|
||||||
|
documentHash: certificate.documentHash,
|
||||||
|
txHash: certificate.txHash,
|
||||||
|
blockNumber: certificate.blockNumber,
|
||||||
|
blockTimestamp: certificate.blockTimestamp,
|
||||||
|
network: certificate.network,
|
||||||
|
contractAddress: certificate.contractAddress,
|
||||||
|
issuer: certificate.issuer,
|
||||||
|
issuedAt: certificate.issuedAt,
|
||||||
|
version: certificate.version,
|
||||||
|
});
|
||||||
|
|
||||||
|
const qrPng = await QRCode.toBuffer(qrPayload, {
|
||||||
|
width: 160,
|
||||||
|
margin: 1,
|
||||||
|
errorCorrectionLevel: "M",
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const doc = new PDFDocument({ size: "A4", margin: 56 });
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
|
||||||
|
doc.on("data", (chunk) => chunks.push(chunk as Buffer));
|
||||||
|
doc.on("end", () => resolve(Buffer.concat(chunks)));
|
||||||
|
|
||||||
|
// Header
|
||||||
|
doc
|
||||||
|
.font("Helvetica-Bold")
|
||||||
|
.fontSize(20)
|
||||||
|
.fillColor("#111827")
|
||||||
|
.text("LexiChain Contract Authenticity Certificate", {
|
||||||
|
align: "center",
|
||||||
|
})
|
||||||
|
.moveDown(0.2);
|
||||||
|
|
||||||
|
doc
|
||||||
|
.font("Helvetica")
|
||||||
|
.fontSize(10)
|
||||||
|
.fillColor("#6B7280")
|
||||||
|
.text("Digitally signed proof of blockchain registration", {
|
||||||
|
align: "center",
|
||||||
|
})
|
||||||
|
.moveDown(1.2);
|
||||||
|
|
||||||
|
// Divider
|
||||||
|
doc
|
||||||
|
.moveTo(doc.page.margins.left, doc.y)
|
||||||
|
.lineTo(doc.page.width - doc.page.margins.right, doc.y)
|
||||||
|
.lineWidth(1)
|
||||||
|
.strokeColor("#E5E7EB")
|
||||||
|
.stroke()
|
||||||
|
.moveDown(0.8);
|
||||||
|
|
||||||
|
const pageLeft = doc.page.margins.left;
|
||||||
|
const pageRight = doc.page.width - doc.page.margins.right;
|
||||||
|
const gap = 24;
|
||||||
|
const qrSize = 120;
|
||||||
|
const rightX = pageRight - qrSize;
|
||||||
|
const leftWidth = pageRight - pageLeft - qrSize - gap;
|
||||||
|
const sectionTitle = (title: string) => {
|
||||||
|
doc
|
||||||
|
.font("Helvetica-Bold")
|
||||||
|
.fontSize(12)
|
||||||
|
.fillColor("#111827")
|
||||||
|
.text(title, pageLeft, doc.y)
|
||||||
|
.moveDown(0.4);
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeFieldAt = (
|
||||||
|
x: number,
|
||||||
|
width: number,
|
||||||
|
label: string,
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
|
doc
|
||||||
|
.font("Helvetica-Bold")
|
||||||
|
.fontSize(10)
|
||||||
|
.fillColor("#111827")
|
||||||
|
.text(`${label} `, x, doc.y, { continued: true, width });
|
||||||
|
doc.font("Helvetica").fillColor("#111827").text(value, { width });
|
||||||
|
doc.moveDown(0.3);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Summary + QR
|
||||||
|
const summaryStartY = doc.y + 6;
|
||||||
|
doc.image(qrPng, rightX, summaryStartY, {
|
||||||
|
width: qrSize,
|
||||||
|
height: qrSize,
|
||||||
|
});
|
||||||
|
doc
|
||||||
|
.font("Helvetica")
|
||||||
|
.fontSize(8)
|
||||||
|
.fillColor("#6B7280")
|
||||||
|
.text("Verification QR", rightX, summaryStartY + qrSize + 4, {
|
||||||
|
width: qrSize,
|
||||||
|
align: "center",
|
||||||
|
});
|
||||||
|
|
||||||
|
doc.y = summaryStartY;
|
||||||
|
doc
|
||||||
|
.font("Helvetica-Bold")
|
||||||
|
.fontSize(12)
|
||||||
|
.fillColor("#111827")
|
||||||
|
.text("Certificate Summary", pageLeft, doc.y, { width: leftWidth })
|
||||||
|
.moveDown(0.4);
|
||||||
|
|
||||||
|
writeFieldAt(
|
||||||
|
pageLeft,
|
||||||
|
leftWidth,
|
||||||
|
"Certificate ID:",
|
||||||
|
certificate.certificateId,
|
||||||
|
);
|
||||||
|
writeFieldAt(
|
||||||
|
pageLeft,
|
||||||
|
leftWidth,
|
||||||
|
"Contract:",
|
||||||
|
certificate.contractTitle || certificate.contractFileName,
|
||||||
|
);
|
||||||
|
writeFieldAt(pageLeft, leftWidth, "Issued At:", certificate.issuedAt);
|
||||||
|
writeFieldAt(pageLeft, leftWidth, "Network:", certificate.network);
|
||||||
|
|
||||||
|
doc.y = Math.max(doc.y, summaryStartY + qrSize + 24);
|
||||||
|
doc.moveDown(0.4);
|
||||||
|
|
||||||
|
// Document Proof
|
||||||
|
sectionTitle("Document Proof");
|
||||||
|
doc.font("Helvetica-Bold").fontSize(10).text("Document Hash:");
|
||||||
|
doc
|
||||||
|
.font("Courier")
|
||||||
|
.fontSize(9)
|
||||||
|
.fillColor("#111827")
|
||||||
|
.text(certificate.documentHash, { lineGap: 2 })
|
||||||
|
.moveDown(0.6);
|
||||||
|
|
||||||
|
// Blockchain Proof
|
||||||
|
sectionTitle("Blockchain Proof");
|
||||||
|
doc.font("Helvetica-Bold").fontSize(10).text("Transaction Hash:");
|
||||||
|
doc
|
||||||
|
.font("Courier")
|
||||||
|
.fontSize(9)
|
||||||
|
.fillColor("#111827")
|
||||||
|
.text(certificate.txHash, { lineGap: 2 })
|
||||||
|
.moveDown(0.4);
|
||||||
|
|
||||||
|
writeFieldAt(
|
||||||
|
pageLeft,
|
||||||
|
pageRight - pageLeft,
|
||||||
|
"Block Number:",
|
||||||
|
String(certificate.blockNumber),
|
||||||
|
);
|
||||||
|
writeFieldAt(
|
||||||
|
pageLeft,
|
||||||
|
pageRight - pageLeft,
|
||||||
|
"Block Timestamp:",
|
||||||
|
certificate.blockTimestamp,
|
||||||
|
);
|
||||||
|
writeFieldAt(
|
||||||
|
pageLeft,
|
||||||
|
pageRight - pageLeft,
|
||||||
|
"Contract Address:",
|
||||||
|
certificate.contractAddress,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Issuer and Signature
|
||||||
|
doc.moveDown(0.2);
|
||||||
|
sectionTitle("Issuer and Signature");
|
||||||
|
writeFieldAt(
|
||||||
|
pageLeft,
|
||||||
|
pageRight - pageLeft,
|
||||||
|
"Issued By:",
|
||||||
|
certificate.issuer,
|
||||||
|
);
|
||||||
|
|
||||||
|
doc.font("Helvetica-Bold").fontSize(10).text("Digital Signature:");
|
||||||
|
doc
|
||||||
|
.font("Courier")
|
||||||
|
.fontSize(8.5)
|
||||||
|
.fillColor("#111827")
|
||||||
|
.text(certificate.signature || "Signature not available", {
|
||||||
|
lineGap: 2,
|
||||||
|
})
|
||||||
|
.moveDown(0.6);
|
||||||
|
|
||||||
|
doc
|
||||||
|
.font("Helvetica")
|
||||||
|
.fontSize(9)
|
||||||
|
.fillColor("#374151")
|
||||||
|
.text(
|
||||||
|
"This certificate verifies that the document referenced above was registered on the blockchain and can be independently verified using the issuer address and signature.",
|
||||||
|
{ align: "left" },
|
||||||
|
)
|
||||||
|
.moveDown(0.4);
|
||||||
|
|
||||||
|
doc
|
||||||
|
.font("Helvetica")
|
||||||
|
.fontSize(8)
|
||||||
|
.fillColor("#6B7280")
|
||||||
|
.text("LexiChain - Confidential and tamper-evident certification.", {
|
||||||
|
align: "center",
|
||||||
|
});
|
||||||
|
|
||||||
|
doc.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,16 @@ interface ContractAnalysisEmailInput {
|
|||||||
blockchain?: BlockchainEmailData | null;
|
blockchain?: BlockchainEmailData | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ContractDeadlineEmailInput {
|
||||||
|
to: string;
|
||||||
|
userDisplayName?: string | null;
|
||||||
|
contractId: string;
|
||||||
|
contractTitle: string | null;
|
||||||
|
contractProvider: string | null;
|
||||||
|
contractEndDate: Date;
|
||||||
|
daysUntilExpiration: number;
|
||||||
|
}
|
||||||
|
|
||||||
let transporter: nodemailer.Transporter | null = null;
|
let transporter: nodemailer.Transporter | null = null;
|
||||||
let transportMode: "smtp" | "ethereal" | null = null;
|
let transportMode: "smtp" | "ethereal" | null = null;
|
||||||
let hasWarnedMissingEmailConfig = false;
|
let hasWarnedMissingEmailConfig = false;
|
||||||
@@ -134,6 +144,17 @@ const formatContractLink = (contractId: string): string | null => {
|
|||||||
return `${baseUrl.replace(/\/$/, "")}/contacts?contract=${contractId}`;
|
return `${baseUrl.replace(/\/$/, "")}/contacts?contract=${contractId}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getBaseUrl = (): string | null => {
|
||||||
|
const baseUrl =
|
||||||
|
process.env.NEXT_PUBLIC_APP_URL?.trim() || process.env.APP_URL?.trim();
|
||||||
|
return baseUrl ? baseUrl.replace(/\/$/, "") : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLogoUrl = (): string | null => {
|
||||||
|
const baseUrl = getBaseUrl();
|
||||||
|
return baseUrl ? `${baseUrl}/LexiChain.png` : null;
|
||||||
|
};
|
||||||
|
|
||||||
export class EmailService {
|
export class EmailService {
|
||||||
static async sendContractAnalysisCompletedEmail(
|
static async sendContractAnalysisCompletedEmail(
|
||||||
input: ContractAnalysisEmailInput,
|
input: ContractAnalysisEmailInput,
|
||||||
@@ -178,6 +199,7 @@ export class EmailService {
|
|||||||
input.blueprint.premiumCurrency,
|
input.blueprint.premiumCurrency,
|
||||||
);
|
);
|
||||||
const contractUrl = formatContractLink(input.contractId);
|
const contractUrl = formatContractLink(input.contractId);
|
||||||
|
const logoUrl = getLogoUrl();
|
||||||
const blockchainStatus = input.blockchain
|
const blockchainStatus = input.blockchain
|
||||||
? "Registered"
|
? "Registered"
|
||||||
: "Not registered (blockchain unavailable or skipped)";
|
: "Not registered (blockchain unavailable or skipped)";
|
||||||
@@ -185,7 +207,7 @@ export class EmailService {
|
|||||||
const textBody = [
|
const textBody = [
|
||||||
`Hello ${recipientName},`,
|
`Hello ${recipientName},`,
|
||||||
"",
|
"",
|
||||||
"Your contract analysis is complete.",
|
"Your LexiChain contract intelligence report is ready.",
|
||||||
"",
|
"",
|
||||||
"Blueprint:",
|
"Blueprint:",
|
||||||
`- Contract title: ${input.contractTitle}`,
|
`- Contract title: ${input.contractTitle}`,
|
||||||
@@ -197,7 +219,7 @@ export class EmailService {
|
|||||||
`- End date: ${formatDateValue(input.blueprint.endDate)}`,
|
`- End date: ${formatDateValue(input.blueprint.endDate)}`,
|
||||||
`- Premium: ${premiumLabel}`,
|
`- Premium: ${premiumLabel}`,
|
||||||
"",
|
"",
|
||||||
"Summary:",
|
"Executive summary:",
|
||||||
input.blueprint.summary,
|
input.blueprint.summary,
|
||||||
"",
|
"",
|
||||||
"Blockchain proof:",
|
"Blockchain proof:",
|
||||||
@@ -212,46 +234,97 @@ export class EmailService {
|
|||||||
"",
|
"",
|
||||||
contractUrl ? `Open in app: ${contractUrl}` : "",
|
contractUrl ? `Open in app: ${contractUrl}` : "",
|
||||||
"",
|
"",
|
||||||
"Keep this email for your records.",
|
"Thank you for trusting LexiChain for secure contract governance.",
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
const htmlBody = `
|
const htmlBody = `
|
||||||
<div style="font-family: Arial, sans-serif; line-height: 1.5; color: #0f172a;">
|
<div style="margin:0;padding:0;background:#0f172a;">
|
||||||
<h2 style="margin-bottom: 12px;">Contract Analysis Completed</h2>
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background:#0f172a;padding:24px 0;">
|
||||||
<p>Hello ${recipientName},</p>
|
<tr>
|
||||||
<p>Your contract analysis has been completed successfully.</p>
|
<td align="center">
|
||||||
|
<table role="presentation" width="640" cellspacing="0" cellpadding="0" style="background:#ffffff;border-radius:20px;overflow:hidden;font-family:Arial, sans-serif;color:#0f172a;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:24px 28px;background:linear-gradient(135deg,#0f172a 0%,#1d4ed8 100%);color:#ffffff;">
|
||||||
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="vertical-align:middle;">
|
||||||
|
${logoUrl ? `<img src="${logoUrl}" alt="LexiChain" width="140" style="display:block;border-radius:10px;" />` : '<strong style="font-size:20px;">LexiChain</strong>'}
|
||||||
|
</td>
|
||||||
|
<td align="right" style="vertical-align:middle;font-size:12px;letter-spacing:1.4px;text-transform:uppercase;opacity:0.85;">
|
||||||
|
Contract Intelligence
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<h1 style="margin:20px 0 6px;font-size:26px;">Your contract insight report is ready</h1>
|
||||||
|
<p style="margin:0;font-size:14px;opacity:0.9;">Clear, verified, and ready for action.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<h3 style="margin-top: 24px; margin-bottom: 8px;">Blueprint</h3>
|
<tr>
|
||||||
<ul>
|
<td style="padding:24px 28px;">
|
||||||
<li><strong>Contract title:</strong> ${input.contractTitle}</li>
|
<p style="margin:0 0 16px;font-size:14px;">Hello ${recipientName},</p>
|
||||||
<li><strong>Original file:</strong> ${input.contractFileName}</li>
|
<p style="margin:0 0 20px;font-size:14px;color:#334155;">
|
||||||
<li><strong>Type:</strong> ${input.blueprint.type}</li>
|
Your LexiChain analysis is complete. Below is the executive blueprint and proof trace.
|
||||||
<li><strong>Provider:</strong> ${input.blueprint.provider ?? "N/A"}</li>
|
</p>
|
||||||
<li><strong>Policy number:</strong> ${input.blueprint.policyNumber ?? "N/A"}</li>
|
|
||||||
<li><strong>Start date:</strong> ${formatDateValue(input.blueprint.startDate)}</li>
|
|
||||||
<li><strong>End date:</strong> ${formatDateValue(input.blueprint.endDate)}</li>
|
|
||||||
<li><strong>Premium:</strong> ${premiumLabel}</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3 style="margin-top: 24px; margin-bottom: 8px;">Summary</h3>
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:14px;padding:16px;">
|
||||||
<p>${input.blueprint.summary.replace(/\n/g, "<br />")}</p>
|
<tr>
|
||||||
|
<td style="font-size:13px;font-weight:bold;padding-bottom:12px;">Blueprint Summary</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:13px;color:#475569;">
|
||||||
|
<div><strong>Contract:</strong> ${input.contractTitle}</div>
|
||||||
|
<div><strong>File:</strong> ${input.contractFileName}</div>
|
||||||
|
<div><strong>Type:</strong> ${input.blueprint.type}</div>
|
||||||
|
<div><strong>Provider:</strong> ${input.blueprint.provider ?? "N/A"}</div>
|
||||||
|
<div><strong>Policy #:</strong> ${input.blueprint.policyNumber ?? "N/A"}</div>
|
||||||
|
<div><strong>Start:</strong> ${formatDateValue(input.blueprint.startDate)}</div>
|
||||||
|
<div><strong>End:</strong> ${formatDateValue(input.blueprint.endDate)}</div>
|
||||||
|
<div><strong>Premium:</strong> ${premiumLabel}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
<h3 style="margin-top: 24px; margin-bottom: 8px;">Blockchain Proof</h3>
|
<div style="margin:18px 0 8px;font-size:13px;font-weight:bold;">Executive Summary</div>
|
||||||
<ul>
|
<p style="margin:0 0 18px;font-size:13px;color:#475569;line-height:1.6;">${input.blueprint.summary.replace(/\n/g, "<br />")}</p>
|
||||||
<li><strong>Status:</strong> ${blockchainStatus}</li>
|
|
||||||
<li><strong>Document hash:</strong> ${input.blockchain?.documentHash ?? "N/A"}</li>
|
|
||||||
<li><strong>Transaction hash:</strong> ${input.blockchain?.txHash ?? "N/A"}</li>
|
|
||||||
<li><strong>Block number:</strong> ${input.blockchain?.blockNumber ?? "N/A"}</li>
|
|
||||||
<li><strong>Block time:</strong> ${input.blockchain?.blockTimestamp?.toISOString() ?? "N/A"}</li>
|
|
||||||
<li><strong>Network:</strong> ${input.blockchain?.network ?? "N/A"}</li>
|
|
||||||
<li><strong>Contract address:</strong> ${input.blockchain?.contractAddress ?? "N/A"}</li>
|
|
||||||
<li><strong>Explorer URL:</strong> ${input.blockchain?.explorerUrl ? `<a href="${input.blockchain.explorerUrl}" target="_blank" rel="noopener noreferrer">Open transaction</a>` : "N/A"}</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
${contractUrl ? `<p><a href="${contractUrl}">Open this contract in your dashboard</a></p>` : ""}
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background:#eef2ff;border:1px solid #c7d2fe;border-radius:14px;padding:16px;">
|
||||||
<p style="margin-top: 24px; font-size: 12px; color: #475569;">Keep this email for your records.</p>
|
<tr>
|
||||||
|
<td style="font-size:13px;font-weight:bold;padding-bottom:12px;color:#1e3a8a;">Blockchain Proof</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:12px;color:#1e3a8a;line-height:1.6;">
|
||||||
|
<div><strong>Status:</strong> ${blockchainStatus}</div>
|
||||||
|
<div><strong>Document hash:</strong> ${input.blockchain?.documentHash ?? "N/A"}</div>
|
||||||
|
<div><strong>Transaction hash:</strong> ${input.blockchain?.txHash ?? "N/A"}</div>
|
||||||
|
<div><strong>Block number:</strong> ${input.blockchain?.blockNumber ?? "N/A"}</div>
|
||||||
|
<div><strong>Block time:</strong> ${input.blockchain?.blockTimestamp?.toISOString() ?? "N/A"}</div>
|
||||||
|
<div><strong>Network:</strong> ${input.blockchain?.network ?? "N/A"}</div>
|
||||||
|
<div><strong>Contract address:</strong> ${input.blockchain?.contractAddress ?? "N/A"}</div>
|
||||||
|
<div><strong>Explorer:</strong> ${input.blockchain?.explorerUrl ? `<a href="${input.blockchain.explorerUrl}" target="_blank" rel="noopener noreferrer" style="color:#1d4ed8;text-decoration:none;">Open transaction</a>` : "N/A"}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
${
|
||||||
|
contractUrl
|
||||||
|
? `
|
||||||
|
<div style="margin-top:22px;">
|
||||||
|
<a href="${contractUrl}" style="display:inline-block;background:#1d4ed8;color:#ffffff;text-decoration:none;padding:12px 18px;border-radius:10px;font-weight:bold;font-size:13px;">Open in LexiChain</a>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
<p style="margin:20px 0 0;font-size:12px;color:#94a3b8;">Precision you can audit. Trust you can prove.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -259,11 +332,14 @@ export class EmailService {
|
|||||||
from,
|
from,
|
||||||
to: input.to,
|
to: input.to,
|
||||||
subject: `Contract analyzed: ${input.contractTitle}`,
|
subject: `Contract analyzed: ${input.contractTitle}`,
|
||||||
text: textBody,
|
|
||||||
html: htmlBody,
|
html: htmlBody,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/html; charset=utf-8",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const previewUrl = nodemailer.getTestMessageUrl(info);
|
const previewUrl =
|
||||||
|
(nodemailer.getTestMessageUrl(info) as string | false) || null;
|
||||||
if (previewUrl) {
|
if (previewUrl) {
|
||||||
console.log(`📨 Ethereal preview URL: ${previewUrl}`);
|
console.log(`📨 Ethereal preview URL: ${previewUrl}`);
|
||||||
}
|
}
|
||||||
@@ -277,4 +353,160 @@ export class EmailService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async sendContractDeadlineReminderEmail(
|
||||||
|
input: ContractDeadlineEmailInput,
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
skipped?: boolean;
|
||||||
|
previewUrl?: string | null;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const mailer = await getTransporter();
|
||||||
|
if (!mailer) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
skipped: true,
|
||||||
|
error: "Email service not configured",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const from =
|
||||||
|
process.env.MAIL_FROM?.trim() ||
|
||||||
|
process.env.EMAIL_USER?.trim() ||
|
||||||
|
(transportMode === "ethereal"
|
||||||
|
? "LexiChain <no-reply@ethereal.email>"
|
||||||
|
: "");
|
||||||
|
if (!from) {
|
||||||
|
warnMissingEmailConfigOnce();
|
||||||
|
return { success: false, skipped: true, error: "MAIL_FROM is missing" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!input.to?.trim()) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
skipped: true,
|
||||||
|
error: "Recipient email is missing",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipientName = input.userDisplayName || "there";
|
||||||
|
const contractUrl = formatContractLink(input.contractId);
|
||||||
|
const logoUrl = getLogoUrl();
|
||||||
|
const endDate = input.contractEndDate.toLocaleDateString();
|
||||||
|
|
||||||
|
const urgencyLabel =
|
||||||
|
input.daysUntilExpiration <= 7
|
||||||
|
? "Urgent"
|
||||||
|
: input.daysUntilExpiration <= 14
|
||||||
|
? "High"
|
||||||
|
: "Planned";
|
||||||
|
|
||||||
|
const textBody = [
|
||||||
|
`Hello ${recipientName},`,
|
||||||
|
"",
|
||||||
|
`Your contract deadline is approaching in ${input.daysUntilExpiration} days.`,
|
||||||
|
"",
|
||||||
|
`Contract: ${input.contractTitle ?? "Untitled contract"}`,
|
||||||
|
`Provider: ${input.contractProvider ?? "N/A"}`,
|
||||||
|
`End date: ${endDate}`,
|
||||||
|
`Urgency: ${urgencyLabel}`,
|
||||||
|
"",
|
||||||
|
contractUrl ? `Open in app: ${contractUrl}` : "",
|
||||||
|
"",
|
||||||
|
"Please review the contract and schedule renewal if needed.",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const htmlBody = `
|
||||||
|
<div style="margin:0;padding:0;background:#0f172a;">
|
||||||
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background:#0f172a;padding:24px 0;">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table role="presentation" width="640" cellspacing="0" cellpadding="0" style="background:#ffffff;border-radius:20px;overflow:hidden;font-family:Arial, sans-serif;color:#0f172a;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:22px 28px;background:linear-gradient(135deg,#0f172a 0%,#2563eb 100%);color:#ffffff;">
|
||||||
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="vertical-align:middle;">
|
||||||
|
${logoUrl ? `<img src="${logoUrl}" alt="LexiChain" width="130" style="display:block;border-radius:10px;" />` : '<strong style="font-size:18px;">LexiChain</strong>'}
|
||||||
|
</td>
|
||||||
|
<td align="right" style="vertical-align:middle;font-size:12px;letter-spacing:1.4px;text-transform:uppercase;opacity:0.85;">
|
||||||
|
Deadline Alert
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<h1 style="margin:18px 0 6px;font-size:24px;">Contract renewal reminder</h1>
|
||||||
|
<p style="margin:0;font-size:14px;opacity:0.9;">Stay ahead of critical dates.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:24px 28px;">
|
||||||
|
<p style="margin:0 0 14px;font-size:14px;">Hello ${recipientName},</p>
|
||||||
|
<p style="margin:0 0 18px;font-size:14px;color:#334155;">
|
||||||
|
Your contract deadline is approaching in <strong>${input.daysUntilExpiration} days</strong>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:14px;padding:16px;">
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:13px;font-weight:bold;padding-bottom:12px;">Deadline Summary</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:13px;color:#475569;line-height:1.6;">
|
||||||
|
<div><strong>Contract:</strong> ${input.contractTitle ?? "Untitled contract"}</div>
|
||||||
|
<div><strong>Provider:</strong> ${input.contractProvider ?? "N/A"}</div>
|
||||||
|
<div><strong>End date:</strong> ${endDate}</div>
|
||||||
|
<div><strong>Urgency:</strong> ${urgencyLabel}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
${
|
||||||
|
contractUrl
|
||||||
|
? `
|
||||||
|
<div style="margin-top:20px;">
|
||||||
|
<a href="${contractUrl}" style="display:inline-block;background:#1d4ed8;color:#ffffff;text-decoration:none;padding:12px 18px;border-radius:10px;font-weight:bold;font-size:13px;">Review in LexiChain</a>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
<p style="margin:18px 0 0;font-size:12px;color:#94a3b8;">Plan renewals early to avoid coverage gaps.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const info = await mailer.sendMail({
|
||||||
|
from,
|
||||||
|
to: input.to,
|
||||||
|
subject: `Contract deadline in ${input.daysUntilExpiration} days`,
|
||||||
|
html: htmlBody,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/html; charset=utf-8",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const previewUrl =
|
||||||
|
(nodemailer.getTestMessageUrl(info) as string | false) || null;
|
||||||
|
if (previewUrl) {
|
||||||
|
console.log(`📨 Ethereal preview URL: ${previewUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, previewUrl };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to send deadline reminder email:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : "Unknown email error",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { prisma } from "@/lib/db/prisma";
|
import { prisma } from "@/lib/db/prisma";
|
||||||
|
import { EmailService } from "@/lib/services/email.service";
|
||||||
|
|
||||||
let hasWarnedMissingNotificationTable = false;
|
let hasWarnedMissingNotificationTable = false;
|
||||||
|
|
||||||
@@ -547,9 +548,9 @@ export class NotificationService {
|
|||||||
* Checks for upcoming contract renewals/expirations and creates notifications
|
* Checks for upcoming contract renewals/expirations and creates notifications
|
||||||
*
|
*
|
||||||
* Scans all contracts for a user and creates DEADLINE notifications for:
|
* Scans all contracts for a user and creates DEADLINE notifications for:
|
||||||
* - 30 days before expiration (CRITICAL)
|
* - 30 days before expiration
|
||||||
* - 15 days before expiration (WARNING)
|
* - 14 days before expiration
|
||||||
* - 7 days before expiration (URGENT)
|
* - 7 days before expiration
|
||||||
*
|
*
|
||||||
* @param userId - The user's ID
|
* @param userId - The user's ID
|
||||||
* @returns Promise with count of created notifications
|
* @returns Promise with count of created notifications
|
||||||
@@ -557,7 +558,7 @@ export class NotificationService {
|
|||||||
* Steps:
|
* Steps:
|
||||||
* 1. Query all COMPLETED contracts with endDate for the user
|
* 1. Query all COMPLETED contracts with endDate for the user
|
||||||
* 2. Calculate days until expiration
|
* 2. Calculate days until expiration
|
||||||
* 3. Create notification if contract expiring in 30, 15, or 7 days
|
* 3. Create notification if contract expiring in 30, 14, or 7 days
|
||||||
* 4. Check for existing notification to avoid duplicates
|
* 4. Check for existing notification to avoid duplicates
|
||||||
* 5. Return summary of created notifications
|
* 5. Return summary of created notifications
|
||||||
*
|
*
|
||||||
@@ -573,6 +574,15 @@ export class NotificationService {
|
|||||||
const today = new Date();
|
const today = new Date();
|
||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
select: {
|
||||||
|
email: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Query all contracts with endDate for this user
|
// Query all contracts with endDate for this user
|
||||||
const contracts = await prisma.contract.findMany({
|
const contracts = await prisma.contract.findMany({
|
||||||
where: {
|
where: {
|
||||||
@@ -610,12 +620,12 @@ export class NotificationService {
|
|||||||
if (daysUntilExpiration === 7) {
|
if (daysUntilExpiration === 7) {
|
||||||
shouldNotify = true;
|
shouldNotify = true;
|
||||||
level = "URGENT";
|
level = "URGENT";
|
||||||
} else if (daysUntilExpiration === 15) {
|
} else if (daysUntilExpiration === 14) {
|
||||||
shouldNotify = true;
|
shouldNotify = true;
|
||||||
level = "WARNING";
|
level = "WARNING";
|
||||||
} else if (daysUntilExpiration === 30) {
|
} else if (daysUntilExpiration === 30) {
|
||||||
shouldNotify = true;
|
shouldNotify = true;
|
||||||
level = "CRITICAL";
|
level = "NOTICE";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldNotify) {
|
if (shouldNotify) {
|
||||||
@@ -634,18 +644,21 @@ export class NotificationService {
|
|||||||
// Only create if not already notified today
|
// Only create if not already notified today
|
||||||
if (!existingNotification) {
|
if (!existingNotification) {
|
||||||
const notificationTitle =
|
const notificationTitle =
|
||||||
level === "CRITICAL"
|
level === "NOTICE"
|
||||||
? `🔴 Contract Expiring in 30 Days`
|
? "Contract renewal reminder (30 days)"
|
||||||
: level === "WARNING"
|
: level === "WARNING"
|
||||||
? `🟠 Contract Expiring in 15 Days`
|
? "Contract renewal window (14 days)"
|
||||||
: `🟡 Contract Expiring in 7 Days`;
|
: "Contract deadline in 7 days";
|
||||||
|
|
||||||
|
const providerLabel = contract.provider || "your provider";
|
||||||
|
const contractLabel = contract.title || "your contract";
|
||||||
|
|
||||||
const notificationMessage =
|
const notificationMessage =
|
||||||
level === "CRITICAL"
|
level === "NOTICE"
|
||||||
? `${contract.title} from ${contract.provider} will expire on ${contractEnd.toLocaleDateString()}. Time to renew!`
|
? `${contractLabel} from ${providerLabel} will expire on ${contractEnd.toLocaleDateString()}. Plan your renewal.`
|
||||||
: level === "WARNING"
|
: level === "WARNING"
|
||||||
? `${contract.title} from ${contract.provider} expires in 15 days. Consider scheduling renewal.`
|
? `${contractLabel} from ${providerLabel} expires in 14 days. Please review renewal steps.`
|
||||||
: `${contract.title} from ${contract.provider} expires in 7 days. Renew now!`;
|
: `${contractLabel} from ${providerLabel} expires in 7 days. Action is required.`;
|
||||||
|
|
||||||
const result = await this.create({
|
const result = await this.create({
|
||||||
userId,
|
userId,
|
||||||
@@ -654,7 +667,7 @@ export class NotificationService {
|
|||||||
message: notificationMessage,
|
message: notificationMessage,
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
actionType: `RENEWAL_${level}`,
|
actionType: `RENEWAL_${level}`,
|
||||||
icon: level === "CRITICAL" ? "AlertCircle" : "AlertTriangle",
|
icon: level === "URGENT" ? "AlertCircle" : "AlertTriangle",
|
||||||
expiresIn: 24 * 60 * 60 * 1000, // 24 hours
|
expiresIn: 24 * 60 * 60 * 1000, // 24 hours
|
||||||
actionData: {
|
actionData: {
|
||||||
level,
|
level,
|
||||||
@@ -667,6 +680,29 @@ export class NotificationService {
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
createdNotifications.push(contract.id);
|
createdNotifications.push(contract.id);
|
||||||
|
|
||||||
|
if (user?.email) {
|
||||||
|
try {
|
||||||
|
await EmailService.sendContractDeadlineReminderEmail({
|
||||||
|
to: user.email,
|
||||||
|
userDisplayName:
|
||||||
|
`${user.firstName ?? ""} ${user.lastName ?? ""}`.trim() ||
|
||||||
|
null,
|
||||||
|
contractId: contract.id,
|
||||||
|
contractTitle: contract.title,
|
||||||
|
contractProvider: contract.provider,
|
||||||
|
contractEndDate: contractEnd,
|
||||||
|
daysUntilExpiration,
|
||||||
|
});
|
||||||
|
} catch (emailError) {
|
||||||
|
console.warn(
|
||||||
|
"Deadline email failed:",
|
||||||
|
emailError instanceof Error
|
||||||
|
? emailError.message
|
||||||
|
: emailError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
output: "standalone",
|
||||||
|
serverExternalPackages: ["pdfkit"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
470
package-lock.json
generated
470
package-lock.json
generated
@@ -58,7 +58,9 @@
|
|||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"nodemailer": "^8.0.7",
|
"nodemailer": "^8.0.7",
|
||||||
"pdf-parse": "^2.4.5",
|
"pdf-parse": "^2.4.5",
|
||||||
|
"pdfkit": "^0.16.0",
|
||||||
"prisma": "^6.19.2",
|
"prisma": "^6.19.2",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-day-picker": "^9.13.2",
|
"react-day-picker": "^9.13.2",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
@@ -83,6 +85,9 @@
|
|||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20 <23"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@adraffy/ens-normalize": {
|
"node_modules/@adraffy/ens-normalize": {
|
||||||
@@ -4402,6 +4407,30 @@
|
|||||||
"url": "https://github.com/sponsors/epoberezkin"
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/any-promise": {
|
"node_modules/any-promise": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||||
@@ -4722,6 +4751,26 @@
|
|||||||
"node": ">= 0.6.0"
|
"node": ">= 0.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.10.0",
|
"version": "2.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
|
||||||
@@ -4773,6 +4822,30 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/brotli": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/browserify-zlib": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pako": "~1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/browserify-zlib/node_modules/pako": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||||
|
"license": "(MIT AND Zlib)"
|
||||||
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.28.1",
|
"version": "4.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
||||||
@@ -4897,6 +4970,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/camelcase": {
|
||||||
|
"version": "5.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||||
|
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/camelcase-css": {
|
"node_modules/camelcase-css": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
|
||||||
@@ -4989,6 +5071,26 @@
|
|||||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cliui": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"strip-ansi": "^6.0.0",
|
||||||
|
"wrap-ansi": "^6.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/clone": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/clsx": {
|
"node_modules/clsx": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
@@ -5014,6 +5116,24 @@
|
|||||||
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||||
@@ -5080,6 +5200,12 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crypto-js": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/css-line-break": {
|
"node_modules/css-line-break": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||||
@@ -5325,6 +5451,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decamelize": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/decimal.js-light": {
|
"node_modules/decimal.js-light": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||||
@@ -5420,6 +5555,12 @@
|
|||||||
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dfa": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/didyoumean": {
|
"node_modules/didyoumean": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||||
@@ -5427,6 +5568,12 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/dijkstrajs": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/dlv": {
|
"node_modules/dlv": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
|
||||||
@@ -6288,7 +6435,6 @@
|
|||||||
"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",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
@@ -6450,6 +6596,23 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/fontkit": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@swc/helpers": "^0.5.12",
|
||||||
|
"brotli": "^1.3.2",
|
||||||
|
"clone": "^2.1.2",
|
||||||
|
"dfa": "^1.2.0",
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"restructure": "^3.0.0",
|
||||||
|
"tiny-inflate": "^1.0.3",
|
||||||
|
"unicode-properties": "^1.4.0",
|
||||||
|
"unicode-trie": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/for-each": {
|
"node_modules/for-each": {
|
||||||
"version": "0.3.5",
|
"version": "0.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||||
@@ -6582,6 +6745,15 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-caller-file": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-intrinsic": {
|
"node_modules/get-intrinsic": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
@@ -7122,6 +7294,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-generator-function": {
|
"node_modules/is-generator-function": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
|
||||||
@@ -7394,6 +7575,13 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jpeg-exif": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==",
|
||||||
|
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/js-cookie": {
|
"node_modules/js-cookie": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||||
@@ -7556,6 +7744,25 @@
|
|||||||
"url": "https://github.com/sponsors/antonk52"
|
"url": "https://github.com/sponsors/antonk52"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/linebreak": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "0.0.8",
|
||||||
|
"unicode-trie": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/linebreak/node_modules/base64-js": {
|
||||||
|
"version": "0.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
|
||||||
|
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"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",
|
||||||
@@ -8196,6 +8403,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/p-try": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pako": {
|
"node_modules/pako": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
||||||
@@ -8206,7 +8422,6 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -8267,6 +8482,19 @@
|
|||||||
"@napi-rs/canvas": "^0.1.80"
|
"@napi-rs/canvas": "^0.1.80"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pdfkit": {
|
||||||
|
"version": "0.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.16.0.tgz",
|
||||||
|
"integrity": "sha512-oXMxkIqXH4uTAtohWdYA41i/f6i2ReB78uhgizN8H4hJEpgR3/Xjy3iu2InNAuwCIabN3PVs8P1D6G4+W2NH0A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"fontkit": "^2.0.4",
|
||||||
|
"jpeg-exif": "^1.1.4",
|
||||||
|
"linebreak": "^1.1.0",
|
||||||
|
"png-js": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/perfect-debounce": {
|
"node_modules/perfect-debounce": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||||
@@ -8330,6 +8558,23 @@
|
|||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/png-js": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/png-js/-/png-js-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-PM/uYGzGdNSzqeOgly68+6wKQDL1SY0a/N+OEa/+br6LnHWOAJB0Npiamnodfq3jd2LS/i2fMeOKSAILjA+m5Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"browserify-zlib": "^0.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pngjs": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/possible-typed-array-names": {
|
"node_modules/possible-typed-array-names": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||||
@@ -8583,6 +8828,23 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/qrcode": {
|
||||||
|
"version": "1.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||||
|
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dijkstrajs": "^1.0.1",
|
||||||
|
"pngjs": "^5.0.0",
|
||||||
|
"yargs": "^15.3.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"qrcode": "bin/qrcode"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/queue-microtask": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
@@ -8910,6 +9172,21 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-directory": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/require-main-filename": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/reselect": {
|
"node_modules/reselect": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||||
@@ -8947,6 +9224,12 @@
|
|||||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/restructure": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/reusify": {
|
"node_modules/reusify": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||||
@@ -9069,6 +9352,12 @@
|
|||||||
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
|
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/set-blocking": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/set-function-length": {
|
"node_modules/set-function-length": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||||
@@ -9347,6 +9636,26 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string-width": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^8.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string-width/node_modules/emoji-regex": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/string.prototype.includes": {
|
"node_modules/string.prototype.includes": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
|
||||||
@@ -9460,6 +9769,18 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/strip-ansi": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/strip-bom": {
|
"node_modules/strip-bom": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
||||||
@@ -9714,6 +10035,12 @@
|
|||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-inflate": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tiny-invariant": {
|
"node_modules/tiny-invariant": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||||
@@ -9983,6 +10310,32 @@
|
|||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/unicode-properties": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.0",
|
||||||
|
"unicode-trie": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unicode-trie": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pako": "^0.2.5",
|
||||||
|
"tiny-inflate": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unicode-trie/node_modules/pako": {
|
||||||
|
"version": "0.2.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
|
||||||
|
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/unrs-resolver": {
|
"node_modules/unrs-resolver": {
|
||||||
"version": "1.11.1",
|
"version": "1.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz",
|
||||||
@@ -10303,6 +10656,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/which-module": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/which-typed-array": {
|
"node_modules/which-typed-array": {
|
||||||
"version": "1.1.20",
|
"version": "1.1.20",
|
||||||
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
|
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
|
||||||
@@ -10335,6 +10694,20 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wrap-ansi": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.17.1",
|
"version": "8.17.1",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
@@ -10356,6 +10729,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/y18n": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
@@ -10363,6 +10742,93 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/yargs": {
|
||||||
|
"version": "15.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||||
|
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cliui": "^6.0.0",
|
||||||
|
"decamelize": "^1.2.0",
|
||||||
|
"find-up": "^4.1.0",
|
||||||
|
"get-caller-file": "^2.0.1",
|
||||||
|
"require-directory": "^2.1.1",
|
||||||
|
"require-main-filename": "^2.0.0",
|
||||||
|
"set-blocking": "^2.0.0",
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"which-module": "^2.0.0",
|
||||||
|
"y18n": "^4.0.0",
|
||||||
|
"yargs-parser": "^18.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs-parser": {
|
||||||
|
"version": "18.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||||
|
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"camelcase": "^5.0.0",
|
||||||
|
"decamelize": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs/node_modules/find-up": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"locate-path": "^5.0.0",
|
||||||
|
"path-exists": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs/node_modules/locate-path": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-locate": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs/node_modules/p-limit": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-try": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs/node_modules/p-locate": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-limit": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yocto-queue": {
|
"node_modules/yocto-queue": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
|
|||||||
@@ -2,8 +2,13 @@
|
|||||||
"name": "bfsi-project",
|
"name": "bfsi-project",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20 <23"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "node scripts/dev-with-chain.mjs",
|
||||||
|
"dev:next": "next dev",
|
||||||
|
"dev:with-chain": "node scripts/dev-with-chain.mjs",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
@@ -58,8 +63,10 @@
|
|||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"nodemailer": "^8.0.7",
|
"nodemailer": "^8.0.7",
|
||||||
|
"pdfkit": "^0.16.0",
|
||||||
"pdf-parse": "^2.4.5",
|
"pdf-parse": "^2.4.5",
|
||||||
"prisma": "^6.19.2",
|
"prisma": "^6.19.2",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-day-picker": "^9.13.2",
|
"react-day-picker": "^9.13.2",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
|
|||||||
BIN
public/screens/contracts_dark.png
Normal file
BIN
public/screens/contracts_dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 955 KiB |
BIN
public/screens/dashboard_dark.png
Normal file
BIN
public/screens/dashboard_dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 361 KiB |
BIN
public/screens/landing_page.png
Normal file
BIN
public/screens/landing_page.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
BIN
public/screens/login_dark.png
Normal file
BIN
public/screens/login_dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
BIN
public/screens/register_dark.png
Normal file
BIN
public/screens/register_dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
247
scripts/dev-with-chain.mjs
Normal file
247
scripts/dev-with-chain.mjs
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
import { spawn } from "child_process";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import path from "path";
|
||||||
|
import process from "process";
|
||||||
|
import { readFile, writeFile } from "fs/promises";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const rootDir = path.resolve(__dirname, "..");
|
||||||
|
const blockchainDir = path.join(rootDir, "blockchain");
|
||||||
|
|
||||||
|
const rpcUrl = "http://127.0.0.1:8545";
|
||||||
|
const defaultDevPrivateKey =
|
||||||
|
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
|
||||||
|
const bootstrapFlag = "__LEXICHAIN_CHAIN_BOOTSTRAPPED";
|
||||||
|
const deployStatePath = path.join(blockchainDir, ".dev-deploy.json");
|
||||||
|
|
||||||
|
function spawnProcess(command, args, options) {
|
||||||
|
return spawn(command, args, {
|
||||||
|
shell: true,
|
||||||
|
stdio: "inherit",
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForRpc(timeoutMs) {
|
||||||
|
const started = Date.now();
|
||||||
|
while (Date.now() - started < timeoutMs) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(rpcUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: 1,
|
||||||
|
method: "eth_blockNumber",
|
||||||
|
params: [],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (response.ok) return;
|
||||||
|
} catch {
|
||||||
|
// Ignore transient connection failures while the node starts.
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
}
|
||||||
|
throw new Error("Hardhat RPC did not become ready in time.");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isRpcReady() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(rpcUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: 1,
|
||||||
|
method: "eth_blockNumber",
|
||||||
|
params: [],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
return response.ok;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHexAddress(value) {
|
||||||
|
return /^0x[a-fA-F0-9]{40}$/.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hasContractCode(address) {
|
||||||
|
if (!isHexAddress(address)) return false;
|
||||||
|
try {
|
||||||
|
const response = await fetch(rpcUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: 1,
|
||||||
|
method: "eth_getCode",
|
||||||
|
params: [address, "latest"],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!response.ok) return false;
|
||||||
|
const payload = await response.json();
|
||||||
|
const code = typeof payload?.result === "string" ? payload.result : "0x";
|
||||||
|
return code !== "0x";
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readSavedAddress() {
|
||||||
|
try {
|
||||||
|
const raw = await readFile(deployStatePath, "utf8");
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
const address = parsed?.address;
|
||||||
|
return isHexAddress(address) ? address : "";
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveAddress(address) {
|
||||||
|
try {
|
||||||
|
await writeFile(
|
||||||
|
deployStatePath,
|
||||||
|
JSON.stringify({ address }, null, 2),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// Non-fatal in dev.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveContractAddress() {
|
||||||
|
const candidates = [];
|
||||||
|
if (process.env.BLOCKCHAIN_CONTRACT_ADDRESS) {
|
||||||
|
candidates.push(process.env.BLOCKCHAIN_CONTRACT_ADDRESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedAddress = await readSavedAddress();
|
||||||
|
if (savedAddress) candidates.push(savedAddress);
|
||||||
|
|
||||||
|
const checked = new Set();
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (!candidate || checked.has(candidate)) continue;
|
||||||
|
checked.add(candidate);
|
||||||
|
if (await hasContractCode(candidate)) {
|
||||||
|
console.log(`Using existing deployed contract: ${candidate}`);
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Deploying contract to localhost...");
|
||||||
|
const deployedAddress = await runDeploy();
|
||||||
|
await saveAddress(deployedAddress);
|
||||||
|
return deployedAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runDeploy() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const deploy = spawn(
|
||||||
|
"npx",
|
||||||
|
["hardhat", "run", "scripts/deploy.ts", "--network", "localhost"],
|
||||||
|
{
|
||||||
|
cwd: blockchainDir,
|
||||||
|
shell: true,
|
||||||
|
stdio: ["inherit", "pipe", "pipe"],
|
||||||
|
env: process.env,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let output = "";
|
||||||
|
deploy.stdout.on("data", (chunk) => {
|
||||||
|
const text = chunk.toString();
|
||||||
|
output += text;
|
||||||
|
process.stdout.write(text);
|
||||||
|
});
|
||||||
|
deploy.stderr.on("data", (chunk) => {
|
||||||
|
const text = chunk.toString();
|
||||||
|
output += text;
|
||||||
|
process.stderr.write(text);
|
||||||
|
});
|
||||||
|
|
||||||
|
deploy.on("close", (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
reject(new Error("Hardhat deploy failed."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addressMatch =
|
||||||
|
output.match(/BLOCKCHAIN_CONTRACT_ADDRESS=(0x[a-fA-F0-9]{40})/) ||
|
||||||
|
output.match(/deployed to:\s*(0x[a-fA-F0-9]{40})/i);
|
||||||
|
|
||||||
|
if (!addressMatch) {
|
||||||
|
reject(new Error("Could not detect deployed contract address."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(addressMatch[1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let nodeProcess;
|
||||||
|
let nextProcess;
|
||||||
|
let startedNodeHere = false;
|
||||||
|
|
||||||
|
function shutdown(code) {
|
||||||
|
if (nextProcess && !nextProcess.killed) {
|
||||||
|
nextProcess.kill("SIGINT");
|
||||||
|
}
|
||||||
|
if (startedNodeHere && nodeProcess && !nodeProcess.killed) {
|
||||||
|
nodeProcess.kill("SIGINT");
|
||||||
|
}
|
||||||
|
process.exit(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on("SIGINT", () => shutdown(0));
|
||||||
|
process.on("SIGTERM", () => shutdown(0));
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const rpcReady = await isRpcReady();
|
||||||
|
if (rpcReady) {
|
||||||
|
console.log("Detected existing Hardhat node on 8545. Reusing it.");
|
||||||
|
} else {
|
||||||
|
console.log("Starting Hardhat node...");
|
||||||
|
startedNodeHere = true;
|
||||||
|
nodeProcess = spawnProcess("npx", ["hardhat", "node"], {
|
||||||
|
cwd: blockchainDir,
|
||||||
|
});
|
||||||
|
await waitForRpc(30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contractAddress = await resolveContractAddress();
|
||||||
|
console.log(`Using contract address: ${contractAddress}`);
|
||||||
|
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
BLOCKCHAIN_NETWORK: "hardhat",
|
||||||
|
BLOCKCHAIN_RPC_URL: rpcUrl,
|
||||||
|
BLOCKCHAIN_CONTRACT_ADDRESS: contractAddress,
|
||||||
|
BLOCKCHAIN_PRIVATE_KEY:
|
||||||
|
process.env.BLOCKCHAIN_PRIVATE_KEY || defaultDevPrivateKey,
|
||||||
|
[bootstrapFlag]: "1",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (process.env[bootstrapFlag] === "1") {
|
||||||
|
throw new Error(
|
||||||
|
"Recursive dev bootstrap detected. Use npm run dev:next inside the bootstrap script.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Starting Next.js dev server...");
|
||||||
|
nextProcess = spawn("npm", ["run", "dev:next"], {
|
||||||
|
cwd: rootDir,
|
||||||
|
shell: true,
|
||||||
|
stdio: "inherit",
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
|
||||||
|
nextProcess.on("close", (code) => shutdown(code ?? 0));
|
||||||
|
})().catch((error) => {
|
||||||
|
console.error(error instanceof Error ? error.message : error);
|
||||||
|
shutdown(1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user