Backup: 2025-06-07 18:32 - Production setup complete

[Restore from backup: vip-coordinator-backup-2025-06-07-18-32-production-setup-complete]
This commit is contained in:
2025-06-07 18:32:00 +02:00
parent aa900505b9
commit ae3702c3b1
32 changed files with 2120 additions and 1494 deletions

View File

@@ -1,5 +1,5 @@
# Database Configuration
DATABASE_URL=postgresql://postgres:password@db:5432/vip_coordinator
DATABASE_URL=postgresql://postgres:changeme@db:5432/vip_coordinator
# Redis Configuration
REDIS_URL=redis://redis:6379
@@ -8,7 +8,7 @@ REDIS_URL=redis://redis:6379
JWT_SECRET=your-super-secure-jwt-secret-key-change-in-production-12345
SESSION_SECRET=your-super-secure-session-secret-change-in-production-67890
# Google OAuth Configuration
# Google OAuth Configuration (optional for local development)
GOOGLE_CLIENT_ID=308004695553-6k34bbq22frc4e76kejnkgq8mncepbbg.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=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
GOOGLE_REDIRECT_URI=https://api.bsa.madeamess.online/auth/google/callback
@@ -21,3 +21,6 @@ AVIATIONSTACK_API_KEY=your-aviationstack-api-key
# Admin Configuration
ADMIN_PASSWORD=admin123
# Port Configuration
PORT=3000

View File

@@ -1,5 +1,5 @@
# Multi-stage build for development and production
FROM node:18-alpine AS base
FROM node:22-alpine AS base
WORKDIR /app
@@ -15,10 +15,7 @@ CMD ["npm", "run", "dev"]
# Production stage
FROM base AS production
RUN npm ci
RUN npm install
COPY . .
RUN npm run build
RUN npm prune --omit=dev
ENV NODE_ENV=production
EXPOSE 3000
CMD ["npm", "start"]
CMD ["npm", "run", "dev"]

View File

@@ -13,7 +13,6 @@
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.2",
"jwks-rsa": "^3.2.0",
"pg": "^8.11.3",
"redis": "^4.6.8",
"uuid": "^9.0.0"
@@ -80,7 +79,6 @@
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz",
"integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==",
"license": "MIT",
"peer": true,
"dependencies": {
"cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0",
@@ -154,6 +152,7 @@
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
"dev": true,
"dependencies": {
"@types/connect": "*",
"@types/node": "*"
@@ -163,6 +162,7 @@
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
@@ -180,6 +180,7 @@
"version": "4.17.22",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.22.tgz",
"integrity": "sha512-eZUmSnhRX9YRSkplpz0N+k6NljUUn5l3EWZIKZvYzhvMphEuNiyyy1viH/ejgt66JWgALwC/gtSUAeQKtSwW/w==",
"dev": true,
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.33",
@@ -191,6 +192,7 @@
"version": "4.19.6",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
"integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
"dev": true,
"dependencies": {
"@types/node": "*",
"@types/qs": "*",
@@ -201,12 +203,14 @@
"node_modules/@types/http-errors": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
"dev": true
},
"node_modules/@types/jsonwebtoken": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz",
"integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/ms": "*",
@@ -216,19 +220,21 @@
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"dev": true
},
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.17.57",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz",
"integrity": "sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==",
"peer": true,
"dev": true,
"dependencies": {
"undici-types": "~6.19.2"
}
@@ -248,17 +254,20 @@
"node_modules/@types/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
"dev": true
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"dev": true
},
"node_modules/@types/send": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
"dev": true,
"dependencies": {
"@types/mime": "^1",
"@types/node": "*"
@@ -268,6 +277,7 @@
"version": "1.15.7",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
"dev": true,
"dependencies": {
"@types/http-errors": "*",
"@types/node": "*",
@@ -1016,15 +1026,6 @@
"node": ">=0.12.0"
}
},
"node_modules/jose": {
"version": "4.15.9",
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
@@ -1064,46 +1065,6 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/jwks-rsa": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.0.tgz",
"integrity": "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww==",
"license": "MIT",
"dependencies": {
"@types/express": "^4.17.20",
"@types/jsonwebtoken": "^9.0.4",
"debug": "^4.3.4",
"jose": "^4.15.4",
"limiter": "^1.1.5",
"lru-memoizer": "^2.2.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/jwks-rsa/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/jwks-rsa/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/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
@@ -1114,17 +1075,6 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/limiter": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
"integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
"license": "MIT"
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -1167,28 +1117,6 @@
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/lru-memoizer": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz",
"integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==",
"license": "MIT",
"dependencies": {
"lodash.clonedeep": "^4.5.0",
"lru-cache": "6.0.0"
}
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@@ -1384,7 +1312,6 @@
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.0.tgz",
"integrity": "sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg==",
"license": "MIT",
"peer": true,
"dependencies": {
"pg-connection-string": "^2.9.0",
"pg-pool": "^3.10.0",
@@ -1990,7 +1917,6 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -2002,7 +1928,8 @@
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true
},
"node_modules/unpipe": {
"version": "1.0.0",
@@ -2119,7 +2046,6 @@
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz",
"integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==",
"peer": true,
"requires": {
"cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0",
@@ -2178,6 +2104,7 @@
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
"dev": true,
"requires": {
"@types/connect": "*",
"@types/node": "*"
@@ -2187,6 +2114,7 @@
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"dev": true,
"requires": {
"@types/node": "*"
}
@@ -2204,6 +2132,7 @@
"version": "4.17.22",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.22.tgz",
"integrity": "sha512-eZUmSnhRX9YRSkplpz0N+k6NljUUn5l3EWZIKZvYzhvMphEuNiyyy1viH/ejgt66JWgALwC/gtSUAeQKtSwW/w==",
"dev": true,
"requires": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.33",
@@ -2215,6 +2144,7 @@
"version": "4.19.6",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
"integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
"dev": true,
"requires": {
"@types/node": "*",
"@types/qs": "*",
@@ -2225,12 +2155,14 @@
"@types/http-errors": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
"dev": true
},
"@types/jsonwebtoken": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz",
"integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==",
"dev": true,
"requires": {
"@types/ms": "*",
"@types/node": "*"
@@ -2239,18 +2171,20 @@
"@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"dev": true
},
"@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
"dev": true
},
"@types/node": {
"version": "20.17.57",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz",
"integrity": "sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==",
"peer": true,
"dev": true,
"requires": {
"undici-types": "~6.19.2"
}
@@ -2269,17 +2203,20 @@
"@types/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
"dev": true
},
"@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"dev": true
},
"@types/send": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
"dev": true,
"requires": {
"@types/mime": "^1",
"@types/node": "*"
@@ -2289,6 +2226,7 @@
"version": "1.15.7",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
"dev": true,
"requires": {
"@types/http-errors": "*",
"@types/node": "*",
@@ -2841,11 +2779,6 @@
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true
},
"jose": {
"version": "4.15.9",
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="
},
"jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
@@ -2880,34 +2813,6 @@
"safe-buffer": "^5.0.1"
}
},
"jwks-rsa": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.0.tgz",
"integrity": "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww==",
"requires": {
"@types/express": "^4.17.20",
"@types/jsonwebtoken": "^9.0.4",
"debug": "^4.3.4",
"jose": "^4.15.4",
"limiter": "^1.1.5",
"lru-memoizer": "^2.2.0"
},
"dependencies": {
"debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"requires": {
"ms": "^2.1.3"
}
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
}
}
},
"jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
@@ -2917,16 +2822,6 @@
"safe-buffer": "^5.0.1"
}
},
"limiter": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
"integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="
},
"lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
},
"lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -2962,23 +2857,6 @@
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
},
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"requires": {
"yallist": "^4.0.0"
}
},
"lru-memoizer": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz",
"integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==",
"requires": {
"lodash.clonedeep": "^4.5.0",
"lru-cache": "6.0.0"
}
},
"make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@@ -3113,7 +2991,6 @@
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.0.tgz",
"integrity": "sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg==",
"peer": true,
"requires": {
"pg-cloudflare": "^1.2.5",
"pg-connection-string": "^2.9.0",
@@ -3509,13 +3386,13 @@
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"peer": true
"dev": true
},
"undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true
},
"unpipe": {
"version": "1.0.0",

View File

@@ -5,7 +5,7 @@
"main": "dist/index.js",
"scripts": {
"start": "node dist/index.js",
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"dev": "npx tsx src/index.ts",
"build": "tsc",
"test": "echo \"Error: no test specified\" && exit 1"
},
@@ -22,7 +22,6 @@
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.2",
"jwks-rsa": "^3.2.0",
"pg": "^8.11.3",
"redis": "^4.6.8",
"uuid": "^9.0.0"
@@ -34,7 +33,9 @@
"@types/node": "^20.5.0",
"@types/pg": "^8.10.2",
"@types/uuid": "^9.0.2",
"ts-node": "^10.9.1",
"ts-node-dev": "^2.0.0",
"typescript": "^5.1.6"
"tsx": "^4.7.0",
"typescript": "^5.6.0"
}
}

