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:
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
203
backend/package-lock.json
generated
203
backend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user