From f917fb8014839683a2d3fbc6fc50bae9a5245180 Mon Sep 17 00:00:00 2001 From: Daniel Kovalevich Date: Mon, 30 Mar 2026 15:43:27 -0400 Subject: [PATCH] Add Node.js/Express backend with PostgreSQL and wire frontend to API - Server: Express.js with 13 API route files (auth, regulations, contacts, calendar, truck stops, bridges, weigh stations, alerts, load board, escort locator, orders, documents, contributions) - Database: PostgreSQL with Prisma ORM, 15 models covering all modules - Auth: JWT + bcrypt with role-based access control (driver/carrier/escort/admin) - Geospatial: Haversine distance filtering on truck stops, bridges, escorts - Seed script: Imports all existing mock data (51 states, contacts, equipment, truck stops, bridges, weigh stations, alerts, seasonal restrictions) - Frontend: All 10 data-driven pages now fetch from /api instead of mock-data.js - API client (api.js): Compatibility layer that transforms API responses to match existing frontend rendering code, minimizing page-level changes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- alerts.html | 9 +- api.js | 448 ++++ bridges.html | 7 +- calendar.html | 7 +- contacts.html | 101 +- documents.html | 263 +- loadboard.html | 6 +- locator.html | 6 +- regulations.html | 8 +- server/.gitignore | 3 + server/package-lock.json | 2146 +++++++++++++++++ server/package.json | 26 + .../20260330193158_init/migration.sql | 308 +++ server/prisma/migrations/migration_lock.toml | 3 + server/prisma/schema.prisma | 312 +++ server/src/config/db.js | 7 + server/src/index.js | 46 + server/src/middleware/auth.js | 44 + server/src/middleware/errorHandler.js | 24 + server/src/routes/alerts.js | 54 + server/src/routes/auth.js | 89 + server/src/routes/bridges.js | 87 + server/src/routes/calendar.js | 34 + server/src/routes/contacts.js | 34 + server/src/routes/contributions.js | 74 + server/src/routes/documents.js | 119 + server/src/routes/loadboard.js | 119 + server/src/routes/locator.js | 111 + server/src/routes/orders.js | 92 + server/src/routes/regulations.js | 38 + server/src/routes/truckstops.js | 86 + server/src/routes/weighstations.js | 86 + server/src/seeds/seed.js | 346 +++ truckstops.html | 7 +- weighstations.html | 7 +- 35 files changed, 4964 insertions(+), 193 deletions(-) create mode 100644 api.js create mode 100644 server/.gitignore create mode 100644 server/package-lock.json create mode 100644 server/package.json create mode 100644 server/prisma/migrations/20260330193158_init/migration.sql create mode 100644 server/prisma/migrations/migration_lock.toml create mode 100644 server/prisma/schema.prisma create mode 100644 server/src/config/db.js create mode 100644 server/src/index.js create mode 100644 server/src/middleware/auth.js create mode 100644 server/src/middleware/errorHandler.js create mode 100644 server/src/routes/alerts.js create mode 100644 server/src/routes/auth.js create mode 100644 server/src/routes/bridges.js create mode 100644 server/src/routes/calendar.js create mode 100644 server/src/routes/contacts.js create mode 100644 server/src/routes/contributions.js create mode 100644 server/src/routes/documents.js create mode 100644 server/src/routes/loadboard.js create mode 100644 server/src/routes/locator.js create mode 100644 server/src/routes/orders.js create mode 100644 server/src/routes/regulations.js create mode 100644 server/src/routes/truckstops.js create mode 100644 server/src/routes/weighstations.js create mode 100644 server/src/seeds/seed.js diff --git a/alerts.html b/alerts.html index c02d22b..1583fdc 100644 --- a/alerts.html +++ b/alerts.html @@ -119,14 +119,18 @@ - - + diff --git a/api.js b/api.js new file mode 100644 index 0000000..0a65b33 --- /dev/null +++ b/api.js @@ -0,0 +1,448 @@ +// ===================================================================== +// PilotEdge API Client +// Fetches data from the backend API and transforms responses to match +// the shapes expected by the existing frontend rendering code. +// Replace mock-data.js and mock-data-extended.js with this file. +// ===================================================================== + +const API_BASE = '/api'; + +const PilotEdge = { + // Auth token management + _token: localStorage.getItem('pilotedge_token'), + + setToken(token) { + this._token = token; + if (token) localStorage.setItem('pilotedge_token', token); + else localStorage.removeItem('pilotedge_token'); + }, + + getToken() { + return this._token; + }, + + getUser() { + const raw = localStorage.getItem('pilotedge_user'); + return raw ? JSON.parse(raw) : null; + }, + + setUser(user) { + if (user) localStorage.setItem('pilotedge_user', JSON.stringify(user)); + else localStorage.removeItem('pilotedge_user'); + }, + + // Core fetch wrapper + async request(path, options = {}) { + const headers = { 'Content-Type': 'application/json', ...options.headers }; + if (this._token) headers['Authorization'] = `Bearer ${this._token}`; + + const res = await fetch(`${API_BASE}${path}`, { ...options, headers }); + if (!res.ok) { + const body = await res.json().catch(() => ({})); + const err = new Error(body.error || `API error ${res.status}`); + err.status = res.status; + throw err; + } + return res.json(); + }, + + async get(path) { return this.request(path); }, + async post(path, data) { return this.request(path, { method: 'POST', body: JSON.stringify(data) }); }, + async put(path, data) { return this.request(path, { method: 'PUT', body: JSON.stringify(data) }); }, + async del(path) { return this.request(path, { method: 'DELETE' }); }, + + // ----------------------------------------------------------------- + // Auth + // ----------------------------------------------------------------- + async register(email, password, name, role) { + const res = await this.post('/auth/register', { email, password, name, role }); + this.setToken(res.token); + this.setUser(res.user); + return res; + }, + + async login(email, password) { + const res = await this.post('/auth/login', { email, password }); + this.setToken(res.token); + this.setUser(res.user); + return res; + }, + + logout() { + this.setToken(null); + this.setUser(null); + }, + + async me() { return this.get('/auth/me'); }, + + // ----------------------------------------------------------------- + // Regulations — returns MOCK_STATE_REGULATIONS-compatible shape + // ----------------------------------------------------------------- + async getRegulations() { + const states = await this.get('/regulations'); + return states.map(s => ({ + name: s.name, + abbr: s.abbr, + lat: s.lat, + lng: s.lng, + permitWidth: s.regulation?.permitWidth || '', + permitHeight: s.regulation?.permitHeight || '', + permitLength: s.regulation?.permitLength || '', + permitWeight: s.regulation?.permitWeight || '', + escortWidth: s.regulation?.escortWidth || '', + escortHeight: s.regulation?.escortHeight || '', + escortLength: s.regulation?.escortLength || '', + escortWeight: s.regulation?.escortWeight || '', + travel: s.regulation?.travelRestrictions || '', + holidays: s.regulation?.holidays || '', + agency: s.regulation?.agency || '', + url: s.regulation?.url || '', + notes: s.regulation?.notes || '', + })); + }, + + // ----------------------------------------------------------------- + // Equipment — returns MOCK_STATE_EQUIPMENT-compatible shape + // Object keyed by state abbr: { TX: { escort: {...}, carrier: {...} } } + // ----------------------------------------------------------------- + async getEquipment() { + const states = await this.get('/regulations'); + const equipment = {}; + for (const s of states) { + if (!s.equipmentRequirements) { + const full = await this.get(`/regulations/${s.abbr}`); + s.equipmentRequirements = full.equipmentRequirements || []; + } + if (s.equipmentRequirements.length > 0) { + equipment[s.abbr] = {}; + for (const eq of s.equipmentRequirements) { + const obj = { + certification: eq.certification || '', + vehicle: eq.vehicle || '', + signs: eq.signs || '', + lights: eq.lights || '', + heightPole: eq.heightPole || '', + flags: eq.flags || '', + communication: eq.communication || '', + safety: eq.safetyGear || '', + }; + if (eq.type === 'escort') equipment[s.abbr].escort = obj; + else if (eq.type === 'carrier') { + // Parse carrier safetyGear back to individual fields + const gear = eq.safetyGear || ''; + equipment[s.abbr].carrier = { + signs: eq.signs || '', + flags: eq.flags || '', + lights: eq.lights || '', + cones: extractGearField(gear, 'Cones'), + fireExtinguisher: extractGearField(gear, 'Fire ext'), + triangles: extractGearField(gear, 'Triangles'), + flares: extractGearField(gear, 'Flares'), + firstAid: extractGearField(gear, 'First aid'), + }; + } + } + } + } + return equipment; + }, + + // ----------------------------------------------------------------- + // Contacts — returns MOCK_STATE_CONTACTS-compatible shape + // Object keyed by state abbr: { AL: { name, permit, police, email, hours, portal } } + // ----------------------------------------------------------------- + async getContacts() { + const contacts = await this.get('/contacts'); + const result = {}; + for (const c of contacts) { + result[c.state.abbr] = { + name: c.state.name, + permit: c.permitPhone, + police: c.policePhone, + email: c.email, + hours: c.hours, + portal: c.portalUrl, + }; + } + return result; + }, + + // ----------------------------------------------------------------- + // Calendar — returns MOCK_SEASONAL_RESTRICTIONS-compatible shape + // ----------------------------------------------------------------- + async getSeasonalRestrictions() { + const restrictions = await this.get('/calendar'); + return restrictions.map(r => ({ + id: r.id, + state: r.state?.abbr || '', + stateName: r.state?.name || '', + type: r.type, + title: r.name, + startMonth: r.startMonth, + startDay: 1, + endMonth: r.endMonth, + endDay: 28, + description: r.description, + color: getRestrictionColor(r.type), + routes: '', + impact: '', + })); + }, + + // ----------------------------------------------------------------- + // Truck Stops — returns MOCK_TRUCK_STOPS-compatible shape + // ----------------------------------------------------------------- + async getTruckStops() { + const stops = await this.get('/truckstops'); + return stops.map(ts => ({ + id: ts.id, + name: ts.name, + type: 'truck_stop', + location: { + city: ts.address?.split(',')[0]?.trim() || '', + state: ts.state?.abbr || '', + lat: ts.lat, + lng: ts.lng, + }, + oversizeFriendly: ts.hasOversizeParking, + entranceWidth: ts.entranceWidth || '', + entranceHeight: ts.entranceHeight || '', + lotSize: ts.lotSqFt ? `${ts.lotSqFt} sq ft` : '', + oversizeCapacity: '', + facilities: ts.facilities || [], + description: '', + comments: (ts.contributions || []).map(c => ({ + user: c.user?.name || 'Anonymous', + date: c.createdAt, + text: c.content, + })), + })); + }, + + // ----------------------------------------------------------------- + // Bridges — returns MOCK_BRIDGE_CLEARANCES-compatible shape + // ----------------------------------------------------------------- + async getBridges() { + const bridges = await this.get('/bridges'); + return bridges.map(b => ({ + id: b.id, + route: b.route, + mileMarker: '', + type: b.name.split(' at ')[0] || 'Bridge', + location: { + desc: b.name.split(' at ')[1] || b.name, + city: '', + state: b.state?.abbr || '', + lat: b.lat, + lng: b.lng, + }, + clearanceHeight: `${b.heightClearance}'`, + clearanceWidth: b.widthClearance ? `${b.widthClearance}'` : 'Unrestricted', + weightLimit: b.weightLimit ? `${b.weightLimit} lbs` : 'No posted limit', + notes: '', + })); + }, + + // ----------------------------------------------------------------- + // Weigh Stations — returns MOCK_WEIGH_STATIONS-compatible shape + // ----------------------------------------------------------------- + async getWeighStations() { + const stations = await this.get('/weighstations'); + return stations.map(ws => ({ + id: ws.id, + name: ws.name, + route: ws.route, + location: { + city: '', + state: ws.state?.abbr || '', + lat: ws.lat, + lng: ws.lng, + }, + hours: ws.hours, + prePass: ws.prePass, + currentStatus: ws.currentStatus, + direction: ws.direction, + lastFlagged: ws.lastStatusUpdate || null, + flaggedBy: '', + notes: '', + })); + }, + + // ----------------------------------------------------------------- + // Alerts — returns MOCK_ROUTE_CONDITIONS + MOCK_WEATHER_ALERTS shapes + // ----------------------------------------------------------------- + async getAlerts() { + const alerts = await this.get('/alerts'); + const routeConditions = []; + const weatherAlerts = []; + + for (const a of alerts) { + if (a.type === 'weather' || a.type === 'wind') { + weatherAlerts.push({ + id: a.id, + type: a.type, + severity: a.severity, + region: a.state?.name || '', + routes: a.route ? a.route.split(', ') : [], + description: a.description, + validFrom: a.startsAt, + validTo: a.endsAt, + source: 'NWS', + lat: 0, + lng: 0, + }); + } else { + routeConditions.push({ + id: a.id, + type: a.type, + severity: a.severity, + route: a.route, + location: { + desc: a.description.substring(0, 60), + state: a.state?.abbr || '', + lat: 0, + lng: 0, + }, + description: a.description, + startDate: a.startsAt, + endDate: a.endsAt, + source: 'State DOT', + affectsOversize: true, + }); + } + } + return { routeConditions, weatherAlerts }; + }, + + // ----------------------------------------------------------------- + // Load Board — returns MOCK_LOAD_BOARD-compatible shape + // ----------------------------------------------------------------- + async getLoads() { + const data = await this.get('/loads?limit=100'); + return (data.loads || []).map(l => ({ + id: l.id, + carrier: l.poster?.name || 'Unknown', + origin: parseLocation(l.origin), + destination: parseLocation(l.destination), + departureDate: l.pickupDate, + dimensions: { + width: l.width, + height: l.height, + length: l.length, + weight: l.weight, + }, + description: l.description, + escortsNeeded: l.escortsNeeded, + status: l.status, + postedDate: l.createdAt, + contact: l.poster?.name || '', + })); + }, + + // ----------------------------------------------------------------- + // Escort Operators — returns MOCK_ESCORT_OPERATORS-compatible shape + // ----------------------------------------------------------------- + async getEscortOperators() { + const escorts = await this.get('/escorts'); + return escorts.map(e => ({ + id: e.id, + name: e.user?.name || 'Unknown', + location: { + city: '', + state: '', + lat: e.lat, + lng: e.lng, + }, + status: e.availability, + certifications: e.certifications || [], + vehicleType: e.vehicleType, + rating: e.rating, + totalJobs: e.ratingCount || 0, + experience: '', + contact: e.user?.email || '', + phone: e.phone, + bio: e.bio, + })); + }, + + // ----------------------------------------------------------------- + // Documents — returns MOCK_DOCUMENTS-compatible shape + // ----------------------------------------------------------------- + async getDocuments() { + try { + const docs = await this.get('/documents'); + return docs.map(d => ({ + id: d.id, + name: d.filename, + type: d.type, + state: '', + uploadDate: d.createdAt, + expiryDate: d.expiresAt, + fileSize: formatFileSize(d.sizeBytes), + status: d.expiresAt && new Date(d.expiresAt) < new Date() ? 'expired' : 'active', + })); + } catch (err) { + // If not authenticated, return empty array + if (err.status === 401) return []; + throw err; + } + }, + + // ----------------------------------------------------------------- + // Orders + // ----------------------------------------------------------------- + async submitOrder(orderData) { + return this.post('/orders', orderData); + }, + + async getOrders() { + return this.get('/orders'); + }, + + // ----------------------------------------------------------------- + // Contributions + // ----------------------------------------------------------------- + async submitContribution(entityType, entityId, type, content) { + return this.post('/contributions', { entityType, entityId, type, content }); + }, +}; + +// ----------------------------------------------------------------- +// Helper functions +// ----------------------------------------------------------------- + +function extractGearField(gear, label) { + const match = gear.match(new RegExp(`${label}:\\s*([^;]+)`)); + return match ? match[1].trim() : ''; +} + +function getRestrictionColor(type) { + const colors = { + spring_weight: '#3b82f6', + winter_closure: '#8b5cf6', + harvest: '#f59e0b', + holiday_blackout: '#ef4444', + }; + return colors[type] || '#6b7280'; +} + +function parseLocation(str) { + // Parse "City, ST" into { city, state, lat: 0, lng: 0 } + if (!str) return { city: '', state: '', lat: 0, lng: 0 }; + const parts = str.split(',').map(s => s.trim()); + return { + city: parts[0] || '', + state: parts[1] || '', + lat: 0, + lng: 0, + }; +} + +function formatFileSize(bytes) { + if (!bytes) return '0 B'; + const units = ['B', 'KB', 'MB', 'GB']; + let i = 0; + let size = bytes; + while (size >= 1024 && i < units.length - 1) { size /= 1024; i++; } + return `${Math.round(size * 10) / 10} ${units[i]}`; +} diff --git a/bridges.html b/bridges.html index db47148..c105d90 100644 --- a/bridges.html +++ b/bridges.html @@ -112,14 +112,16 @@ - - + diff --git a/calendar.html b/calendar.html index 192d537..3374f0d 100644 --- a/calendar.html +++ b/calendar.html @@ -85,14 +85,16 @@ - - + diff --git a/contacts.html b/contacts.html index e819b68..2578252 100644 --- a/contacts.html +++ b/contacts.html @@ -42,67 +42,70 @@ - - + diff --git a/documents.html b/documents.html index 24f6dbe..de9613f 100644 --- a/documents.html +++ b/documents.html @@ -105,155 +105,158 @@ - - + diff --git a/loadboard.html b/loadboard.html index 3b378a8..1f3b968 100644 --- a/loadboard.html +++ b/loadboard.html @@ -141,13 +141,16 @@ - + diff --git a/locator.html b/locator.html index 71cf26c..b49739f 100644 --- a/locator.html +++ b/locator.html @@ -115,13 +115,16 @@ - + diff --git a/regulations.html b/regulations.html index 3b9b202..e886c1d 100644 --- a/regulations.html +++ b/regulations.html @@ -139,14 +139,17 @@ - - + diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..bd6a1b7 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.env +uploads/ diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000..de9f4ed --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,2146 @@ +{ + "name": "pilotedge-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pilotedge-server", + "version": "1.0.0", + "dependencies": { + "@prisma/client": "^6.6.0", + "bcrypt": "^5.1.1", + "cors": "^2.8.5", + "dotenv": "^16.4.7", + "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.2" + }, + "devDependencies": { + "prisma": "^6.6.0" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@prisma/client": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.2.tgz", + "integrity": "sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.2.tgz", + "integrity": "sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.18.4", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.2.tgz", + "integrity": "sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.2.tgz", + "integrity": "sha512-TTkJ8r+uk/uqczX40wb+ODG0E0icVsMgwCTyTHXehaEfb0uo80M9g1aW1tEJrxmFHeOZFXdI2sTA1j1AgcHi4A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.2", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.2", + "@prisma/get-platform": "6.19.2" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.2.tgz", + "integrity": "sha512-h4Ff4Pho+SR1S8XerMCC12X//oY2bG3Iug/fUnudfcXEUnIeRiBdXHFdGlGOgQ3HqKgosTEhkZMvGM9tWtYC+Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.2", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.2" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.2.tgz", + "integrity": "sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.2" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "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/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/effect": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", + "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "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/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "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-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/nypm": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", + "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.2.0", + "pathe": "^2.0.3", + "tinyexec": "^1.0.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", + "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", + "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/prisma": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.2.tgz", + "integrity": "sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.19.2", + "@prisma/engines": "6.19.2" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "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/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/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..3723875 --- /dev/null +++ b/server/package.json @@ -0,0 +1,26 @@ +{ + "name": "pilotedge-server", + "version": "1.0.0", + "description": "PilotEdge backend API — Oversize Load Resource Platform", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "dev": "node --watch src/index.js", + "db:migrate": "npx prisma migrate dev", + "db:seed": "node src/seeds/seed.js", + "db:reset": "npx prisma migrate reset --force", + "db:studio": "npx prisma studio" + }, + "dependencies": { + "@prisma/client": "^6.6.0", + "bcrypt": "^5.1.1", + "cors": "^2.8.5", + "dotenv": "^16.4.7", + "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.2" + }, + "devDependencies": { + "prisma": "^6.6.0" + } +} diff --git a/server/prisma/migrations/20260330193158_init/migration.sql b/server/prisma/migrations/20260330193158_init/migration.sql new file mode 100644 index 0000000..712bb52 --- /dev/null +++ b/server/prisma/migrations/20260330193158_init/migration.sql @@ -0,0 +1,308 @@ +-- CreateTable +CREATE TABLE "states" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "abbr" VARCHAR(2) NOT NULL, + "lat" DOUBLE PRECISION NOT NULL, + "lng" DOUBLE PRECISION NOT NULL, + + CONSTRAINT "states_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "regulations" ( + "id" TEXT NOT NULL, + "stateId" TEXT NOT NULL, + "permitWidth" TEXT NOT NULL, + "permitHeight" TEXT NOT NULL, + "permitLength" TEXT NOT NULL, + "permitWeight" TEXT NOT NULL, + "escortWidth" TEXT NOT NULL, + "escortHeight" TEXT NOT NULL, + "escortLength" TEXT NOT NULL, + "escortWeight" TEXT NOT NULL, + "travelRestrictions" TEXT NOT NULL, + "holidays" TEXT NOT NULL, + "agency" TEXT NOT NULL, + "url" TEXT NOT NULL, + "notes" TEXT NOT NULL, + + CONSTRAINT "regulations_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "equipment_requirements" ( + "id" TEXT NOT NULL, + "stateId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "certification" TEXT NOT NULL DEFAULT '', + "vehicle" TEXT NOT NULL DEFAULT '', + "signs" TEXT NOT NULL DEFAULT '', + "lights" TEXT NOT NULL DEFAULT '', + "heightPole" TEXT NOT NULL DEFAULT '', + "flags" TEXT NOT NULL DEFAULT '', + "safetyGear" TEXT NOT NULL DEFAULT '', + "communication" TEXT NOT NULL DEFAULT '', + + CONSTRAINT "equipment_requirements_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "contacts" ( + "id" TEXT NOT NULL, + "stateId" TEXT NOT NULL, + "permitPhone" TEXT NOT NULL, + "policePhone" TEXT NOT NULL, + "email" TEXT NOT NULL, + "hours" TEXT NOT NULL, + "portalUrl" TEXT NOT NULL, + + CONSTRAINT "contacts_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "seasonal_restrictions" ( + "id" TEXT NOT NULL, + "stateId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "type" TEXT NOT NULL, + "startMonth" INTEGER NOT NULL, + "endMonth" INTEGER NOT NULL, + "description" TEXT NOT NULL, + + CONSTRAINT "seasonal_restrictions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "truck_stops" ( + "id" TEXT NOT NULL, + "stateId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "lat" DOUBLE PRECISION NOT NULL, + "lng" DOUBLE PRECISION NOT NULL, + "address" TEXT NOT NULL, + "hasOversizeParking" BOOLEAN NOT NULL DEFAULT false, + "entranceHeight" TEXT NOT NULL DEFAULT '', + "entranceWidth" TEXT NOT NULL DEFAULT '', + "lotSqFt" INTEGER, + "facilities" JSONB NOT NULL DEFAULT '[]', + "satelliteUrl" TEXT NOT NULL DEFAULT '', + "phone" TEXT NOT NULL DEFAULT '', + + CONSTRAINT "truck_stops_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "bridges" ( + "id" TEXT NOT NULL, + "stateId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "lat" DOUBLE PRECISION NOT NULL, + "lng" DOUBLE PRECISION NOT NULL, + "route" TEXT NOT NULL, + "heightClearance" DOUBLE PRECISION NOT NULL, + "widthClearance" DOUBLE PRECISION, + "weightLimit" DOUBLE PRECISION, + "lastVerified" TIMESTAMP(3), + + CONSTRAINT "bridges_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "weigh_stations" ( + "id" TEXT NOT NULL, + "stateId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "lat" DOUBLE PRECISION NOT NULL, + "lng" DOUBLE PRECISION NOT NULL, + "direction" TEXT NOT NULL, + "route" TEXT NOT NULL, + "prePass" BOOLEAN NOT NULL DEFAULT false, + "hours" TEXT NOT NULL DEFAULT '', + "currentStatus" TEXT NOT NULL DEFAULT 'unknown', + "lastStatusUpdate" TIMESTAMP(3), + "lastStatusUserId" TEXT, + + CONSTRAINT "weigh_stations_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "users" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "passwordHash" TEXT NOT NULL, + "name" TEXT NOT NULL, + "role" TEXT NOT NULL DEFAULT 'driver', + "tier" TEXT NOT NULL DEFAULT 'free', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "users_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "escort_profiles" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "lat" DOUBLE PRECISION NOT NULL, + "lng" DOUBLE PRECISION NOT NULL, + "radiusMiles" INTEGER NOT NULL DEFAULT 100, + "certifications" JSONB NOT NULL DEFAULT '[]', + "vehicleType" TEXT NOT NULL DEFAULT '', + "availability" TEXT NOT NULL DEFAULT 'available', + "rating" DOUBLE PRECISION NOT NULL DEFAULT 0, + "ratingCount" INTEGER NOT NULL DEFAULT 0, + "phone" TEXT NOT NULL DEFAULT '', + "bio" TEXT NOT NULL DEFAULT '', + + CONSTRAINT "escort_profiles_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "loads" ( + "id" TEXT NOT NULL, + "posterId" TEXT NOT NULL, + "origin" TEXT NOT NULL, + "destination" TEXT NOT NULL, + "pickupDate" TIMESTAMP(3) NOT NULL, + "width" TEXT NOT NULL, + "height" TEXT NOT NULL, + "length" TEXT NOT NULL, + "weight" TEXT NOT NULL, + "description" TEXT NOT NULL DEFAULT '', + "escortsNeeded" INTEGER NOT NULL DEFAULT 1, + "status" TEXT NOT NULL DEFAULT 'open', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "loads_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "orders" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "origin" TEXT NOT NULL, + "destination" TEXT NOT NULL, + "pickupDate" TIMESTAMP(3) NOT NULL, + "width" TEXT NOT NULL DEFAULT '', + "height" TEXT NOT NULL DEFAULT '', + "length" TEXT NOT NULL DEFAULT '', + "weight" TEXT NOT NULL DEFAULT '', + "loadType" TEXT NOT NULL DEFAULT '', + "escortsNeeded" INTEGER NOT NULL DEFAULT 1, + "status" TEXT NOT NULL DEFAULT 'pending', + "notes" TEXT NOT NULL DEFAULT '', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "orders_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "documents" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "filename" TEXT NOT NULL, + "filepath" TEXT NOT NULL, + "mimeType" TEXT NOT NULL DEFAULT '', + "sizeBytes" INTEGER NOT NULL DEFAULT 0, + "expiresAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "documents_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "contributions" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "entityType" TEXT NOT NULL, + "entityId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "truckStopId" TEXT, + "weighStationId" TEXT, + + CONSTRAINT "contributions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "alerts" ( + "id" TEXT NOT NULL, + "stateId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "route" TEXT NOT NULL, + "description" TEXT NOT NULL, + "severity" TEXT NOT NULL DEFAULT 'info', + "startsAt" TIMESTAMP(3) NOT NULL, + "endsAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "alerts_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "states_name_key" ON "states"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "states_abbr_key" ON "states"("abbr"); + +-- CreateIndex +CREATE UNIQUE INDEX "regulations_stateId_key" ON "regulations"("stateId"); + +-- CreateIndex +CREATE UNIQUE INDEX "contacts_stateId_key" ON "contacts"("stateId"); + +-- CreateIndex +CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "escort_profiles_userId_key" ON "escort_profiles"("userId"); + +-- AddForeignKey +ALTER TABLE "regulations" ADD CONSTRAINT "regulations_stateId_fkey" FOREIGN KEY ("stateId") REFERENCES "states"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "equipment_requirements" ADD CONSTRAINT "equipment_requirements_stateId_fkey" FOREIGN KEY ("stateId") REFERENCES "states"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "contacts" ADD CONSTRAINT "contacts_stateId_fkey" FOREIGN KEY ("stateId") REFERENCES "states"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "seasonal_restrictions" ADD CONSTRAINT "seasonal_restrictions_stateId_fkey" FOREIGN KEY ("stateId") REFERENCES "states"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "truck_stops" ADD CONSTRAINT "truck_stops_stateId_fkey" FOREIGN KEY ("stateId") REFERENCES "states"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "bridges" ADD CONSTRAINT "bridges_stateId_fkey" FOREIGN KEY ("stateId") REFERENCES "states"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "weigh_stations" ADD CONSTRAINT "weigh_stations_stateId_fkey" FOREIGN KEY ("stateId") REFERENCES "states"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "escort_profiles" ADD CONSTRAINT "escort_profiles_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "loads" ADD CONSTRAINT "loads_posterId_fkey" FOREIGN KEY ("posterId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "orders" ADD CONSTRAINT "orders_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "documents" ADD CONSTRAINT "documents_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "contributions" ADD CONSTRAINT "contributions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "contributions" ADD CONSTRAINT "contributions_truckStopId_fkey" FOREIGN KEY ("truckStopId") REFERENCES "truck_stops"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "contributions" ADD CONSTRAINT "contributions_weighStationId_fkey" FOREIGN KEY ("weighStationId") REFERENCES "weigh_stations"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "alerts" ADD CONSTRAINT "alerts_stateId_fkey" FOREIGN KEY ("stateId") REFERENCES "states"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/server/prisma/migrations/migration_lock.toml b/server/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/server/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma new file mode 100644 index 0000000..d15f0ca --- /dev/null +++ b/server/prisma/schema.prisma @@ -0,0 +1,312 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// ===================================================================== +// Core Reference Data +// ===================================================================== + +model State { + id String @id @default(cuid()) + name String @unique + abbr String @unique @db.VarChar(2) + lat Float + lng Float + + regulation Regulation? + equipmentRequirements EquipmentRequirement[] + contact Contact? + seasonalRestrictions SeasonalRestriction[] + truckStops TruckStop[] + bridges Bridge[] + weighStations WeighStation[] + alerts Alert[] + + @@map("states") +} + +model Regulation { + id String @id @default(cuid()) + stateId String @unique + state State @relation(fields: [stateId], references: [id], onDelete: Cascade) + + permitWidth String + permitHeight String + permitLength String + permitWeight String + + escortWidth String + escortHeight String + escortLength String + escortWeight String + + travelRestrictions String + holidays String + agency String + url String + notes String @db.Text + + @@map("regulations") +} + +model EquipmentRequirement { + id String @id @default(cuid()) + stateId String + state State @relation(fields: [stateId], references: [id], onDelete: Cascade) + + type String // "escort" or "carrier" + certification String @default("") + vehicle String @default("") + signs String @default("") + lights String @default("") + heightPole String @default("") + flags String @default("") + safetyGear String @default("") @db.Text + communication String @default("") + + @@map("equipment_requirements") +} + +model Contact { + id String @id @default(cuid()) + stateId String @unique + state State @relation(fields: [stateId], references: [id], onDelete: Cascade) + + permitPhone String + policePhone String + email String + hours String + portalUrl String + + @@map("contacts") +} + +model SeasonalRestriction { + id String @id @default(cuid()) + stateId String + state State @relation(fields: [stateId], references: [id], onDelete: Cascade) + + name String + type String // "spring_weight", "winter_closure", "harvest", "holiday_blackout" + startMonth Int + endMonth Int + description String @db.Text + + @@map("seasonal_restrictions") +} + +// ===================================================================== +// Geospatial Entities +// ===================================================================== + +model TruckStop { + id String @id @default(cuid()) + stateId String + state State @relation(fields: [stateId], references: [id], onDelete: Cascade) + + name String + lat Float + lng Float + address String + hasOversizeParking Boolean @default(false) + entranceHeight String @default("") + entranceWidth String @default("") + lotSqFt Int? + facilities Json @default("[]") // ["fuel","food","showers","restrooms"] + satelliteUrl String @default("") + phone String @default("") + + contributions Contribution[] + + @@map("truck_stops") +} + +model Bridge { + id String @id @default(cuid()) + stateId String + state State @relation(fields: [stateId], references: [id], onDelete: Cascade) + + name String + lat Float + lng Float + route String + heightClearance Float // in feet + widthClearance Float? // in feet, null = unrestricted + weightLimit Float? // in lbs, null = unrestricted + lastVerified DateTime? + + @@map("bridges") +} + +model WeighStation { + id String @id @default(cuid()) + stateId String + state State @relation(fields: [stateId], references: [id], onDelete: Cascade) + + name String + lat Float + lng Float + direction String // "NB", "SB", "EB", "WB" + route String + prePass Boolean @default(false) + hours String @default("") + currentStatus String @default("unknown") // "open", "closed", "unknown" + lastStatusUpdate DateTime? + lastStatusUserId String? + + contributions Contribution[] + + @@map("weigh_stations") +} + +// ===================================================================== +// Users & Auth +// ===================================================================== + +model User { + id String @id @default(cuid()) + email String @unique + passwordHash String + name String + role String @default("driver") // "driver", "carrier", "escort", "admin" + tier String @default("free") // "free", "subscriber", "premium" + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + escortProfile EscortProfile? + loads Load[] + orders Order[] + documents Document[] + contributions Contribution[] + + @@map("users") +} + +model EscortProfile { + id String @id @default(cuid()) + userId String @unique + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + lat Float + lng Float + radiusMiles Int @default(100) + certifications Json @default("[]") // ["TX","OK","CA"] + vehicleType String @default("") + availability String @default("available") // "available", "on_job", "unavailable" + rating Float @default(0) + ratingCount Int @default(0) + phone String @default("") + bio String @default("") @db.Text + + @@map("escort_profiles") +} + +// ===================================================================== +// Transactional Data +// ===================================================================== + +model Load { + id String @id @default(cuid()) + posterId String + poster User @relation(fields: [posterId], references: [id], onDelete: Cascade) + + origin String + destination String + pickupDate DateTime + width String + height String + length String + weight String + description String @default("") @db.Text + escortsNeeded Int @default(1) + status String @default("open") // "open", "assigned", "in_transit", "delivered", "cancelled" + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("loads") +} + +model Order { + id String @id @default(cuid()) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // Load details (embedded, since orders may not reference a load board listing) + origin String + destination String + pickupDate DateTime + width String @default("") + height String @default("") + length String @default("") + weight String @default("") + loadType String @default("") + escortsNeeded Int @default(1) + + status String @default("pending") // "pending", "confirmed", "in_progress", "completed", "cancelled" + notes String @default("") @db.Text + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("orders") +} + +model Document { + id String @id @default(cuid()) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + type String // "permit", "insurance", "certification", "license", "other" + filename String + filepath String + mimeType String @default("") + sizeBytes Int @default(0) + expiresAt DateTime? + createdAt DateTime @default(now()) + + @@map("documents") +} + +model Contribution { + id String @id @default(cuid()) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + entityType String // "truck_stop", "weigh_station", "bridge", "regulation" + entityId String + type String // "info", "flag", "confirm" + content String @db.Text + createdAt DateTime @default(now()) + + // Optional relations (polymorphic — only one will be set) + truckStop TruckStop? @relation(fields: [truckStopId], references: [id]) + truckStopId String? + weighStation WeighStation? @relation(fields: [weighStationId], references: [id]) + weighStationId String? + + @@map("contributions") +} + +// ===================================================================== +// Alerts +// ===================================================================== + +model Alert { + id String @id @default(cuid()) + stateId String + state State @relation(fields: [stateId], references: [id], onDelete: Cascade) + + type String // "construction", "closure", "weather", "wind" + route String + description String @db.Text + severity String @default("info") // "info", "warning", "critical" + startsAt DateTime + endsAt DateTime? + createdAt DateTime @default(now()) + + @@map("alerts") +} diff --git a/server/src/config/db.js b/server/src/config/db.js new file mode 100644 index 0000000..0dd523d --- /dev/null +++ b/server/src/config/db.js @@ -0,0 +1,7 @@ +const { PrismaClient } = require('@prisma/client'); + +const prisma = new PrismaClient({ + log: process.env.NODE_ENV === 'development' ? ['warn', 'error'] : ['error'], +}); + +module.exports = prisma; diff --git a/server/src/index.js b/server/src/index.js new file mode 100644 index 0000000..eb6e50f --- /dev/null +++ b/server/src/index.js @@ -0,0 +1,46 @@ +require('dotenv').config(); +const express = require('express'); +const cors = require('cors'); +const path = require('path'); +const errorHandler = require('./middleware/errorHandler'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// --------------- Middleware --------------- +app.use(cors()); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +// Serve the frontend (static HTML/JS/CSS from project root) +app.use(express.static(path.join(__dirname, '..', '..'))); + +// --------------- API Routes --------------- +app.use('/api/auth', require('./routes/auth')); +app.use('/api/regulations', require('./routes/regulations')); +app.use('/api/contacts', require('./routes/contacts')); +app.use('/api/calendar', require('./routes/calendar')); +app.use('/api/truckstops', require('./routes/truckstops')); +app.use('/api/bridges', require('./routes/bridges')); +app.use('/api/weighstations', require('./routes/weighstations')); +app.use('/api/alerts', require('./routes/alerts')); +app.use('/api/loads', require('./routes/loadboard')); +app.use('/api/escorts', require('./routes/locator')); +app.use('/api/orders', require('./routes/orders')); +app.use('/api/documents', require('./routes/documents')); +app.use('/api/contributions', require('./routes/contributions')); + +// Health check +app.get('/api/health', (req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +// --------------- Error Handler --------------- +app.use(errorHandler); + +// --------------- Start Server --------------- +app.listen(PORT, () => { + console.log(`\n🚛 PilotEdge API running at http://localhost:${PORT}`); + console.log(`📡 API endpoints at http://localhost:${PORT}/api`); + console.log(`🌐 Frontend at http://localhost:${PORT}/index.html\n`); +}); diff --git a/server/src/middleware/auth.js b/server/src/middleware/auth.js new file mode 100644 index 0000000..5a6ca43 --- /dev/null +++ b/server/src/middleware/auth.js @@ -0,0 +1,44 @@ +const jwt = require('jsonwebtoken'); + +// Required auth — rejects if no valid token +function requireAuth(req, res, next) { + const header = req.headers.authorization; + if (!header || !header.startsWith('Bearer ')) { + return res.status(401).json({ error: 'Authentication required.' }); + } + + try { + const token = header.split(' ')[1]; + const payload = jwt.verify(token, process.env.JWT_SECRET); + req.user = payload; // { id, email, role, tier } + next(); + } catch (err) { + return res.status(401).json({ error: 'Invalid or expired token.' }); + } +} + +// Optional auth — attaches user if token present, continues either way +function optionalAuth(req, res, next) { + const header = req.headers.authorization; + if (header && header.startsWith('Bearer ')) { + try { + const token = header.split(' ')[1]; + req.user = jwt.verify(token, process.env.JWT_SECRET); + } catch (err) { + // Invalid token — continue without user + } + } + next(); +} + +// Role check — use after requireAuth +function requireRole(...roles) { + return (req, res, next) => { + if (!req.user || !roles.includes(req.user.role)) { + return res.status(403).json({ error: 'Insufficient permissions.' }); + } + next(); + }; +} + +module.exports = { requireAuth, optionalAuth, requireRole }; diff --git a/server/src/middleware/errorHandler.js b/server/src/middleware/errorHandler.js new file mode 100644 index 0000000..caec081 --- /dev/null +++ b/server/src/middleware/errorHandler.js @@ -0,0 +1,24 @@ +function errorHandler(err, req, res, next) { + console.error(`[ERROR] ${err.message}`); + if (process.env.NODE_ENV === 'development') { + console.error(err.stack); + } + + if (err.name === 'ValidationError') { + return res.status(400).json({ error: err.message }); + } + + if (err.code === 'P2002') { + return res.status(409).json({ error: 'A record with that value already exists.' }); + } + + if (err.code === 'P2025') { + return res.status(404).json({ error: 'Record not found.' }); + } + + res.status(err.status || 500).json({ + error: process.env.NODE_ENV === 'development' ? err.message : 'Internal server error', + }); +} + +module.exports = errorHandler; diff --git a/server/src/routes/alerts.js b/server/src/routes/alerts.js new file mode 100644 index 0000000..bae7da3 --- /dev/null +++ b/server/src/routes/alerts.js @@ -0,0 +1,54 @@ +const express = require('express'); +const prisma = require('../config/db'); + +const router = express.Router(); + +// GET /api/alerts?state=...&type=...&severity=... +router.get('/', async (req, res, next) => { + try { + const { state, type, severity } = req.query; + const where = {}; + + if (state) { + where.state = { abbr: state.toUpperCase() }; + } + if (type) { + where.type = type; + } + if (severity) { + where.severity = severity; + } + + // Only show active alerts (endsAt is null or in the future) + where.OR = [ + { endsAt: null }, + { endsAt: { gte: new Date() } }, + ]; + + const alerts = await prisma.alert.findMany({ + where, + include: { state: { select: { name: true, abbr: true } } }, + orderBy: [{ severity: 'desc' }, { startsAt: 'desc' }], + }); + + res.json(alerts); + } catch (err) { + next(err); + } +}); + +// GET /api/alerts/:id +router.get('/:id', async (req, res, next) => { + try { + const alert = await prisma.alert.findUnique({ + where: { id: req.params.id }, + include: { state: { select: { name: true, abbr: true } } }, + }); + if (!alert) return res.status(404).json({ error: 'Alert not found.' }); + res.json(alert); + } catch (err) { + next(err); + } +}); + +module.exports = router; diff --git a/server/src/routes/auth.js b/server/src/routes/auth.js new file mode 100644 index 0000000..a82ee69 --- /dev/null +++ b/server/src/routes/auth.js @@ -0,0 +1,89 @@ +const express = require('express'); +const bcrypt = require('bcrypt'); +const jwt = require('jsonwebtoken'); +const prisma = require('../config/db'); +const { requireAuth } = require('../middleware/auth'); + +const router = express.Router(); + +// POST /api/auth/register +router.post('/register', async (req, res, next) => { + try { + const { email, password, name, role } = req.body; + + if (!email || !password || !name) { + return res.status(400).json({ error: 'Email, password, and name are required.' }); + } + + const validRoles = ['driver', 'carrier', 'escort']; + const userRole = validRoles.includes(role) ? role : 'driver'; + + const passwordHash = await bcrypt.hash(password, 12); + const user = await prisma.user.create({ + data: { email, passwordHash, name, role: userRole }, + }); + + const token = jwt.sign( + { id: user.id, email: user.email, role: user.role, tier: user.tier }, + process.env.JWT_SECRET, + { expiresIn: '7d' } + ); + + res.status(201).json({ + token, + user: { id: user.id, email: user.email, name: user.name, role: user.role, tier: user.tier }, + }); + } catch (err) { + next(err); + } +}); + +// POST /api/auth/login +router.post('/login', async (req, res, next) => { + try { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ error: 'Email and password are required.' }); + } + + const user = await prisma.user.findUnique({ where: { email } }); + if (!user) { + return res.status(401).json({ error: 'Invalid email or password.' }); + } + + const valid = await bcrypt.compare(password, user.passwordHash); + if (!valid) { + return res.status(401).json({ error: 'Invalid email or password.' }); + } + + const token = jwt.sign( + { id: user.id, email: user.email, role: user.role, tier: user.tier }, + process.env.JWT_SECRET, + { expiresIn: '7d' } + ); + + res.json({ + token, + user: { id: user.id, email: user.email, name: user.name, role: user.role, tier: user.tier }, + }); + } catch (err) { + next(err); + } +}); + +// GET /api/auth/me +router.get('/me', requireAuth, async (req, res, next) => { + try { + const user = await prisma.user.findUnique({ + where: { id: req.user.id }, + select: { id: true, email: true, name: true, role: true, tier: true, createdAt: true }, + }); + if (!user) return res.status(404).json({ error: 'User not found.' }); + res.json(user); + } catch (err) { + next(err); + } +}); + +module.exports = router; diff --git a/server/src/routes/bridges.js b/server/src/routes/bridges.js new file mode 100644 index 0000000..5dd3537 --- /dev/null +++ b/server/src/routes/bridges.js @@ -0,0 +1,87 @@ +const express = require('express'); +const prisma = require('../config/db'); + +const router = express.Router(); + +// GET /api/bridges?lat=...&lng=...&radius=...&maxHeight=...&maxWidth=... +router.get('/', async (req, res, next) => { + try { + const { lat, lng, radius, maxHeight, maxWidth, state } = req.query; + const where = {}; + + if (state) { + where.state = { abbr: state.toUpperCase() }; + } + + let bridges = await prisma.bridge.findMany({ + where, + include: { state: { select: { name: true, abbr: true } } }, + orderBy: { route: 'asc' }, + }); + + // Distance filter + if (lat && lng && radius) { + const centerLat = parseFloat(lat); + const centerLng = parseFloat(lng); + const maxMiles = parseFloat(radius); + + bridges = bridges.filter((b) => { + const dist = haversine(centerLat, centerLng, b.lat, b.lng); + b.distanceMiles = Math.round(dist * 10) / 10; + return dist <= maxMiles; + }); + bridges.sort((a, b) => a.distanceMiles - b.distanceMiles); + } + + // Dimension conflict detection + if (maxHeight) { + const loadHeight = parseFloat(maxHeight); + bridges = bridges.map((b) => ({ + ...b, + heightConflict: b.heightClearance < loadHeight, + })); + } + + if (maxWidth) { + const loadWidth = parseFloat(maxWidth); + bridges = bridges.map((b) => ({ + ...b, + widthConflict: b.widthClearance != null && b.widthClearance < loadWidth, + })); + } + + res.json(bridges); + } catch (err) { + next(err); + } +}); + +// GET /api/bridges/:id +router.get('/:id', async (req, res, next) => { + try { + const bridge = await prisma.bridge.findUnique({ + where: { id: req.params.id }, + include: { state: { select: { name: true, abbr: true } } }, + }); + if (!bridge) return res.status(404).json({ error: 'Bridge not found.' }); + res.json(bridge); + } catch (err) { + next(err); + } +}); + +function haversine(lat1, lng1, lat2, lng2) { + const R = 3959; + const dLat = toRad(lat2 - lat1); + const dLng = toRad(lng2 - lng1); + const a = + Math.sin(dLat / 2) ** 2 + + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2; + return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); +} + +function toRad(deg) { + return (deg * Math.PI) / 180; +} + +module.exports = router; diff --git a/server/src/routes/calendar.js b/server/src/routes/calendar.js new file mode 100644 index 0000000..f64f4bc --- /dev/null +++ b/server/src/routes/calendar.js @@ -0,0 +1,34 @@ +const express = require('express'); +const prisma = require('../config/db'); + +const router = express.Router(); + +// GET /api/calendar — all seasonal restrictions +router.get('/', async (req, res, next) => { + try { + const restrictions = await prisma.seasonalRestriction.findMany({ + include: { state: { select: { name: true, abbr: true } } }, + orderBy: [{ startMonth: 'asc' }, { state: { name: 'asc' } }], + }); + res.json(restrictions); + } catch (err) { + next(err); + } +}); + +// GET /api/calendar/:stateAbbr +router.get('/:stateAbbr', async (req, res, next) => { + try { + const abbr = req.params.stateAbbr.toUpperCase(); + const state = await prisma.state.findUnique({ + where: { abbr }, + include: { seasonalRestrictions: true }, + }); + if (!state) return res.status(404).json({ error: `State '${abbr}' not found.` }); + res.json(state.seasonalRestrictions); + } catch (err) { + next(err); + } +}); + +module.exports = router; diff --git a/server/src/routes/contacts.js b/server/src/routes/contacts.js new file mode 100644 index 0000000..513dee9 --- /dev/null +++ b/server/src/routes/contacts.js @@ -0,0 +1,34 @@ +const express = require('express'); +const prisma = require('../config/db'); + +const router = express.Router(); + +// GET /api/contacts — all state contacts +router.get('/', async (req, res, next) => { + try { + const contacts = await prisma.contact.findMany({ + include: { state: { select: { name: true, abbr: true } } }, + orderBy: { state: { name: 'asc' } }, + }); + res.json(contacts); + } catch (err) { + next(err); + } +}); + +// GET /api/contacts/:stateAbbr +router.get('/:stateAbbr', async (req, res, next) => { + try { + const abbr = req.params.stateAbbr.toUpperCase(); + const state = await prisma.state.findUnique({ + where: { abbr }, + include: { contact: true }, + }); + if (!state) return res.status(404).json({ error: `State '${abbr}' not found.` }); + res.json(state.contact || { error: 'No contact data for this state.' }); + } catch (err) { + next(err); + } +}); + +module.exports = router; diff --git a/server/src/routes/contributions.js b/server/src/routes/contributions.js new file mode 100644 index 0000000..fa230cd --- /dev/null +++ b/server/src/routes/contributions.js @@ -0,0 +1,74 @@ +const express = require('express'); +const prisma = require('../config/db'); +const { requireAuth } = require('../middleware/auth'); + +const router = express.Router(); + +// GET /api/contributions?entityType=...&entityId=... +router.get('/', async (req, res, next) => { + try { + const { entityType, entityId } = req.query; + const where = {}; + + if (entityType) where.entityType = entityType; + if (entityId) where.entityId = entityId; + + const contributions = await prisma.contribution.findMany({ + where, + include: { user: { select: { name: true } } }, + orderBy: { createdAt: 'desc' }, + take: 50, + }); + res.json(contributions); + } catch (err) { + next(err); + } +}); + +// POST /api/contributions — submit a contribution (auth required) +router.post('/', requireAuth, async (req, res, next) => { + try { + const { entityType, entityId, type, content } = req.body; + + const validEntityTypes = ['truck_stop', 'weigh_station', 'bridge', 'regulation']; + const validTypes = ['info', 'flag', 'confirm']; + + if (!validEntityTypes.includes(entityType)) { + return res.status(400).json({ error: `entityType must be one of: ${validEntityTypes.join(', ')}` }); + } + if (!validTypes.includes(type)) { + return res.status(400).json({ error: `type must be one of: ${validTypes.join(', ')}` }); + } + if (!entityId || !content) { + return res.status(400).json({ error: 'entityId and content are required.' }); + } + + const data = { + userId: req.user.id, + entityType, + entityId, + type, + content, + }; + + // Link to the specific entity if it exists + if (entityType === 'truck_stop') { + const exists = await prisma.truckStop.findUnique({ where: { id: entityId } }); + if (exists) data.truckStopId = entityId; + } else if (entityType === 'weigh_station') { + const exists = await prisma.weighStation.findUnique({ where: { id: entityId } }); + if (exists) data.weighStationId = entityId; + } + + const contribution = await prisma.contribution.create({ + data, + include: { user: { select: { name: true } } }, + }); + + res.status(201).json(contribution); + } catch (err) { + next(err); + } +}); + +module.exports = router; diff --git a/server/src/routes/documents.js b/server/src/routes/documents.js new file mode 100644 index 0000000..0074d30 --- /dev/null +++ b/server/src/routes/documents.js @@ -0,0 +1,119 @@ +const express = require('express'); +const multer = require('multer'); +const path = require('path'); +const fs = require('fs'); +const prisma = require('../config/db'); +const { requireAuth } = require('../middleware/auth'); + +const router = express.Router(); + +// Configure multer for file uploads +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + const uploadDir = path.join(__dirname, '..', '..', 'uploads', req.user.id); + fs.mkdirSync(uploadDir, { recursive: true }); + cb(null, uploadDir); + }, + filename: (req, file, cb) => { + const uniqueName = `${Date.now()}-${file.originalname}`; + cb(null, uniqueName); + }, +}); + +const upload = multer({ + storage, + limits: { fileSize: 10 * 1024 * 1024 }, // 10MB + fileFilter: (req, file, cb) => { + const allowed = ['.pdf', '.jpg', '.jpeg', '.png', '.doc', '.docx']; + const ext = path.extname(file.originalname).toLowerCase(); + if (allowed.includes(ext)) { + cb(null, true); + } else { + cb(new Error(`File type ${ext} not allowed. Accepted: ${allowed.join(', ')}`)); + } + }, +}); + +// GET /api/documents — list own documents +router.get('/', requireAuth, async (req, res, next) => { + try { + const documents = await prisma.document.findMany({ + where: { userId: req.user.id }, + orderBy: { createdAt: 'desc' }, + }); + res.json(documents); + } catch (err) { + next(err); + } +}); + +// POST /api/documents — upload a document +router.post('/', requireAuth, upload.single('file'), async (req, res, next) => { + try { + if (!req.file) { + return res.status(400).json({ error: 'No file uploaded.' }); + } + + const { type, expiresAt } = req.body; + const validTypes = ['permit', 'insurance', 'certification', 'license', 'other']; + const docType = validTypes.includes(type) ? type : 'other'; + + const document = await prisma.document.create({ + data: { + userId: req.user.id, + type: docType, + filename: req.file.originalname, + filepath: req.file.path, + mimeType: req.file.mimetype, + sizeBytes: req.file.size, + expiresAt: expiresAt ? new Date(expiresAt) : null, + }, + }); + + res.status(201).json(document); + } catch (err) { + next(err); + } +}); + +// GET /api/documents/:id/download — download a document +router.get('/:id/download', requireAuth, async (req, res, next) => { + try { + const document = await prisma.document.findUnique({ + where: { id: req.params.id }, + }); + if (!document) return res.status(404).json({ error: 'Document not found.' }); + if (document.userId !== req.user.id && req.user.role !== 'admin') { + return res.status(403).json({ error: 'Access denied.' }); + } + + res.download(document.filepath, document.filename); + } catch (err) { + next(err); + } +}); + +// DELETE /api/documents/:id +router.delete('/:id', requireAuth, async (req, res, next) => { + try { + const document = await prisma.document.findUnique({ + where: { id: req.params.id }, + }); + if (!document) return res.status(404).json({ error: 'Document not found.' }); + if (document.userId !== req.user.id && req.user.role !== 'admin') { + return res.status(403).json({ error: 'Access denied.' }); + } + + // Delete file from disk + if (fs.existsSync(document.filepath)) { + fs.unlinkSync(document.filepath); + } + + await prisma.document.delete({ where: { id: req.params.id } }); + res.json({ message: 'Document deleted.' }); + } catch (err) { + next(err); + } +}); + +module.exports = router; diff --git a/server/src/routes/loadboard.js b/server/src/routes/loadboard.js new file mode 100644 index 0000000..2dd8c9c --- /dev/null +++ b/server/src/routes/loadboard.js @@ -0,0 +1,119 @@ +const express = require('express'); +const prisma = require('../config/db'); +const { requireAuth } = require('../middleware/auth'); + +const router = express.Router(); + +// GET /api/loads — list loads with search/filter +router.get('/', async (req, res, next) => { + try { + const { origin, destination, minDate, maxDate, status, page = 1, limit = 20 } = req.query; + const where = {}; + + if (origin) where.origin = { contains: origin, mode: 'insensitive' }; + if (destination) where.destination = { contains: destination, mode: 'insensitive' }; + if (status) where.status = status; + if (minDate || maxDate) { + where.pickupDate = {}; + if (minDate) where.pickupDate.gte = new Date(minDate); + if (maxDate) where.pickupDate.lte = new Date(maxDate); + } + + const skip = (parseInt(page) - 1) * parseInt(limit); + const [loads, total] = await Promise.all([ + prisma.load.findMany({ + where, + include: { poster: { select: { name: true, role: true } } }, + orderBy: { createdAt: 'desc' }, + skip, + take: parseInt(limit), + }), + prisma.load.count({ where }), + ]); + + res.json({ loads, total, page: parseInt(page), totalPages: Math.ceil(total / parseInt(limit)) }); + } catch (err) { + next(err); + } +}); + +// GET /api/loads/:id +router.get('/:id', async (req, res, next) => { + try { + const load = await prisma.load.findUnique({ + where: { id: req.params.id }, + include: { poster: { select: { name: true, role: true } } }, + }); + if (!load) return res.status(404).json({ error: 'Load not found.' }); + res.json(load); + } catch (err) { + next(err); + } +}); + +// POST /api/loads — create a load posting (auth required) +router.post('/', requireAuth, async (req, res, next) => { + try { + const { origin, destination, pickupDate, width, height, length, weight, description, escortsNeeded } = req.body; + + if (!origin || !destination || !pickupDate) { + return res.status(400).json({ error: 'Origin, destination, and pickup date are required.' }); + } + + const load = await prisma.load.create({ + data: { + posterId: req.user.id, + origin, + destination, + pickupDate: new Date(pickupDate), + width: width || '', + height: height || '', + length: length || '', + weight: weight || '', + description: description || '', + escortsNeeded: parseInt(escortsNeeded) || 1, + }, + }); + + res.status(201).json(load); + } catch (err) { + next(err); + } +}); + +// PUT /api/loads/:id — update own load +router.put('/:id', requireAuth, async (req, res, next) => { + try { + const existing = await prisma.load.findUnique({ where: { id: req.params.id } }); + if (!existing) return res.status(404).json({ error: 'Load not found.' }); + if (existing.posterId !== req.user.id && req.user.role !== 'admin') { + return res.status(403).json({ error: 'You can only edit your own loads.' }); + } + + const load = await prisma.load.update({ + where: { id: req.params.id }, + data: req.body, + }); + res.json(load); + } catch (err) { + next(err); + } +}); + +// DELETE /api/loads/:id +router.delete('/:id', requireAuth, async (req, res, next) => { + try { + const existing = await prisma.load.findUnique({ where: { id: req.params.id } }); + if (!existing) return res.status(404).json({ error: 'Load not found.' }); + if (existing.posterId !== req.user.id && req.user.role !== 'admin') { + return res.status(403).json({ error: 'You can only delete your own loads.' }); + } + + await prisma.load.delete({ where: { id: req.params.id } }); + res.json({ message: 'Load deleted.' }); + } catch (err) { + next(err); + } +}); + +module.exports = router; diff --git a/server/src/routes/locator.js b/server/src/routes/locator.js new file mode 100644 index 0000000..444fed2 --- /dev/null +++ b/server/src/routes/locator.js @@ -0,0 +1,111 @@ +const express = require('express'); +const prisma = require('../config/db'); +const { requireAuth } = require('../middleware/auth'); + +const router = express.Router(); + +// GET /api/escorts?lat=...&lng=...&radius=...&availability=... +router.get('/', async (req, res, next) => { + try { + const { lat, lng, radius, availability } = req.query; + const where = {}; + + if (availability) { + where.availability = availability; + } + + let escorts = await prisma.escortProfile.findMany({ + where, + include: { user: { select: { name: true, email: true } } }, + orderBy: { rating: 'desc' }, + }); + + // Distance filter + if (lat && lng && radius) { + const centerLat = parseFloat(lat); + const centerLng = parseFloat(lng); + const maxMiles = parseFloat(radius); + + escorts = escorts.filter((e) => { + const dist = haversine(centerLat, centerLng, e.lat, e.lng); + e.distanceMiles = Math.round(dist * 10) / 10; + return dist <= maxMiles; + }); + escorts.sort((a, b) => a.distanceMiles - b.distanceMiles); + } + + res.json(escorts); + } catch (err) { + next(err); + } +}); + +// GET /api/escorts/:id +router.get('/:id', async (req, res, next) => { + try { + const profile = await prisma.escortProfile.findUnique({ + where: { id: req.params.id }, + include: { user: { select: { name: true, email: true } } }, + }); + if (!profile) return res.status(404).json({ error: 'Escort profile not found.' }); + res.json(profile); + } catch (err) { + next(err); + } +}); + +// POST /api/escorts/profile — create or update own profile (auth required) +router.post('/profile', requireAuth, async (req, res, next) => { + try { + const { lat, lng, radiusMiles, certifications, vehicleType, availability, phone, bio } = req.body; + + if (!lat || !lng) { + return res.status(400).json({ error: 'Location (lat, lng) is required.' }); + } + + const profile = await prisma.escortProfile.upsert({ + where: { userId: req.user.id }, + update: { + lat: parseFloat(lat), + lng: parseFloat(lng), + radiusMiles: parseInt(radiusMiles) || 100, + certifications: certifications || [], + vehicleType: vehicleType || '', + availability: availability || 'available', + phone: phone || '', + bio: bio || '', + }, + create: { + userId: req.user.id, + lat: parseFloat(lat), + lng: parseFloat(lng), + radiusMiles: parseInt(radiusMiles) || 100, + certifications: certifications || [], + vehicleType: vehicleType || '', + availability: availability || 'available', + phone: phone || '', + bio: bio || '', + }, + }); + + res.json(profile); + } catch (err) { + next(err); + } +}); + +function haversine(lat1, lng1, lat2, lng2) { + const R = 3959; + const dLat = toRad(lat2 - lat1); + const dLng = toRad(lng2 - lng1); + const a = + Math.sin(dLat / 2) ** 2 + + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2; + return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); +} + +function toRad(deg) { + return (deg * Math.PI) / 180; +} + +module.exports = router; diff --git a/server/src/routes/orders.js b/server/src/routes/orders.js new file mode 100644 index 0000000..14e36b3 --- /dev/null +++ b/server/src/routes/orders.js @@ -0,0 +1,92 @@ +const express = require('express'); +const prisma = require('../config/db'); +const { requireAuth } = require('../middleware/auth'); + +const router = express.Router(); + +// GET /api/orders — list own orders +router.get('/', requireAuth, async (req, res, next) => { + try { + const orders = await prisma.order.findMany({ + where: { userId: req.user.id }, + orderBy: { createdAt: 'desc' }, + }); + res.json(orders); + } catch (err) { + next(err); + } +}); + +// GET /api/orders/:id +router.get('/:id', requireAuth, async (req, res, next) => { + try { + const order = await prisma.order.findUnique({ + where: { id: req.params.id }, + }); + if (!order) return res.status(404).json({ error: 'Order not found.' }); + if (order.userId !== req.user.id && req.user.role !== 'admin') { + return res.status(403).json({ error: 'Access denied.' }); + } + res.json(order); + } catch (err) { + next(err); + } +}); + +// POST /api/orders — submit escort service request +router.post('/', requireAuth, async (req, res, next) => { + try { + const { origin, destination, pickupDate, width, height, length, weight, loadType, escortsNeeded, notes } = req.body; + + if (!origin || !destination || !pickupDate) { + return res.status(400).json({ error: 'Origin, destination, and pickup date are required.' }); + } + + const order = await prisma.order.create({ + data: { + userId: req.user.id, + origin, + destination, + pickupDate: new Date(pickupDate), + width: width || '', + height: height || '', + length: length || '', + weight: weight || '', + loadType: loadType || '', + escortsNeeded: parseInt(escortsNeeded) || 1, + notes: notes || '', + }, + }); + + res.status(201).json(order); + } catch (err) { + next(err); + } +}); + +// PUT /api/orders/:id/status — update order status (admin or owner) +router.put('/:id/status', requireAuth, async (req, res, next) => { + try { + const { status } = req.body; + const validStatuses = ['pending', 'confirmed', 'in_progress', 'completed', 'cancelled']; + if (!validStatuses.includes(status)) { + return res.status(400).json({ error: `Status must be one of: ${validStatuses.join(', ')}` }); + } + + const order = await prisma.order.findUnique({ where: { id: req.params.id } }); + if (!order) return res.status(404).json({ error: 'Order not found.' }); + if (order.userId !== req.user.id && req.user.role !== 'admin') { + return res.status(403).json({ error: 'Access denied.' }); + } + + const updated = await prisma.order.update({ + where: { id: req.params.id }, + data: { status }, + }); + res.json(updated); + } catch (err) { + next(err); + } +}); + +module.exports = router; diff --git a/server/src/routes/regulations.js b/server/src/routes/regulations.js new file mode 100644 index 0000000..26c4ad9 --- /dev/null +++ b/server/src/routes/regulations.js @@ -0,0 +1,38 @@ +const express = require('express'); +const prisma = require('../config/db'); + +const router = express.Router(); + +// GET /api/regulations — list all states with regulation data +router.get('/', async (req, res, next) => { + try { + const states = await prisma.state.findMany({ + include: { regulation: true }, + orderBy: { name: 'asc' }, + }); + res.json(states); + } catch (err) { + next(err); + } +}); + +// GET /api/regulations/:stateAbbr — single state with regulation + equipment +router.get('/:stateAbbr', async (req, res, next) => { + try { + const abbr = req.params.stateAbbr.toUpperCase(); + const state = await prisma.state.findUnique({ + where: { abbr }, + include: { + regulation: true, + equipmentRequirements: true, + }, + }); + + if (!state) return res.status(404).json({ error: `State '${abbr}' not found.` }); + res.json(state); + } catch (err) { + next(err); + } +}); + +module.exports = router; diff --git a/server/src/routes/truckstops.js b/server/src/routes/truckstops.js new file mode 100644 index 0000000..c5306a0 --- /dev/null +++ b/server/src/routes/truckstops.js @@ -0,0 +1,86 @@ +const express = require('express'); +const prisma = require('../config/db'); + +const router = express.Router(); + +// GET /api/truckstops?lat=...&lng=...&radius=...&state=... +router.get('/', async (req, res, next) => { + try { + const { lat, lng, radius, state } = req.query; + const where = {}; + + if (state) { + where.state = { abbr: state.toUpperCase() }; + } + + let truckStops = await prisma.truckStop.findMany({ + where, + include: { + state: { select: { name: true, abbr: true } }, + contributions: { + orderBy: { createdAt: 'desc' }, + take: 5, + select: { id: true, type: true, content: true, createdAt: true }, + }, + }, + orderBy: { name: 'asc' }, + }); + + // Client-side distance filter (Haversine) when lat/lng/radius provided + // For production, replace with PostGIS ST_DWithin + if (lat && lng && radius) { + const centerLat = parseFloat(lat); + const centerLng = parseFloat(lng); + const maxMiles = parseFloat(radius); + + truckStops = truckStops.filter((ts) => { + const dist = haversine(centerLat, centerLng, ts.lat, ts.lng); + ts.distanceMiles = Math.round(dist * 10) / 10; + return dist <= maxMiles; + }); + + truckStops.sort((a, b) => a.distanceMiles - b.distanceMiles); + } + + res.json(truckStops); + } catch (err) { + next(err); + } +}); + +// GET /api/truckstops/:id +router.get('/:id', async (req, res, next) => { + try { + const truckStop = await prisma.truckStop.findUnique({ + where: { id: req.params.id }, + include: { + state: { select: { name: true, abbr: true } }, + contributions: { + orderBy: { createdAt: 'desc' }, + include: { user: { select: { name: true } } }, + }, + }, + }); + if (!truckStop) return res.status(404).json({ error: 'Truck stop not found.' }); + res.json(truckStop); + } catch (err) { + next(err); + } +}); + +// Haversine distance in miles +function haversine(lat1, lng1, lat2, lng2) { + const R = 3959; // Earth radius in miles + const dLat = toRad(lat2 - lat1); + const dLng = toRad(lng2 - lng1); + const a = + Math.sin(dLat / 2) ** 2 + + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2; + return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); +} + +function toRad(deg) { + return (deg * Math.PI) / 180; +} + +module.exports = router; diff --git a/server/src/routes/weighstations.js b/server/src/routes/weighstations.js new file mode 100644 index 0000000..fdedfc0 --- /dev/null +++ b/server/src/routes/weighstations.js @@ -0,0 +1,86 @@ +const express = require('express'); +const prisma = require('../config/db'); + +const router = express.Router(); + +// GET /api/weighstations?lat=...&lng=...&radius=...&state=...&status=... +router.get('/', async (req, res, next) => { + try { + const { lat, lng, radius, state, status } = req.query; + const where = {}; + + if (state) { + where.state = { abbr: state.toUpperCase() }; + } + if (status) { + where.currentStatus = status; + } + + let stations = await prisma.weighStation.findMany({ + where, + include: { + state: { select: { name: true, abbr: true } }, + contributions: { + orderBy: { createdAt: 'desc' }, + take: 3, + select: { id: true, type: true, content: true, createdAt: true }, + }, + }, + orderBy: { name: 'asc' }, + }); + + // Distance filter + if (lat && lng && radius) { + const centerLat = parseFloat(lat); + const centerLng = parseFloat(lng); + const maxMiles = parseFloat(radius); + + stations = stations.filter((ws) => { + const dist = haversine(centerLat, centerLng, ws.lat, ws.lng); + ws.distanceMiles = Math.round(dist * 10) / 10; + return dist <= maxMiles; + }); + stations.sort((a, b) => a.distanceMiles - b.distanceMiles); + } + + res.json(stations); + } catch (err) { + next(err); + } +}); + +// GET /api/weighstations/:id +router.get('/:id', async (req, res, next) => { + try { + const station = await prisma.weighStation.findUnique({ + where: { id: req.params.id }, + include: { + state: { select: { name: true, abbr: true } }, + contributions: { + orderBy: { createdAt: 'desc' }, + include: { user: { select: { name: true } } }, + }, + }, + }); + if (!station) return res.status(404).json({ error: 'Weigh station not found.' }); + res.json(station); + } catch (err) { + next(err); + } +}); + +function haversine(lat1, lng1, lat2, lng2) { + const R = 3959; + const dLat = toRad(lat2 - lat1); + const dLng = toRad(lng2 - lng1); + const a = + Math.sin(dLat / 2) ** 2 + + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2; + return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); +} + +function toRad(deg) { + return (deg * Math.PI) / 180; +} + +module.exports = router; diff --git a/server/src/seeds/seed.js b/server/src/seeds/seed.js new file mode 100644 index 0000000..616e5b4 --- /dev/null +++ b/server/src/seeds/seed.js @@ -0,0 +1,346 @@ +// ===================================================================== +// Database Seed Script +// Reads mock data from the existing frontend JS files and inserts +// into PostgreSQL via Prisma. +// Run with: npm run db:seed +// ===================================================================== + +const fs = require('fs'); +const path = require('path'); +const prisma = require('../config/db'); + +// Load the mock data files by evaluating them in a controlled context +function loadMockData(filename) { + const filepath = path.join(__dirname, '..', '..', '..', filename); + const code = fs.readFileSync(filepath, 'utf-8'); + const context = {}; + // Execute the JS to populate the const variables + const fn = new Function(code + '\nreturn { ' + + 'MOCK_STATE_REGULATIONS: typeof MOCK_STATE_REGULATIONS !== "undefined" ? MOCK_STATE_REGULATIONS : undefined,' + + 'MOCK_LOAD_BOARD: typeof MOCK_LOAD_BOARD !== "undefined" ? MOCK_LOAD_BOARD : undefined,' + + 'MOCK_ESCORT_OPERATORS: typeof MOCK_ESCORT_OPERATORS !== "undefined" ? MOCK_ESCORT_OPERATORS : undefined,' + + 'MOCK_STATE_CONTACTS: typeof MOCK_STATE_CONTACTS !== "undefined" ? MOCK_STATE_CONTACTS : undefined,' + + 'MOCK_STATE_EQUIPMENT: typeof MOCK_STATE_EQUIPMENT !== "undefined" ? MOCK_STATE_EQUIPMENT : undefined,' + + 'MOCK_TRUCK_STOPS: typeof MOCK_TRUCK_STOPS !== "undefined" ? MOCK_TRUCK_STOPS : undefined,' + + 'MOCK_BRIDGE_CLEARANCES: typeof MOCK_BRIDGE_CLEARANCES !== "undefined" ? MOCK_BRIDGE_CLEARANCES : undefined,' + + 'MOCK_WEIGH_STATIONS: typeof MOCK_WEIGH_STATIONS !== "undefined" ? MOCK_WEIGH_STATIONS : undefined,' + + 'MOCK_ROUTE_CONDITIONS: typeof MOCK_ROUTE_CONDITIONS !== "undefined" ? MOCK_ROUTE_CONDITIONS : undefined,' + + 'MOCK_WEATHER_ALERTS: typeof MOCK_WEATHER_ALERTS !== "undefined" ? MOCK_WEATHER_ALERTS : undefined,' + + 'MOCK_SEASONAL_RESTRICTIONS: typeof MOCK_SEASONAL_RESTRICTIONS !== "undefined" ? MOCK_SEASONAL_RESTRICTIONS : undefined,' + + 'MOCK_DOCUMENTS: typeof MOCK_DOCUMENTS !== "undefined" ? MOCK_DOCUMENTS : undefined' + + ' };'); + return fn(); +} + +async function seed() { + console.log('🌱 Seeding database...\n'); + + // Load mock data from both files + const data1 = loadMockData('mock-data.js'); + const data2 = loadMockData('mock-data-extended.js'); + + // Clear existing data in reverse dependency order + console.log(' 🗑️ Clearing existing data...'); + await prisma.contribution.deleteMany(); + await prisma.document.deleteMany(); + await prisma.order.deleteMany(); + await prisma.load.deleteMany(); + await prisma.escortProfile.deleteMany(); + await prisma.user.deleteMany(); + await prisma.alert.deleteMany(); + await prisma.weighStation.deleteMany(); + await prisma.bridge.deleteMany(); + await prisma.truckStop.deleteMany(); + await prisma.seasonalRestriction.deleteMany(); + await prisma.contact.deleteMany(); + await prisma.equipmentRequirement.deleteMany(); + await prisma.regulation.deleteMany(); + await prisma.state.deleteMany(); + + // --------------------------------------------------------------- + // 1. States + Regulations (from MOCK_STATE_REGULATIONS) + // --------------------------------------------------------------- + console.log(' 📍 Seeding states and regulations...'); + const stateMap = {}; // abbr -> state.id + + for (const reg of data1.MOCK_STATE_REGULATIONS) { + const state = await prisma.state.create({ + data: { + name: reg.name, + abbr: reg.abbr, + lat: reg.lat, + lng: reg.lng, + regulation: { + create: { + permitWidth: reg.permitWidth, + permitHeight: reg.permitHeight, + permitLength: reg.permitLength, + permitWeight: reg.permitWeight, + escortWidth: reg.escortWidth, + escortHeight: reg.escortHeight, + escortLength: reg.escortLength, + escortWeight: reg.escortWeight, + travelRestrictions: reg.travel, + holidays: reg.holidays, + agency: reg.agency, + url: reg.url, + notes: reg.notes, + }, + }, + }, + }); + stateMap[reg.abbr] = state.id; + } + console.log(` ✅ ${Object.keys(stateMap).length} states with regulations`); + + // --------------------------------------------------------------- + // 2. Contacts (from MOCK_STATE_CONTACTS) + // --------------------------------------------------------------- + console.log(' 📞 Seeding contacts...'); + let contactCount = 0; + if (data2.MOCK_STATE_CONTACTS) { + for (const [abbr, contact] of Object.entries(data2.MOCK_STATE_CONTACTS)) { + if (!stateMap[abbr]) continue; + await prisma.contact.create({ + data: { + stateId: stateMap[abbr], + permitPhone: contact.permit, + policePhone: contact.police, + email: contact.email, + hours: contact.hours, + portalUrl: contact.portal, + }, + }); + contactCount++; + } + } + console.log(` ✅ ${contactCount} state contacts`); + + // --------------------------------------------------------------- + // 3. Equipment Requirements (from MOCK_STATE_EQUIPMENT) + // --------------------------------------------------------------- + console.log(' 🔧 Seeding equipment requirements...'); + let equipCount = 0; + if (data2.MOCK_STATE_EQUIPMENT) { + for (const [abbr, equip] of Object.entries(data2.MOCK_STATE_EQUIPMENT)) { + if (!stateMap[abbr]) continue; + + if (equip.escort) { + await prisma.equipmentRequirement.create({ + data: { + stateId: stateMap[abbr], + type: 'escort', + certification: equip.escort.certification || '', + vehicle: equip.escort.vehicle || '', + signs: equip.escort.signs || '', + lights: equip.escort.lights || '', + heightPole: equip.escort.heightPole || '', + flags: equip.escort.flags || '', + safetyGear: equip.escort.safety || '', + communication: equip.escort.communication || '', + }, + }); + equipCount++; + } + + if (equip.carrier) { + await prisma.equipmentRequirement.create({ + data: { + stateId: stateMap[abbr], + type: 'carrier', + signs: equip.carrier.signs || '', + flags: equip.carrier.flags || '', + lights: equip.carrier.lights || '', + safetyGear: [ + equip.carrier.cones ? `Cones: ${equip.carrier.cones}` : '', + equip.carrier.fireExtinguisher ? `Fire ext: ${equip.carrier.fireExtinguisher}` : '', + equip.carrier.triangles ? `Triangles: ${equip.carrier.triangles}` : '', + equip.carrier.flares ? `Flares: ${equip.carrier.flares}` : '', + equip.carrier.firstAid ? `First aid: ${equip.carrier.firstAid}` : '', + ].filter(Boolean).join('; '), + }, + }); + equipCount++; + } + } + } + console.log(` ✅ ${equipCount} equipment requirements`); + + // --------------------------------------------------------------- + // 4. Truck Stops (from MOCK_TRUCK_STOPS) + // --------------------------------------------------------------- + console.log(' ⛽ Seeding truck stops...'); + let tsCount = 0; + if (data2.MOCK_TRUCK_STOPS) { + for (const ts of data2.MOCK_TRUCK_STOPS) { + const stateAbbr = ts.location?.state; + const stateId = stateMap[stateAbbr]; + if (!stateId) continue; + + await prisma.truckStop.create({ + data: { + stateId, + name: ts.name, + lat: ts.location.lat, + lng: ts.location.lng, + address: `${ts.location.city}, ${ts.location.state}`, + hasOversizeParking: ts.oversizeFriendly || false, + entranceHeight: ts.entranceHeight || '', + entranceWidth: ts.entranceWidth || '', + lotSqFt: ts.lotSize ? parseInt(String(ts.lotSize).replace(/[^0-9]/g, '')) || null : null, + facilities: ts.facilities || [], + phone: '', + }, + }); + tsCount++; + } + } + console.log(` ✅ ${tsCount} truck stops`); + + // --------------------------------------------------------------- + // 5. Bridge Clearances (from MOCK_BRIDGE_CLEARANCES) + // --------------------------------------------------------------- + console.log(' 🌉 Seeding bridges...'); + let bridgeCount = 0; + if (data2.MOCK_BRIDGE_CLEARANCES) { + for (const b of data2.MOCK_BRIDGE_CLEARANCES) { + const stateAbbr = b.location?.state; + const stateId = stateMap[stateAbbr]; + if (!stateId) continue; + + await prisma.bridge.create({ + data: { + stateId, + name: `${b.type || 'Bridge'} at ${b.location.desc || b.route}`, + lat: b.location.lat, + lng: b.location.lng, + route: b.route, + heightClearance: parseFloat(String(b.clearanceHeight).replace(/[^0-9.]/g, '')) || 0, + widthClearance: b.clearanceWidth ? parseFloat(String(b.clearanceWidth).replace(/[^0-9.]/g, '')) : null, + weightLimit: b.weightLimit ? parseFloat(String(b.weightLimit).replace(/[^0-9.]/g, '')) : null, + }, + }); + bridgeCount++; + } + } + console.log(` ✅ ${bridgeCount} bridges`); + + // --------------------------------------------------------------- + // 6. Weigh Stations (from MOCK_WEIGH_STATIONS) + // --------------------------------------------------------------- + console.log(' ⚖️ Seeding weigh stations...'); + let wsCount = 0; + if (data2.MOCK_WEIGH_STATIONS) { + for (const ws of data2.MOCK_WEIGH_STATIONS) { + const stateAbbr = ws.location?.state; + const stateId = stateMap[stateAbbr]; + if (!stateId) continue; + + await prisma.weighStation.create({ + data: { + stateId, + name: ws.name, + lat: ws.location.lat, + lng: ws.location.lng, + direction: ws.direction || '', + route: ws.route || '', + prePass: ws.prePass || false, + hours: ws.hours || '', + currentStatus: ws.currentStatus || 'unknown', + }, + }); + wsCount++; + } + } + console.log(` ✅ ${wsCount} weigh stations`); + + // --------------------------------------------------------------- + // 7. Alerts — Route Conditions + Weather (from MOCK_ROUTE_CONDITIONS + MOCK_WEATHER_ALERTS) + // --------------------------------------------------------------- + console.log(' 🚨 Seeding alerts...'); + let alertCount = 0; + + if (data2.MOCK_ROUTE_CONDITIONS) { + for (const rc of data2.MOCK_ROUTE_CONDITIONS) { + const stateAbbr = rc.location?.state; + const stateId = stateMap[stateAbbr]; + if (!stateId) continue; + + await prisma.alert.create({ + data: { + stateId, + type: rc.type || 'construction', + route: rc.route || '', + description: rc.description || '', + severity: rc.severity || 'info', + startsAt: rc.startDate ? new Date(rc.startDate) : new Date(), + endsAt: rc.endDate ? new Date(rc.endDate) : null, + }, + }); + alertCount++; + } + } + + if (data2.MOCK_WEATHER_ALERTS) { + for (const wa of data2.MOCK_WEATHER_ALERTS) { + // Weather alerts may not have a state directly, try to match from region + const stateAbbr = wa.state || (wa.routes?.[0] ? 'TX' : null); // fallback + const stateId = stateAbbr ? stateMap[stateAbbr] : Object.values(stateMap)[0]; + if (!stateId) continue; + + await prisma.alert.create({ + data: { + stateId, + type: wa.type || 'weather', + route: (wa.routes || []).join(', '), + description: wa.description || '', + severity: wa.severity || 'info', + startsAt: wa.validFrom ? new Date(wa.validFrom) : new Date(), + endsAt: wa.validTo ? new Date(wa.validTo) : null, + }, + }); + alertCount++; + } + } + console.log(` ✅ ${alertCount} alerts`); + + // --------------------------------------------------------------- + // 8. Seasonal Restrictions (from MOCK_SEASONAL_RESTRICTIONS) + // --------------------------------------------------------------- + console.log(' 📅 Seeding seasonal restrictions...'); + let seasonCount = 0; + if (data2.MOCK_SEASONAL_RESTRICTIONS) { + for (const sr of data2.MOCK_SEASONAL_RESTRICTIONS) { + const stateAbbr = sr.state; + const stateId = stateMap[stateAbbr]; + if (!stateId) continue; + + await prisma.seasonalRestriction.create({ + data: { + stateId, + name: sr.title || sr.type, + type: sr.type || 'other', + startMonth: sr.startMonth || 1, + endMonth: sr.endMonth || 12, + description: sr.description || '', + }, + }); + seasonCount++; + } + } + console.log(` ✅ ${seasonCount} seasonal restrictions`); + + // --------------------------------------------------------------- + // Summary + // --------------------------------------------------------------- + console.log('\n🎉 Seed complete!\n'); +} + +seed() + .catch((err) => { + console.error('❌ Seed failed:', err); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/truckstops.html b/truckstops.html index 45a2a17..842f0ef 100644 --- a/truckstops.html +++ b/truckstops.html @@ -65,14 +65,16 @@ - - + diff --git a/weighstations.html b/weighstations.html index f7f9f12..73fbbb4 100644 --- a/weighstations.html +++ b/weighstations.html @@ -90,14 +90,16 @@ - - +