View File

@@ -3,11 +3,8 @@ import dotenv from 'dotenv';
dotenv.config();
const useSSL = process.env.DATABASE_SSL === 'true';
const pool = new Pool({
connectionString: process.env.DATABASE_URL || 'postgresql://postgres:changeme@localhost:5432/vip_coordinator',
ssl: useSSL ? { rejectUnauthorized: false } : false,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,

View File

@@ -1,178 +1,134 @@
import jwt, { JwtHeader, JwtPayload } from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
import jwt from 'jsonwebtoken';
const auth0Domain = process.env.AUTH0_DOMAIN;
const auth0Audience = process.env.AUTH0_AUDIENCE;
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
if (!auth0Domain) {
console.warn('⚠️ AUTH0_DOMAIN is not set. Authentication routes will reject requests until configured.');
export interface User {
id: string;
google_id: string;
email: string;
name: string;
profile_picture_url?: string;
role: 'driver' | 'coordinator' | 'administrator';
created_at?: string;
last_login?: string;
is_active?: boolean;
updated_at?: string;
}
if (!auth0Audience) {
console.warn('⚠️ AUTH0_AUDIENCE is not set. Authentication routes will reject requests until configured.');
export function generateToken(user: User): string {
return jwt.sign(
{
id: user.id,
google_id: user.google_id,
email: user.email,
name: user.name,
profile_picture_url: user.profile_picture_url,
role: user.role
},
JWT_SECRET,
{ expiresIn: '24h' }
);
}
const jwks = auth0Domain
? jwksClient({
jwksUri: `https://${auth0Domain}/.well-known/jwks.json`,
cache: true,
cacheMaxEntries: 5,
cacheMaxAge: 10 * 60 * 1000
})
: null;
const PROFILE_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
const profileCache = new Map<string, { profile: Auth0UserProfile; expiresAt: number }>();
const inflightProfileRequests = new Map<string, Promise<Auth0UserProfile>>();
export interface Auth0UserProfile {
sub: string;
email?: string;
name?: string;
nickname?: string;
picture?: string;
[key: string]: unknown;
export function verifyToken(token: string): User | null {
try {
const decoded = jwt.verify(token, JWT_SECRET) as any;
return {
id: decoded.id,
google_id: decoded.google_id,
email: decoded.email,
name: decoded.name,
profile_picture_url: decoded.profile_picture_url,
role: decoded.role
};
} catch (error) {
return null;
}
}
export interface VerifiedAccessToken extends JwtPayload {
sub: string;
azp?: string;
scope?: string;
// Simple Google OAuth2 client using fetch
export async function verifyGoogleToken(googleToken: string): Promise<any> {
try {
const response = await fetch(`https://www.googleapis.com/oauth2/v1/userinfo?access_token=${googleToken}`);
if (!response.ok) {
throw new Error('Invalid Google token');
}
return await response.json();
} catch (error) {
console.error('Error verifying Google token:', error);
return null;
}
}
async function getSigningKey(header: JwtHeader): Promise<string> {
if (!jwks) {
throw new Error('Auth0 JWKS client not initialised');
// Get Google OAuth2 URL
export function getGoogleAuthUrl(): string {
const clientId = process.env.GOOGLE_CLIENT_ID;
const redirectUri = process.env.GOOGLE_REDIRECT_URI || 'http://localhost:3000/auth/google/callback';
if (!clientId) {
throw new Error('GOOGLE_CLIENT_ID not configured');
}
if (!header.kid) {
throw new Error('Token signing key id (kid) is missing');
}
const signingKey = await new Promise<jwksClient.SigningKey>((resolve, reject) => {
jwks.getSigningKey(header.kid as string, (err, key) => {
if (err) {
return reject(err);
}
if (!key) {
return reject(new Error('Signing key not found'));
}
resolve(key);
});
const params = new URLSearchParams({
client_id: clientId,
redirect_uri: redirectUri,
response_type: 'code',
scope: 'openid email profile',
access_type: 'offline',
prompt: 'consent'
});
const publicKey =
typeof signingKey.getPublicKey === 'function'
? signingKey.getPublicKey()
: (signingKey as any).publicKey || (signingKey as any).rsaPublicKey;
if (!publicKey) {
throw new Error('Unable to derive signing key');
}
return publicKey;
return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
}
export async function verifyAccessToken(token: string): Promise<VerifiedAccessToken> {
if (!auth0Domain || !auth0Audience) {
throw new Error('Auth0 configuration is incomplete');
// Exchange authorization code for tokens
export async function exchangeCodeForTokens(code: string): Promise<any> {
const clientId = process.env.GOOGLE_CLIENT_ID;
const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
const redirectUri = process.env.GOOGLE_REDIRECT_URI || 'http://localhost:3000/auth/google/callback';
if (!clientId || !clientSecret) {
throw new Error('Google OAuth credentials not configured');
}
const decoded = jwt.decode(token, { complete: true });
if (!decoded || typeof decoded === 'string') {
throw new Error('Invalid JWT');
}
const signingKey = await getSigningKey(decoded.header);
return jwt.verify(token, signingKey, {
algorithms: ['RS256'],
audience: auth0Audience,
issuer: `https://${auth0Domain}/`
}) as VerifiedAccessToken;
}
export async function fetchAuth0UserProfile(accessToken: string, cacheKey: string, expiresAt?: number): Promise<Auth0UserProfile> {
if (!auth0Domain) {
throw new Error('Auth0 configuration is incomplete');
}
const now = Date.now();
const cached = profileCache.get(cacheKey);
if (cached && cached.expiresAt > now) {
return cached.profile;
}
const ttl = expiresAt ? Math.max(0, expiresAt * 1000 - now) : PROFILE_CACHE_TTL_MS;
if (inflightProfileRequests.has(cacheKey)) {
return inflightProfileRequests.get(cacheKey)!;
}
const fetchPromise = (async () => {
const response = await fetch(`https://${auth0Domain}/userinfo`, {
try {
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`
}
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
client_id: clientId,
client_secret: clientSecret,
code,
grant_type: 'authorization_code',
redirect_uri: redirectUri,
}),
});
if (!response.ok) {
throw new Error(`Failed to fetch Auth0 user profile (${response.status})`);
throw new Error('Failed to exchange code for tokens');
}
const profile = (await response.json()) as Auth0UserProfile;
profileCache.set(cacheKey, { profile, expiresAt: now + ttl });
inflightProfileRequests.delete(cacheKey);
return profile;
})().catch(error => {
inflightProfileRequests.delete(cacheKey);
return await response.json();
} catch (error) {
console.error('Error exchanging code for tokens:', error);
throw error;
});
inflightProfileRequests.set(cacheKey, fetchPromise);
return fetchPromise;
}
export function clearAuth0ProfileCache(cacheKey?: string) {
if (cacheKey) {
profileCache.delete(cacheKey);
inflightProfileRequests.delete(cacheKey);
} else {
profileCache.clear();
inflightProfileRequests.clear();
}
}
export function getCachedProfile(cacheKey: string): Auth0UserProfile | undefined {
const cached = profileCache.get(cacheKey);
if (cached && cached.expiresAt > Date.now()) {
return cached.profile;
}
return undefined;
}
export function cacheAuth0Profile(cacheKey: string, profile: Auth0UserProfile, expiresAt?: number) {
const ttl = expiresAt ? Math.max(0, expiresAt * 1000 - Date.now()) : PROFILE_CACHE_TTL_MS;
profileCache.set(cacheKey, { profile, expiresAt: Date.now() + ttl });
}
export async function fetchFreshAuth0Profile(accessToken: string): Promise<Auth0UserProfile> {
if (!auth0Domain) {
throw new Error('Auth0 configuration is incomplete');
}
const response = await fetch(`https://${auth0Domain}/userinfo`, {
headers: {
Authorization: `Bearer ${accessToken}`
// Get user info from Google
export async function getGoogleUserInfo(accessToken: string): Promise<any> {
try {
const response = await fetch(`https://www.googleapis.com/oauth2/v2/userinfo?access_token=${accessToken}`);
if (!response.ok) {
throw new Error('Failed to get user info');
}
});
if (!response.ok) {
throw new Error(`Failed to fetch Auth0 user profile (${response.status})`);
return await response.json();
} catch (error) {
console.error('Error getting Google user info:', error);
throw error;
}
return (await response.json()) as Auth0UserProfile;
}
export function isAuth0Configured(): boolean {
return Boolean(auth0Domain && auth0Audience);
}

View File

@@ -19,6 +19,8 @@ app.use(cors({
origin: [
process.env.FRONTEND_URL || 'http://localhost:5173',
'https://bsa.madeamess.online:5173',
'https://bsa.madeamess.online',
'https://api.bsa.madeamess.online',
'http://bsa.madeamess.online:5173'
],
credentials: true
@@ -46,6 +48,9 @@ app.get('/api/health', (req: Request, res: Response) => {
// Data is now persisted using dataService - no more in-memory storage!
// Simple admin password (in production, use proper auth)
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin123';
// Initialize flight tracking scheduler
const flightTracker = new FlightTrackingScheduler(flightService);
@@ -606,21 +611,35 @@ app.get('/api/drivers/:driverId/schedule', requireAuth, async (req: Request, res
});
// Admin routes
app.get('/api/admin/settings', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => {
app.post('/api/admin/authenticate', (req: Request, res: Response) => {
const { password } = req.body;
if (password === ADMIN_PASSWORD) {
res.json({ success: true });
} else {
res.status(401).json({ error: 'Invalid password' });
}
});
app.get('/api/admin/settings', async (req: Request, res: Response) => {
const adminAuth = req.headers['admin-auth'];
if (adminAuth !== 'true') {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
const adminSettings = await enhancedDataService.getAdminSettings();
const apiKeys = adminSettings.apiKeys || {};
// Return settings but mask API keys for display only
// IMPORTANT: Don't return the actual keys, just indicate they exist
const maskedSettings = {
apiKeys: {
aviationStackKey: apiKeys.aviationStackKey ? '***' + apiKeys.aviationStackKey.slice(-4) : '',
googleMapsKey: apiKeys.googleMapsKey ? '***' + apiKeys.googleMapsKey.slice(-4) : '',
twilioKey: apiKeys.twilioKey ? '***' + apiKeys.twilioKey.slice(-4) : '',
auth0Domain: apiKeys.auth0Domain ? '***' + apiKeys.auth0Domain.slice(-4) : '',
auth0ClientId: apiKeys.auth0ClientId ? '***' + apiKeys.auth0ClientId.slice(-4) : '',
auth0ClientSecret: apiKeys.auth0ClientSecret ? '***' + apiKeys.auth0ClientSecret.slice(-4) : ''
aviationStackKey: adminSettings.apiKeys.aviationStackKey ? '***' + adminSettings.apiKeys.aviationStackKey.slice(-4) : '',
googleMapsKey: adminSettings.apiKeys.googleMapsKey ? '***' + adminSettings.apiKeys.googleMapsKey.slice(-4) : '',
twilioKey: adminSettings.apiKeys.twilioKey ? '***' + adminSettings.apiKeys.twilioKey.slice(-4) : '',
googleClientId: adminSettings.apiKeys.googleClientId ? '***' + adminSettings.apiKeys.googleClientId.slice(-4) : '',
googleClientSecret: adminSettings.apiKeys.googleClientSecret ? '***' + adminSettings.apiKeys.googleClientSecret.slice(-4) : ''
},
systemSettings: adminSettings.systemSettings
};
@@ -631,11 +650,16 @@ app.get('/api/admin/settings', requireAuth, requireRole(['administrator']), asyn
}
});
app.post('/api/admin/settings', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => {
app.post('/api/admin/settings', async (req: Request, res: Response) => {
const adminAuth = req.headers['admin-auth'];
if (adminAuth !== 'true') {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
const { apiKeys, systemSettings } = req.body;
const currentSettings = await enhancedDataService.getAdminSettings();
currentSettings.apiKeys = currentSettings.apiKeys || {};
// Update API keys (only if provided and not masked)
if (apiKeys) {
@@ -650,17 +674,15 @@ app.post('/api/admin/settings', requireAuth, requireRole(['administrator']), asy
if (apiKeys.twilioKey && !apiKeys.twilioKey.startsWith('***')) {
currentSettings.apiKeys.twilioKey = apiKeys.twilioKey;
}
if (apiKeys.auth0Domain && !apiKeys.auth0Domain.startsWith('***')) {
currentSettings.apiKeys.auth0Domain = apiKeys.auth0Domain;
process.env.AUTH0_DOMAIN = apiKeys.auth0Domain;
if (apiKeys.googleClientId && !apiKeys.googleClientId.startsWith('***')) {
currentSettings.apiKeys.googleClientId = apiKeys.googleClientId;
// Update the environment variable for Google OAuth
process.env.GOOGLE_CLIENT_ID = apiKeys.googleClientId;
}
if (apiKeys.auth0ClientId && !apiKeys.auth0ClientId.startsWith('***')) {
currentSettings.apiKeys.auth0ClientId = apiKeys.auth0ClientId;
process.env.AUTH0_CLIENT_ID = apiKeys.auth0ClientId;
}
if (apiKeys.auth0ClientSecret && !apiKeys.auth0ClientSecret.startsWith('***')) {
currentSettings.apiKeys.auth0ClientSecret = apiKeys.auth0ClientSecret;
process.env.AUTH0_CLIENT_SECRET = apiKeys.auth0ClientSecret;
if (apiKeys.googleClientSecret && !apiKeys.googleClientSecret.startsWith('***')) {
currentSettings.apiKeys.googleClientSecret = apiKeys.googleClientSecret;
// Update the environment variable for Google OAuth
process.env.GOOGLE_CLIENT_SECRET = apiKeys.googleClientSecret;
}
}
@@ -678,7 +700,13 @@ app.post('/api/admin/settings', requireAuth, requireRole(['administrator']), asy
}
});
app.post('/api/admin/test-api/:apiType', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => {
app.post('/api/admin/test-api/:apiType', async (req: Request, res: Response) => {
const adminAuth = req.headers['admin-auth'];
if (adminAuth !== 'true') {
return res.status(401).json({ error: 'Unauthorized' });
}
const { apiType } = req.params;
const { apiKey } = req.body;
@@ -727,6 +755,7 @@ async function startServer() {
// Start the server
app.listen(port, () => {
console.log(`🚀 Server is running on port ${port}`);
console.log(`🔐 Admin password: ${ADMIN_PASSWORD}`);
console.log(`📊 Admin dashboard: http://localhost:${port === 3000 ? 5173 : port}/admin`);
console.log(`🏥 Health check: http://localhost:${port}/api/health`);
console.log(`📚 API docs: http://localhost:${port}/api-docs.html`);

View File

@@ -1,176 +1,64 @@
import express, { Request, Response, NextFunction } from 'express';
import {
fetchAuth0UserProfile,
isAuth0Configured,
verifyAccessToken,
VerifiedAccessToken,
Auth0UserProfile,
getCachedProfile,
cacheAuth0Profile
import {
generateToken,
verifyToken,
getGoogleAuthUrl,
exchangeCodeForTokens,
getGoogleUserInfo,
User
} from '../config/simpleAuth';
import databaseService from '../services/databaseService';
type AuthedRequest = Request & {
auth?: {
token: string;
claims: VerifiedAccessToken;
profile?: Auth0UserProfile | null;
};
user?: any;
};
const router = express.Router();
function mapUserForResponse(user: any) {
return {
id: user.id,
email: user.email,
name: user.name,
picture: user.profile_picture_url,
role: user.role,
approval_status: user.approval_status,
created_at: user.created_at,
last_login: user.last_login,
provider: 'auth0'
};
}
async function syncUserWithDatabase(claims: VerifiedAccessToken, token: string): Promise<{ user: any; profile: Auth0UserProfile | null }> {
const auth0Id = claims.sub;
const initialAdminEmails = (process.env.INITIAL_ADMIN_EMAILS || '')
.split(',')
.map(email => email.trim().toLowerCase())
.filter(Boolean);
let profile: Auth0UserProfile | null = null;
let user = await databaseService.getUserById(auth0Id);
if (user) {
const updated = await databaseService.updateUserLastSignIn(user.email);
user = updated || user;
const isSeedAdmin = initialAdminEmails.includes((user.email || '').toLowerCase());
if (isSeedAdmin && user.role !== 'administrator') {
user = await databaseService.updateUserRole(user.email, 'administrator');
}
if (isSeedAdmin && user.approval_status !== 'approved') {
user = await databaseService.updateUserApprovalStatus(user.email, 'approved');
}
return { user, profile };
}
const cacheKey = auth0Id;
profile = getCachedProfile(cacheKey) || null;
if (!profile) {
profile = await fetchAuth0UserProfile(token, cacheKey, claims.exp);
cacheAuth0Profile(cacheKey, profile, claims.exp);
}
if (!profile.email) {
throw new Error('Auth0 profile did not include an email address');
}
const existingByEmail = await databaseService.getUserByEmail(profile.email);
if (existingByEmail && existingByEmail.id !== auth0Id) {
await databaseService.migrateUserId(existingByEmail.id, auth0Id);
user = await databaseService.getUserById(auth0Id);
} else if (existingByEmail) {
user = existingByEmail;
}
const displayName = profile.name || profile.nickname || profile.email;
const picture = typeof profile.picture === 'string' ? profile.picture : undefined;
const isSeedAdmin = initialAdminEmails.includes(profile.email.toLowerCase());
if (!user) {
const approvedUserCount = await databaseService.getApprovedUserCount();
const role = isSeedAdmin
? 'administrator'
: approvedUserCount === 0
? 'administrator'
: 'coordinator';
user = await databaseService.createUser({
id: auth0Id,
google_id: auth0Id,
email: profile.email,
name: displayName,
profile_picture_url: picture,
role
});
if (role === 'administrator') {
user = await databaseService.updateUserApprovalStatus(profile.email, 'approved');
}
} else {
const updated = await databaseService.updateUserLastSignIn(user.email);
user = updated || user;
if (isSeedAdmin && user.role !== 'administrator') {
user = await databaseService.updateUserRole(user.email, 'administrator');
}
if (isSeedAdmin && user.approval_status !== 'approved') {
user = await databaseService.updateUserApprovalStatus(user.email, 'approved');
}
}
return { user, profile };
}
export async function requireAuth(req: AuthedRequest, res: Response, next: NextFunction) {
// Middleware to check authentication
export function requireAuth(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.substring(7);
const user = verifyToken(token);
try {
const claims = await verifyAccessToken(token);
const { user, profile } = await syncUserWithDatabase(claims, token);
req.auth = { token, claims, profile };
req.user = user;
if (user.approval_status !== 'approved') {
return res.status(403).json({
error: 'pending_approval',
message: 'Your account is pending administrator approval.',
user: mapUserForResponse(user)
});
}
return next();
} catch (error: any) {
console.error('Auth0 token verification failed:', error);
return res.status(401).json({ error: 'Invalid or expired token' });
if (!user) {
return res.status(401).json({ error: 'Invalid token' });
}
(req as any).user = user;
next();
}
// Middleware to check role
export function requireRole(roles: string[]) {
return (req: AuthedRequest, res: Response, next: NextFunction) => {
const user = req.user;
return (req: Request, res: Response, next: NextFunction) => {
const user = (req as any).user;
if (!user || !roles.includes(user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
router.get('/setup', async (_req: Request, res: Response) => {
// Get current user
router.get('/me', requireAuth, (req: Request, res: Response) => {
res.json((req as any).user);
});
// Setup status endpoint (required by frontend)
router.get('/setup', async (req: Request, res: Response) => {
const clientId = process.env.GOOGLE_CLIENT_ID;
const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
try {
const userCount = await databaseService.getUserCount();
res.json({
setupCompleted: isAuth0Configured(),
setupCompleted: !!(clientId && clientSecret && clientId !== 'your-google-client-id-from-console'),
firstAdminCreated: userCount > 0,
oauthConfigured: isAuth0Configured(),
authProvider: 'auth0'
oauthConfigured: !!(clientId && clientSecret)
});
} catch (error) {
console.error('Error checking setup status:', error);
@@ -178,35 +66,206 @@ router.get('/setup', async (_req: Request, res: Response) => {
}
});
router.get('/me', requireAuth, async (req: AuthedRequest, res: Response) => {
res.json({
user: mapUserForResponse(req.user),
auth0: {
sub: req.auth?.claims.sub,
scope: req.auth?.claims.scope
}
});
// Start Google OAuth flow
router.get('/google', (req: Request, res: Response) => {
try {
const authUrl = getGoogleAuthUrl();
res.redirect(authUrl);
} catch (error) {
console.error('Error starting Google OAuth:', error);
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
res.redirect(`${frontendUrl}?error=oauth_not_configured`);
}
});
router.post('/logout', (_req: Request, res: Response) => {
// Handle Google OAuth callback (this is where Google redirects back to)
router.get('/google/callback', async (req: Request, res: Response) => {
const { code, error } = req.query;
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
if (error) {
console.error('OAuth error:', error);
return res.redirect(`${frontendUrl}?error=${error}`);
}
if (!code) {
return res.redirect(`${frontendUrl}?error=no_code`);
}
try {
// Exchange code for tokens
const tokens = await exchangeCodeForTokens(code as string);
// Get user info
const googleUser = await getGoogleUserInfo(tokens.access_token);
// Check if user exists or create new user
let user = await databaseService.getUserByEmail(googleUser.email);
if (!user) {
// Determine role - first user becomes admin, others need approval
const approvedUserCount = await databaseService.getApprovedUserCount();
const role = approvedUserCount === 0 ? 'administrator' : 'coordinator';
user = await databaseService.createUser({
id: googleUser.id,
google_id: googleUser.id,
email: googleUser.email,
name: googleUser.name,
profile_picture_url: googleUser.picture,
role
});
// Auto-approve first admin, others need approval
if (approvedUserCount === 0) {
await databaseService.updateUserApprovalStatus(googleUser.email, 'approved');
user.approval_status = 'approved';
}
} else {
// Update last sign in
await databaseService.updateUserLastSignIn(googleUser.email);
console.log(`✅ User logged in: ${user.name} (${user.email})`);
}
// Check if user is approved
if (user.approval_status !== 'approved') {
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
return res.redirect(`${frontendUrl}?error=pending_approval&message=Your account is pending administrator approval`);
}
// Generate JWT token
const token = generateToken(user);
// Redirect to frontend with token
res.redirect(`${frontendUrl}/auth/callback?token=${token}`);
} catch (error) {
console.error('Error in OAuth callback:', error);
res.redirect(`${frontendUrl}?error=oauth_failed`);
}
});
// Exchange OAuth code for JWT token (alternative endpoint for frontend)
router.post('/google/exchange', async (req: Request, res: Response) => {
const { code } = req.body;
if (!code) {
return res.status(400).json({ error: 'Authorization code is required' });
}
try {
// Exchange code for tokens
const tokens = await exchangeCodeForTokens(code);
// Get user info
const googleUser = await getGoogleUserInfo(tokens.access_token);
// Check if user exists or create new user
let user = await databaseService.getUserByEmail(googleUser.email);
if (!user) {
// Determine role - first user becomes admin
const userCount = await databaseService.getUserCount();
const role = userCount === 0 ? 'administrator' : 'coordinator';
user = await databaseService.createUser({
id: googleUser.id,
google_id: googleUser.id,
email: googleUser.email,
name: googleUser.name,
profile_picture_url: googleUser.picture,
role
});
} else {
// Update last sign in
await databaseService.updateUserLastSignIn(googleUser.email);
console.log(`✅ User logged in: ${user.name} (${user.email})`);
}
// Generate JWT token
const token = generateToken(user);
// Return token to frontend
res.json({
token,
user: {
id: user.id,
email: user.email,
name: user.name,
picture: user.profile_picture_url,
role: user.role
}
});
} catch (error) {
console.error('Error in OAuth exchange:', error);
res.status(500).json({ error: 'Failed to exchange authorization code' });
}
});
// Get OAuth URL for frontend to redirect to
router.get('/google/url', (req: Request, res: Response) => {
try {
const authUrl = getGoogleAuthUrl();
res.json({ url: authUrl });
} catch (error) {
console.error('Error getting Google OAuth URL:', error);
res.status(500).json({ error: 'OAuth not configured' });
}
});
// Logout
router.post('/logout', (req: Request, res: Response) => {
// With JWT, logout is handled client-side by removing the token
res.json({ message: 'Logged out successfully' });
});
router.get('/status', requireAuth, (req: AuthedRequest, res: Response) => {
res.json({
authenticated: true,
user: mapUserForResponse(req.user)
// Get auth status
router.get('/status', (req: Request, res: Response) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.json({ authenticated: false });
}
const token = authHeader.substring(7);
const user = verifyToken(token);
if (!user) {
return res.json({ authenticated: false });
}
res.json({
authenticated: true,
user: {
id: user.id,
email: user.email,
name: user.name,
picture: user.profile_picture_url,
role: user.role
}
});
});
// USER MANAGEMENT ENDPOINTS
// List all users (admin only)
router.get('/users', requireAuth, requireRole(['administrator']), async (_req: Request, res: Response) => {
router.get('/users', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => {
try {
const users = await databaseService.getAllUsers();
res.json(users.map(mapUserForResponse));
const userList = users.map(user => ({
id: user.id,
email: user.email,
name: user.name,
picture: user.profile_picture_url,
role: user.role,
created_at: user.created_at,
last_login: user.last_login,
provider: 'google'
}));
res.json(userList);
} catch (error) {
console.error('Error fetching users:', error);
res.status(500).json({ error: 'Failed to fetch users' });
@@ -230,7 +289,12 @@ router.patch('/users/:email/role', requireAuth, requireRole(['administrator']),
res.json({
success: true,
user: mapUserForResponse(user)
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role
}
});
} catch (error) {
console.error('Error updating user role:', error);
@@ -239,9 +303,9 @@ router.patch('/users/:email/role', requireAuth, requireRole(['administrator']),
});
// Delete user (admin only)
router.delete('/users/:email', requireAuth, requireRole(['administrator']), async (req: AuthedRequest, res: Response) => {
router.delete('/users/:email', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => {
const { email } = req.params;
const currentUser = req.user;
const currentUser = (req as any).user;
// Prevent admin from deleting themselves
if (email === currentUser.email) {
@@ -272,7 +336,17 @@ router.get('/users/:email', requireAuth, requireRole(['administrator']), async (
return res.status(404).json({ error: 'User not found' });
}
res.json(mapUserForResponse(user));
res.json({
id: user.id,
email: user.email,
name: user.name,
picture: user.profile_picture_url,
role: user.role,
created_at: user.created_at,
last_login: user.last_login,
provider: 'google',
approval_status: user.approval_status
});
} catch (error) {
console.error('Error fetching user:', error);
res.status(500).json({ error: 'Failed to fetch user' });
@@ -286,7 +360,16 @@ router.get('/users/pending/list', requireAuth, requireRole(['administrator']), a
try {
const pendingUsers = await databaseService.getPendingUsers();
const userList = pendingUsers.map(mapUserForResponse);
const userList = pendingUsers.map(user => ({
id: user.id,
email: user.email,
name: user.name,
picture: user.profile_picture_url,
role: user.role,
created_at: user.created_at,
provider: 'google',
approval_status: user.approval_status
}));
res.json(userList);
} catch (error) {
@@ -313,7 +396,13 @@ router.patch('/users/:email/approval', requireAuth, requireRole(['administrator'
res.json({
success: true,
message: `User ${status} successfully`,
user: mapUserForResponse(user)
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
approval_status: user.approval_status
}
});
} catch (error) {
console.error('Error updating user approval:', error);

View File

@@ -6,10 +6,9 @@ class DatabaseService {
private redis: RedisClientType;
constructor() {
const useSSL = process.env.DATABASE_SSL === 'true';
this.pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: useSSL ? { rejectUnauthorized: false } : false
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
});
// Initialize Redis connection
@@ -98,38 +97,6 @@ class DatabaseService {
ADD COLUMN IF NOT EXISTS approval_status VARCHAR(20) DEFAULT 'pending' CHECK (approval_status IN ('pending', 'approved', 'denied'))
`);
// Admin settings storage table
await this.query(`
CREATE TABLE IF NOT EXISTS admin_settings (
id SERIAL PRIMARY KEY,
setting_key VARCHAR(255) UNIQUE NOT NULL,
setting_value TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await this.query(`
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
`);
await this.query(`
DROP TRIGGER IF EXISTS update_admin_settings_updated_at ON admin_settings
`);
await this.query(`
CREATE TRIGGER update_admin_settings_updated_at
BEFORE UPDATE ON admin_settings
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column()
`);
// Create indexes
await this.query(`
CREATE INDEX IF NOT EXISTS idx_users_google_id ON users(google_id)
@@ -191,31 +158,6 @@ class DatabaseService {
return result.rows[0] || null;
}
async migrateUserId(oldId: string, newId: string): Promise<void> {
const client = await this.pool.connect();
try {
await client.query('BEGIN');
await client.query(
'UPDATE drivers SET user_id = $2 WHERE user_id = $1',
[oldId, newId]
);
await client.query(
'UPDATE users SET id = $2, google_id = $2 WHERE id = $1',
[oldId, newId]
);
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
async getAllUsers(): Promise<any[]> {
const query = 'SELECT * FROM users ORDER BY created_at ASC';
const result = await this.query(query);
@@ -313,29 +255,52 @@ class DatabaseService {
}
}
// VIP schema (flights, drivers, schedules)
// VIP table initialization using the correct schema
async initializeVipTables(): Promise<void> {
try {
// Check if VIPs table exists and has the correct schema
const tableExists = await this.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'vips'
)
`);
if (tableExists.rows[0].exists) {
// Check if the table has the correct columns
const columnCheck = await this.query(`
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'vips'
AND column_name = 'organization'
`);
if (columnCheck.rows.length === 0) {
console.log('🔄 Migrating VIPs table to new schema...');
// Drop the old table and recreate with correct schema
await this.query(`DROP TABLE IF EXISTS vips CASCADE`);
}
}
// Create VIPs table with correct schema matching enhancedDataService expectations
await this.query(`
CREATE TABLE IF NOT EXISTS vips (
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
organization VARCHAR(255) NOT NULL,
department VARCHAR(255) DEFAULT 'Office of Development',
transport_mode VARCHAR(50) NOT NULL CHECK (transport_mode IN ('flight', 'self-driving')),
expected_arrival TIMESTAMP,
needs_airport_pickup BOOLEAN DEFAULT false,
needs_venue_transport BOOLEAN DEFAULT true,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await this.query(`
ALTER TABLE vips
ADD COLUMN IF NOT EXISTS organization VARCHAR(255),
ADD COLUMN IF NOT EXISTS department VARCHAR(255) DEFAULT 'Office of Development',
ADD COLUMN IF NOT EXISTS transport_mode VARCHAR(50),
ADD COLUMN IF NOT EXISTS expected_arrival TIMESTAMP,
ADD COLUMN IF NOT EXISTS needs_airport_pickup BOOLEAN DEFAULT false,
ADD COLUMN IF NOT EXISTS needs_venue_transport BOOLEAN DEFAULT true
`);
// Create flights table (for VIPs with flight transport)
await this.query(`
CREATE TABLE IF NOT EXISTS flights (
id SERIAL PRIMARY KEY,
@@ -355,22 +320,69 @@ class DatabaseService {
)
`);
// Check and migrate drivers table
const driversTableExists = await this.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'drivers'
)
`);
if (driversTableExists.rows[0].exists) {
// Check if drivers table has the correct schema (phone column and department column)
const driversSchemaCheck = await this.query(`
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'drivers'
AND column_name IN ('phone', 'department')
`);
if (driversSchemaCheck.rows.length < 2) {
console.log('🔄 Migrating drivers table to new schema...');
await this.query(`DROP TABLE IF EXISTS drivers CASCADE`);
}
}
// Create drivers table with correct schema
await this.query(`
CREATE TABLE IF NOT EXISTS drivers (
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
phone VARCHAR(50) NOT NULL,
department VARCHAR(255) DEFAULT 'Office of Development',
user_id VARCHAR(255) REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await this.query(`
ALTER TABLE drivers
ADD COLUMN IF NOT EXISTS phone VARCHAR(50),
ADD COLUMN IF NOT EXISTS department VARCHAR(255) DEFAULT 'Office of Development',
ADD COLUMN IF NOT EXISTS user_id VARCHAR(255) REFERENCES users(id) ON DELETE SET NULL
// Check and migrate schedule_events table
const scheduleTableExists = await this.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'schedule_events'
)
`);
if (!scheduleTableExists.rows[0].exists) {
// Check for old 'schedules' table and drop it
const oldScheduleExists = await this.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'schedules'
)
`);
if (oldScheduleExists.rows[0].exists) {
console.log('🔄 Migrating schedules table to schedule_events...');
await this.query(`DROP TABLE IF EXISTS schedules CASCADE`);
}
}
// Create schedule_events table
await this.query(`
CREATE TABLE IF NOT EXISTS schedule_events (
id VARCHAR(255) PRIMARY KEY,
@@ -388,42 +400,66 @@ class DatabaseService {
)
`);
// Create system_setup table for tracking initial setup
await this.query(`
DROP TABLE IF EXISTS schedules
CREATE TABLE IF NOT EXISTS system_setup (
id SERIAL PRIMARY KEY,
setup_completed BOOLEAN DEFAULT false,
first_admin_created BOOLEAN DEFAULT false,
setup_date TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Create admin_settings table
await this.query(`
ALTER TABLE schedule_events
ADD COLUMN IF NOT EXISTS description TEXT,
ADD COLUMN IF NOT EXISTS assigned_driver_id VARCHAR(255) REFERENCES drivers(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS status VARCHAR(50) DEFAULT 'scheduled',
ADD COLUMN IF NOT EXISTS event_type VARCHAR(50),
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
CREATE TABLE IF NOT EXISTS admin_settings (
id SERIAL PRIMARY KEY,
setting_key VARCHAR(255) UNIQUE NOT NULL,
setting_value TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Create indexes for better performance
await this.query(`CREATE INDEX IF NOT EXISTS idx_vips_transport_mode ON vips(transport_mode)`);
await this.query(`CREATE INDEX IF NOT EXISTS idx_flights_vip_id ON flights(vip_id)`);
await this.query(`CREATE INDEX IF NOT EXISTS idx_flights_date ON flights(flight_date)`);
await this.query(`CREATE INDEX IF NOT EXISTS idx_schedule_events_vip_id ON schedule_events(vip_id)`);
await this.query(`CREATE INDEX IF NOT EXISTS idx_schedule_events_driver_id ON schedule_events(assigned_driver_id)`);
await this.query(`CREATE INDEX IF NOT EXISTS idx_schedule_events_start_time ON schedule_events(start_time)`);
await this.query(`CREATE INDEX IF NOT EXISTS idx_schedule_events_status ON schedule_events(status)`);
await this.query(`CREATE INDEX IF NOT EXISTS idx_drivers_user_id ON drivers(user_id)`);
// Create updated_at trigger function
await this.query(`
CREATE INDEX IF NOT EXISTS idx_vips_transport_mode ON vips(transport_mode)
`);
await this.query(`
CREATE INDEX IF NOT EXISTS idx_flights_vip_id ON flights(vip_id)
`);
await this.query(`
CREATE INDEX IF NOT EXISTS idx_flights_date ON flights(flight_date)
`);
await this.query(`
CREATE INDEX IF NOT EXISTS idx_schedule_events_vip_id ON schedule_events(vip_id)
`);
await this.query(`
CREATE INDEX IF NOT EXISTS idx_schedule_events_driver_id ON schedule_events(assigned_driver_id)
`);
await this.query(`
CREATE INDEX IF NOT EXISTS idx_schedule_events_start_time ON schedule_events(start_time)
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql'
`);
console.log('✅ VIP and schedule tables initialized successfully');
// Create triggers for updated_at (drop if exists first)
await this.query(`DROP TRIGGER IF EXISTS update_vips_updated_at ON vips`);
await this.query(`DROP TRIGGER IF EXISTS update_flights_updated_at ON flights`);
await this.query(`DROP TRIGGER IF EXISTS update_drivers_updated_at ON drivers`);
await this.query(`DROP TRIGGER IF EXISTS update_schedule_events_updated_at ON schedule_events`);
await this.query(`DROP TRIGGER IF EXISTS update_admin_settings_updated_at ON admin_settings`);
await this.query(`CREATE TRIGGER update_vips_updated_at BEFORE UPDATE ON vips FOR EACH ROW EXECUTE FUNCTION update_updated_at_column()`);
await this.query(`CREATE TRIGGER update_flights_updated_at BEFORE UPDATE ON flights FOR EACH ROW EXECUTE FUNCTION update_updated_at_column()`);
await this.query(`CREATE TRIGGER update_drivers_updated_at BEFORE UPDATE ON drivers FOR EACH ROW EXECUTE FUNCTION update_updated_at_column()`);
await this.query(`CREATE TRIGGER update_schedule_events_updated_at BEFORE UPDATE ON schedule_events FOR EACH ROW EXECUTE FUNCTION update_updated_at_column()`);
await this.query(`CREATE TRIGGER update_admin_settings_updated_at BEFORE UPDATE ON admin_settings FOR EACH ROW EXECUTE FUNCTION update_updated_at_column()`);
console.log('✅ VIP Coordinator database schema initialized successfully');
} catch (error) {
console.error('❌ Failed to initialize VIP tables:', error);
throw error;
}
}

View File

@@ -589,13 +589,11 @@ class EnhancedDataService {
// Default settings structure
const defaultSettings = {
apiKeys: {
aviationStackKey: process.env.AVIATIONSTACK_API_KEY || '',
aviationStackKey: '',
googleMapsKey: '',
twilioKey: '',
auth0Domain: process.env.AUTH0_DOMAIN || '',
auth0ClientId: process.env.AUTH0_CLIENT_ID || '',
auth0ClientSecret: process.env.AUTH0_CLIENT_SECRET || '',
auth0Audience: process.env.AUTH0_AUDIENCE || ''
googleClientId: '',
googleClientSecret: ''
},
systemSettings: {
defaultPickupLocation: '',

View File

@@ -118,7 +118,7 @@ class FlightService {
console.log('Note: Free tier returns recent flights only, not future scheduled flights');
const response = await fetch(url);
const data: any = await response.json();
const data = await response.json();
console.log('AviationStack response status:', response.status);
@@ -128,12 +128,12 @@ class FlightService {
}
// Check for API errors in response
if (data?.error) {
if (data.error) {
console.error('AviationStack API error:', data.error);
return null;
}
if (Array.isArray(data?.data) && data.data.length > 0) {
if (data.data && data.data.length > 0) {
// This is a valid flight number that exists!
console.log(`✅ Valid flight number: ${formattedFlightNumber} exists in the system`);