From 56571c445e12b5ab183b7354018007ae2a029e0b Mon Sep 17 00:00:00 2001 From: Chandini Date: Thu, 4 Sep 2025 15:29:50 +0530 Subject: [PATCH] backend changes --- docker-compose.yml | 49 +- services/api-gateway/src/server.js | 87 + services/git-integration/Dockerfile | 30 + services/git-integration/package-lock.json | 1716 +++++++++++++++++ services/git-integration/package.json | 25 + services/git-integration/src/app.js | 103 + .../git-integration/src/config/database.js | 54 + .../src/migrations/001_github_integration.sql | 61 + .../002_repository_file_storage.sql | 109 ++ .../003_add_user_id_to_template_refs.sql | 21 + .../git-integration/src/migrations/migrate.js | 81 + .../src/routes/github-integration.routes.js | 561 ++++++ .../src/routes/github-oauth.js | 169 ++ .../src/services/file-storage.service.js | 373 ++++ .../services/github-integration.service.js | 402 ++++ .../src/services/github-oauth.js | 153 ++ 16 files changed, 3993 insertions(+), 1 deletion(-) create mode 100644 services/git-integration/Dockerfile create mode 100644 services/git-integration/package-lock.json create mode 100644 services/git-integration/package.json create mode 100644 services/git-integration/src/app.js create mode 100644 services/git-integration/src/config/database.js create mode 100644 services/git-integration/src/migrations/001_github_integration.sql create mode 100644 services/git-integration/src/migrations/002_repository_file_storage.sql create mode 100644 services/git-integration/src/migrations/003_add_user_id_to_template_refs.sql create mode 100644 services/git-integration/src/migrations/migrate.js create mode 100644 services/git-integration/src/routes/github-integration.routes.js create mode 100644 services/git-integration/src/routes/github-oauth.js create mode 100644 services/git-integration/src/services/file-storage.service.js create mode 100644 services/git-integration/src/services/github-integration.service.js create mode 100644 services/git-integration/src/services/github-oauth.js diff --git a/docker-compose.yml b/docker-compose.yml index 4fe744f..94b9924 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -594,6 +594,7 @@ services: # Service URLs - USER_AUTH_URL=http://user-auth:8011 - TEMPLATE_MANAGER_URL=http://template-manager:8009 + - GIT_INTEGRATION_URL=http://git-integration:8012 - REQUIREMENT_PROCESSOR_URL=http://requirement-processor:8001 - TECH_STACK_SELECTOR_URL=http://tech-stack-selector:8002 - ARCHITECTURE_DESIGNER_URL=http://architecture-designer:8003 @@ -905,7 +906,51 @@ services: interval: 30s timeout: 10s retries: 3 - start_period: 40s + start_period: 40s + restart: unless-stopped + + git-integration: + build: ./services/git-integration + container_name: pipeline_git_integration + ports: + - "8012:8012" + env_file: + - ./services/git-integration/.env + environment: + - PORT=8012 + - HOST=0.0.0.0 + - FRONTEND_URL=http://localhost:3000 + - POSTGRES_HOST=postgres + - POSTGRES_PORT=5432 + - POSTGRES_DB=dev_pipeline + - POSTGRES_USER=pipeline_admin + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - REDIS_HOST=redis + - REDIS_PORT=6379 + - REDIS_PASSWORD=${REDIS_PASSWORD} + - NODE_ENV=development + - GITHUB_REDIRECT_URI=http://localhost:8012/api/github/auth/github/callback + - ATTACHED_REPOS_DIR=/tmp/attached-repos + - SESSION_SECRET=git-integration-secret-key-2024 + volumes: + - git_repos_data:/tmp/attached-repos + networks: + - pipeline_network + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + migrations: + condition: service_completed_successfully + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8012/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + restart: unless-stopped + self-improving-generator: build: ./self-improving-generator container_name: pipeline_self_improving_generator @@ -1041,6 +1086,8 @@ volumes: driver: local rabbitmq_logs: driver: local + git_repos_data: + driver: local n8n_data: driver: local neo4j_data: diff --git a/services/api-gateway/src/server.js b/services/api-gateway/src/server.js index e9d0918..5f5ca9e 100644 --- a/services/api-gateway/src/server.js +++ b/services/api-gateway/src/server.js @@ -42,6 +42,7 @@ global.io = io; const serviceTargets = { USER_AUTH_URL: process.env.USER_AUTH_URL || 'http://localhost:8011', TEMPLATE_MANAGER_URL: process.env.TEMPLATE_MANAGER_URL || 'http://localhost:8009', + GIT_INTEGRATION_URL: process.env.GIT_INTEGRATION_URL || 'http://localhost:8012', REQUIREMENT_PROCESSOR_URL: process.env.REQUIREMENT_PROCESSOR_URL || 'http://localhost:8001', TECH_STACK_SELECTOR_URL: process.env.TECH_STACK_SELECTOR_URL || 'http://localhost:8002', ARCHITECTURE_DESIGNER_URL: process.env.ARCHITECTURE_DESIGNER_URL || 'http://localhost:8003', @@ -93,6 +94,7 @@ app.use('/api/gateway', express.json({ limit: '10mb' })); app.use('/api/auth', express.json({ limit: '10mb' })); app.use('/api/templates', express.json({ limit: '10mb' })); app.use('/api/features', express.json({ limit: '10mb' })); +app.use('/api/github', express.json({ limit: '10mb' })); app.use('/health', express.json({ limit: '10mb' })); // Trust proxy for accurate IP addresses @@ -136,6 +138,7 @@ app.get('/health', (req, res) => { services: { user_auth: process.env.USER_AUTH_URL ? 'configured' : 'not configured', template_manager: process.env.TEMPLATE_MANAGER_URL ? 'configured' : 'not configured', + git_integration: process.env.GIT_INTEGRATION_URL ? 'configured' : 'not configured', requirement_processor: process.env.REQUIREMENT_PROCESSOR_URL ? 'configured' : 'not configured', tech_stack_selector: process.env.TECH_STACK_SELECTOR_URL ? 'configured' : 'not configured', architecture_designer: process.env.ARCHITECTURE_DESIGNER_URL ? 'configured' : 'not configured', @@ -416,6 +419,88 @@ app.use('/api/features', } ); +// Git Integration Service - Direct HTTP forwarding +console.log('🔧 Registering /api/github proxy route...'); +app.use('/api/github', + createServiceLimiter(200), + // Conditionally require auth: allow public GETs, require token for write ops + (req, res, next) => { + // Allow unauthenticated access for read-only requests and specific public endpoints + if (req.method === 'GET') { + return next(); + } + // Allowlist certain POST endpoints that must be public to initiate flows + const url = req.originalUrl || ''; + const isPublicGithubEndpoint = ( + url.startsWith('/api/github/test-access') || + url.startsWith('/api/github/auth/github') || + url.startsWith('/api/github/auth/github/callback') || + url.startsWith('/api/github/auth/github/status') + ); + if (isPublicGithubEndpoint) { + return next(); + } + return authMiddleware.verifyToken(req, res, () => authMiddleware.forwardUserContext(req, res, next)); + }, + (req, res, next) => { + const gitServiceUrl = serviceTargets.GIT_INTEGRATION_URL; + console.log(`🔥 [GIT PROXY] ${req.method} ${req.originalUrl} → ${gitServiceUrl}${req.originalUrl}`); + + // Set response timeout to prevent hanging + res.setTimeout(15000, () => { + console.error('❌ [GIT PROXY] Response timeout'); + if (!res.headersSent) { + res.status(504).json({ error: 'Gateway timeout', service: 'git-integration' }); + } + }); + + const options = { + method: req.method, + url: `${gitServiceUrl}${req.originalUrl}`, + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'API-Gateway/1.0', + 'Connection': 'keep-alive', + // Forward user context from auth middleware + 'X-User-ID': req.user?.id || req.user?.userId, + 'X-User-Role': req.user?.role, + 'Authorization': req.headers.authorization + }, + timeout: 8000, + validateStatus: () => true, + maxRedirects: 0 + }; + + // Always include request body for POST/PUT/PATCH requests + if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') { + options.data = req.body || {}; + console.log(`📦 [GIT PROXY] Request body:`, JSON.stringify(req.body)); + } + + axios(options) + .then(response => { + console.log(`✅ [GIT PROXY] Response: ${response.status} for ${req.method} ${req.originalUrl}`); + if (!res.headersSent) { + res.status(response.status).json(response.data); + } + }) + .catch(error => { + console.error(`❌ [GIT PROXY ERROR]:`, error.message); + if (!res.headersSent) { + if (error.response) { + res.status(error.response.status).json(error.response.data); + } else { + res.status(502).json({ + error: 'Git integration service unavailable', + message: error.code || error.message, + service: 'git-integration' + }); + } + } + }); + } +); + // Gateway management endpoints app.get('/api/gateway/info', authMiddleware.verifyToken, (req, res) => { res.json({ @@ -464,6 +549,7 @@ app.get('/', (req, res) => { services: { auth: '/api/auth', templates: '/api/templates', + github: '/api/github', requirements: '/api/requirements', tech_stack: '/api/tech-stack', architecture: '/api/architecture', @@ -488,6 +574,7 @@ app.use('*', (req, res) => { available_services: { auth: '/api/auth', templates: '/api/templates', + github: '/api/github', requirements: '/api/requirements', tech_stack: '/api/tech-stack', architecture: '/api/architecture', diff --git a/services/git-integration/Dockerfile b/services/git-integration/Dockerfile new file mode 100644 index 0000000..f81e732 --- /dev/null +++ b/services/git-integration/Dockerfile @@ -0,0 +1,30 @@ +FROM node:18-alpine + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm install + +# Copy source code +COPY . . + +# Create non-root user +RUN addgroup -g 1001 -S nodejs +RUN adduser -S git-integration -u 1001 + +# Change ownership +RUN chown -R git-integration:nodejs /app +USER git-integration + +# Expose port +EXPOSE 8012 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8012/health || exit 1 + +# Start the application +CMD ["npm", "start"] diff --git a/services/git-integration/package-lock.json b/services/git-integration/package-lock.json new file mode 100644 index 0000000..6dcd503 --- /dev/null +++ b/services/git-integration/package-lock.json @@ -0,0 +1,1716 @@ +{ + "name": "git-integration", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "git-integration", + "version": "1.0.0", + "dependencies": { + "@octokit/rest": "^20.0.2", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-session": "^1.18.2", + "helmet": "^7.1.0", + "morgan": "^1.10.0", + "pg": "^8.11.3", + "uuid": "^9.0.1" + }, + "devDependencies": { + "nodemon": "^3.0.2" + } + }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", + "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "11.4.4-cjs.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.4-cjs.2.tgz", + "integrity": "sha512-2dK6z8fhs8lla5PaOTgqfCGBxgAv/le+EhPs27KklPhm1bKObpu6lXzwfUEQ16ajXzqNrKMujsFyo9K2eaoISw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.7.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-4.0.1.tgz", + "integrity": "sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "13.3.2-cjs.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.3.2-cjs.1.tgz", + "integrity": "sha512-VUjIjOOvF2oELQmiFpWA1aOPdawpyaCUqcEBc/UOUnj3Xp6DJGrJ1+bjUIIDzdHjnFNO6q57ODMfdEZnoBkCwQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.8.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5" + } + }, + "node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest": { + "version": "20.1.2", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.1.2.tgz", + "integrity": "sha512-GmYiltypkHHtihFwPRxlaorG5R9VAHuk/vbszVoRTGXnAsY60wYLkh/E2XiFmdZmqrisw+9FaazS1i5SbdWYgA==", + "license": "MIT", + "dependencies": { + "@octokit/core": "^5.0.2", + "@octokit/plugin-paginate-rest": "11.4.4-cjs.2", + "@octokit/plugin-request-log": "^4.0.0", + "@octokit/plugin-rest-endpoint-methods": "13.3.2-cjs.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "license": "Apache-2.0" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "license": "ISC" + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/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/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "license": "ISC" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/services/git-integration/package.json b/services/git-integration/package.json new file mode 100644 index 0000000..4ee829d --- /dev/null +++ b/services/git-integration/package.json @@ -0,0 +1,25 @@ +{ + "name": "git-integration", + "version": "1.0.0", + "description": "Git Integration Service with GitHub Integration", + "main": "src/app.js", + "scripts": { + "start": "node src/app.js", + "dev": "nodemon src/app.js", + "migrate": "node src/migrations/migrate.js" + }, + "dependencies": { + "@octokit/rest": "^20.0.2", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-session": "^1.18.2", + "helmet": "^7.1.0", + "morgan": "^1.10.0", + "pg": "^8.11.3", + "uuid": "^9.0.1" + }, + "devDependencies": { + "nodemon": "^3.0.2" + } +} diff --git a/services/git-integration/src/app.js b/services/git-integration/src/app.js new file mode 100644 index 0000000..fdadbde --- /dev/null +++ b/services/git-integration/src/app.js @@ -0,0 +1,103 @@ +require('dotenv').config(); +const express = require('express'); +const cors = require('cors'); +const helmet = require('helmet'); +const session = require('express-session'); +const morgan = require('morgan'); + +// Import database +const database = require('./config/database'); + +// Import routes +const githubRoutes = require('./routes/github-integration.routes'); +const githubOAuthRoutes = require('./routes/github-oauth'); + +const app = express(); +const PORT = process.env.PORT || 8012; + +// Middleware +app.use(helmet()); +app.use(cors()); +app.use(morgan('combined')); +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true })); + +// Session middleware +app.use(session({ + secret: process.env.SESSION_SECRET || 'git-integration-secret-key-2024', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true if using HTTPS + httpOnly: true, + maxAge: 24 * 60 * 60 * 1000 // 24 hours + } +})); + +// Routes +app.use('/api/github', githubRoutes); +app.use('/api/github', githubOAuthRoutes); + +// Health check endpoint +app.get('/health', (req, res) => { + res.status(200).json({ + status: 'healthy', + service: 'git-integration', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + version: '1.0.0' + }); +}); + +// Root endpoint +app.get('/', (req, res) => { + res.json({ + message: 'Git Integration Service', + version: '1.0.0', + endpoints: { + health: '/health', + github: '/api/github', + oauth: '/api/github/auth' + } + }); +}); + +// Error handling middleware +app.use((err, req, res, next) => { + console.error('Error:', err); + res.status(500).json({ + success: false, + message: 'Internal server error', + error: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong' + }); +}); + +// 404 handler +app.use('*', (req, res) => { + res.status(404).json({ + success: false, + message: 'Endpoint not found' + }); +}); + +// Graceful shutdown +process.on('SIGTERM', async () => { + console.log('SIGTERM received, shutting down gracefully'); + await database.close(); + process.exit(0); +}); + +process.on('SIGINT', async () => { + console.log('SIGINT received, shutting down gracefully'); + await database.close(); + process.exit(0); +}); + +// Start server +app.listen(PORT, '0.0.0.0', () => { + console.log(`🚀 Git Integration Service running on port ${PORT}`); + console.log(`📊 Health check: http://localhost:${PORT}/health`); + console.log(`🔗 GitHub API: http://localhost:${PORT}/api/github`); +}); + +module.exports = app; diff --git a/services/git-integration/src/config/database.js b/services/git-integration/src/config/database.js new file mode 100644 index 0000000..e0a58c9 --- /dev/null +++ b/services/git-integration/src/config/database.js @@ -0,0 +1,54 @@ +const { Pool } = require('pg'); + +class Database { + constructor() { + this.pool = new Pool({ + host: process.env.POSTGRES_HOST || 'localhost', + port: process.env.POSTGRES_PORT || 5432, + database: process.env.POSTGRES_DB || 'dev_pipeline', + user: process.env.POSTGRES_USER || 'pipeline_admin', + password: process.env.POSTGRES_PASSWORD || 'secure_pipeline_2024', + max: 20, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 2000, + }); + + // Test connection on startup + this.testConnection(); + } + + async testConnection() { + try { + const client = await this.pool.connect(); + console.log('✅ Database connected successfully'); + client.release(); + } catch (err) { + console.error('❌ Database connection failed:', err.message); + process.exit(1); + } + } + + async query(text, params) { + const start = Date.now(); + try { + const res = await this.pool.query(text, params); + const duration = Date.now() - start; + console.log('📊 Query executed:', { text: text.substring(0, 50), duration, rows: res.rowCount }); + return res; + } catch (err) { + console.error('❌ Query error:', err.message); + throw err; + } + } + + async getClient() { + return await this.pool.connect(); + } + + async close() { + await this.pool.end(); + console.log('🔌 Database connection closed'); + } +} + +module.exports = new Database(); diff --git a/services/git-integration/src/migrations/001_github_integration.sql b/services/git-integration/src/migrations/001_github_integration.sql new file mode 100644 index 0000000..475743e --- /dev/null +++ b/services/git-integration/src/migrations/001_github_integration.sql @@ -0,0 +1,61 @@ +-- Migration 001: Add GitHub Integration Tables (PostgreSQL Only) +-- This migration adds support for GitHub repository integration + +-- Create table for GitHub repositories +CREATE TABLE IF NOT EXISTS github_repositories ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + template_id UUID REFERENCES templates(id) ON DELETE CASCADE, + repository_url VARCHAR(500) NOT NULL, + repository_name VARCHAR(200) NOT NULL, + owner_name VARCHAR(100) NOT NULL, + branch_name VARCHAR(100) DEFAULT 'main', + is_public BOOLEAN DEFAULT true, + requires_auth BOOLEAN DEFAULT false, + last_synced_at TIMESTAMP, + sync_status VARCHAR(50) DEFAULT 'pending', + metadata JSONB, + codebase_analysis JSONB, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +-- Create table for feature-codebase mappings +CREATE TABLE IF NOT EXISTS feature_codebase_mappings ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + template_id UUID REFERENCES templates(id) ON DELETE CASCADE, + feature_id UUID REFERENCES template_features(id) ON DELETE CASCADE, + repository_id UUID REFERENCES github_repositories(id) ON DELETE CASCADE, + file_paths TEXT[], + code_snippets JSONB, + implementation_notes TEXT, + created_at TIMESTAMP DEFAULT NOW() +); + +-- Create indexes for better performance +CREATE INDEX IF NOT EXISTS idx_github_repos_template_id ON github_repositories(template_id); +CREATE INDEX IF NOT EXISTS idx_github_repos_owner_name ON github_repositories(owner_name); +CREATE INDEX IF NOT EXISTS idx_feature_mappings_feature_id ON feature_codebase_mappings(feature_id); +CREATE INDEX IF NOT EXISTS idx_feature_mappings_repo_id ON feature_codebase_mappings(repository_id); + +-- Add trigger to update timestamp +CREATE TRIGGER update_github_repos_updated_at BEFORE UPDATE ON github_repositories + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +-- ============================================= +-- GitHub OAuth Tables +-- ============================================= + +-- Create table to store GitHub OAuth tokens +CREATE TABLE IF NOT EXISTS github_user_tokens ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + access_token TEXT NOT NULL, + github_username VARCHAR(100) NOT NULL, + github_user_id INTEGER NOT NULL, + scopes JSONB, + expires_at TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +-- Create index for faster lookups +CREATE INDEX IF NOT EXISTS idx_github_user_tokens_github_username ON github_user_tokens(github_username); diff --git a/services/git-integration/src/migrations/002_repository_file_storage.sql b/services/git-integration/src/migrations/002_repository_file_storage.sql new file mode 100644 index 0000000..f9cd1a7 --- /dev/null +++ b/services/git-integration/src/migrations/002_repository_file_storage.sql @@ -0,0 +1,109 @@ +-- Migration 002: Add Repository File Storage Tables +-- This migration adds comprehensive file system storage in PostgreSQL + +-- Create table for repository local storage tracking +CREATE TABLE IF NOT EXISTS repository_storage ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + repository_id UUID REFERENCES github_repositories(id) ON DELETE CASCADE, + local_path TEXT NOT NULL, + storage_status VARCHAR(50) DEFAULT 'pending', -- pending, downloading, completed, error + total_files_count INTEGER DEFAULT 0, + total_directories_count INTEGER DEFAULT 0, + total_size_bytes BIGINT DEFAULT 0, + download_started_at TIMESTAMP, + download_completed_at TIMESTAMP, + last_verified_at TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + UNIQUE(repository_id) +); + +-- Create table for directory structure +CREATE TABLE IF NOT EXISTS repository_directories ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + repository_id UUID REFERENCES github_repositories(id) ON DELETE CASCADE, + storage_id UUID REFERENCES repository_storage(id) ON DELETE CASCADE, + parent_directory_id UUID REFERENCES repository_directories(id) ON DELETE CASCADE, + directory_name VARCHAR(255) NOT NULL, + relative_path TEXT NOT NULL, -- path from repository root + absolute_path TEXT NOT NULL, -- full local filesystem path + level INTEGER DEFAULT 0, -- depth in hierarchy (0 = root) + files_count INTEGER DEFAULT 0, + subdirectories_count INTEGER DEFAULT 0, + total_size_bytes BIGINT DEFAULT 0, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +-- Create table for individual files +CREATE TABLE IF NOT EXISTS repository_files ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + repository_id UUID REFERENCES github_repositories(id) ON DELETE CASCADE, + storage_id UUID REFERENCES repository_storage(id) ON DELETE CASCADE, + directory_id UUID REFERENCES repository_directories(id) ON DELETE SET NULL, + filename VARCHAR(255) NOT NULL, + file_extension VARCHAR(50), + relative_path TEXT NOT NULL, -- path from repository root + absolute_path TEXT NOT NULL, -- full local filesystem path + file_size_bytes BIGINT DEFAULT 0, + file_hash VARCHAR(64), -- SHA-256 hash for integrity + mime_type VARCHAR(100), + is_binary BOOLEAN DEFAULT false, + encoding VARCHAR(50) DEFAULT 'utf-8', + github_sha VARCHAR(40), -- GitHub blob SHA for tracking changes + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +-- Create table for file contents (for text files and searchability) +CREATE TABLE IF NOT EXISTS repository_file_contents ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + file_id UUID REFERENCES repository_files(id) ON DELETE CASCADE, + content_text TEXT, -- full content for text files + content_preview TEXT, -- first 1000 characters for quick preview + language_detected VARCHAR(50), -- programming language detected + line_count INTEGER DEFAULT 0, + char_count INTEGER DEFAULT 0, + is_indexed BOOLEAN DEFAULT false, -- for search indexing status + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + UNIQUE(file_id) +); + +-- Create indexes for performance +CREATE INDEX IF NOT EXISTS idx_repository_storage_repo_id ON repository_storage(repository_id); +CREATE INDEX IF NOT EXISTS idx_repository_storage_status ON repository_storage(storage_status); + +CREATE INDEX IF NOT EXISTS idx_repo_directories_repo_id ON repository_directories(repository_id); +CREATE INDEX IF NOT EXISTS idx_repo_directories_parent_id ON repository_directories(parent_directory_id); +CREATE INDEX IF NOT EXISTS idx_repo_directories_storage_id ON repository_directories(storage_id); +CREATE INDEX IF NOT EXISTS idx_repo_directories_level ON repository_directories(level); +CREATE INDEX IF NOT EXISTS idx_repo_directories_relative_path ON repository_directories(relative_path); + +CREATE INDEX IF NOT EXISTS idx_repo_files_repo_id ON repository_files(repository_id); +CREATE INDEX IF NOT EXISTS idx_repo_files_directory_id ON repository_files(directory_id); +CREATE INDEX IF NOT EXISTS idx_repo_files_storage_id ON repository_files(storage_id); +CREATE INDEX IF NOT EXISTS idx_repo_files_extension ON repository_files(file_extension); +CREATE INDEX IF NOT EXISTS idx_repo_files_filename ON repository_files(filename); +CREATE INDEX IF NOT EXISTS idx_repo_files_relative_path ON repository_files(relative_path); +CREATE INDEX IF NOT EXISTS idx_repo_files_is_binary ON repository_files(is_binary); + +CREATE INDEX IF NOT EXISTS idx_file_contents_file_id ON repository_file_contents(file_id); +CREATE INDEX IF NOT EXISTS idx_file_contents_language ON repository_file_contents(language_detected); +CREATE INDEX IF NOT EXISTS idx_file_contents_is_indexed ON repository_file_contents(is_indexed); + +-- Full text search index for file contents +CREATE INDEX IF NOT EXISTS idx_file_contents_text_search ON repository_file_contents USING gin(to_tsvector('english', content_text)); + +-- Add update triggers +CREATE TRIGGER update_repository_storage_updated_at BEFORE UPDATE ON repository_storage + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_repository_directories_updated_at BEFORE UPDATE ON repository_directories + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_repository_files_updated_at BEFORE UPDATE ON repository_files + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_repository_file_contents_updated_at BEFORE UPDATE ON repository_file_contents + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); diff --git a/services/git-integration/src/migrations/003_add_user_id_to_template_refs.sql b/services/git-integration/src/migrations/003_add_user_id_to_template_refs.sql new file mode 100644 index 0000000..a8d1169 --- /dev/null +++ b/services/git-integration/src/migrations/003_add_user_id_to_template_refs.sql @@ -0,0 +1,21 @@ +-- Migration 003: Add user_id to tables that reference template_id +-- This ensures we always track which user owns/initiated records tied to a template + +-- Add user_id to github_repositories +ALTER TABLE IF EXISTS github_repositories + ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES users(id) ON DELETE CASCADE; + +-- Indexes for github_repositories +CREATE INDEX IF NOT EXISTS idx_github_repos_user_id ON github_repositories(user_id); +CREATE INDEX IF NOT EXISTS idx_github_repos_template_user ON github_repositories(template_id, user_id); + +-- Add user_id to feature_codebase_mappings +ALTER TABLE IF EXISTS feature_codebase_mappings + ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES users(id) ON DELETE CASCADE; + +-- Indexes for feature_codebase_mappings +CREATE INDEX IF NOT EXISTS idx_feature_mappings_user_id ON feature_codebase_mappings(user_id); +CREATE INDEX IF NOT EXISTS idx_feature_mappings_template_user ON feature_codebase_mappings(template_id, user_id); + +-- Note: Columns are nullable to allow backfill before enforcing NOT NULL if desired + diff --git a/services/git-integration/src/migrations/migrate.js b/services/git-integration/src/migrations/migrate.js new file mode 100644 index 0000000..6102fcf --- /dev/null +++ b/services/git-integration/src/migrations/migrate.js @@ -0,0 +1,81 @@ +const fs = require('fs'); +const path = require('path'); +const database = require('../config/database'); + +const migrationsDir = path.join(__dirname); + +async function runMigrations() { + console.log('🚀 Starting Git Integration database migration...'); + + try { + // Connect to database + await database.testConnection(); + console.log('✅ Database connected successfully'); + + // Get list of migration files + const migrationFiles = fs.readdirSync(migrationsDir) + .filter(file => file.endsWith('.sql')) + .sort(); + + console.log(`📄 Found ${migrationFiles.length} migration files:`, migrationFiles); + + for (const migrationFile of migrationFiles) { + console.log(`🚀 Running migration: ${migrationFile}`); + + const migrationPath = path.join(migrationsDir, migrationFile); + const migrationSQL = fs.readFileSync(migrationPath, 'utf8'); + + try { + await database.query(migrationSQL); + console.log(`✅ Migration ${migrationFile} completed successfully!`); + } catch (err) { + const message = (err && err.message) ? err.message.toLowerCase() : ''; + const code = err && err.code ? err.code : ''; + // Continue on idempotency-safe errors (objects already exist) + const isIdempotentError = + message.includes('already exists') || + code === '42710' /* duplicate_object */ || + code === '42P07' /* duplicate_table */ || + code === '42701' /* duplicate_column */ || + code === '42P06' /* duplicate_schema */ || + code === '42723' /* duplicate_function */; + + if (isIdempotentError) { + console.warn(`⚠️ Skipping idempotent error for ${migrationFile}:`, err.message); + continue; + } + + throw err; // rethrow non-idempotent errors + } + } + + // Verify tables were created + const tablesQuery = ` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name NOT LIKE 'pg_%' + ORDER BY table_name + `; + + const tablesResult = await database.query(tablesQuery); + const tableNames = tablesResult.rows.map(row => row.table_name); + + console.log('🔍 Verified tables:', tableNames); + console.log('🎉 All migrations completed successfully!'); + + } catch (error) { + console.error('❌ Migration failed:', error); + process.exit(1); + } finally { + await database.close(); + console.log('🔌 Database connection closed'); + } +} + +// Run migrations if this file is executed directly +if (require.main === module) { + runMigrations(); +} + +module.exports = { runMigrations }; diff --git a/services/git-integration/src/routes/github-integration.routes.js b/services/git-integration/src/routes/github-integration.routes.js new file mode 100644 index 0000000..65f423f --- /dev/null +++ b/services/git-integration/src/routes/github-integration.routes.js @@ -0,0 +1,561 @@ +// Updated routes/github-integration.js +const express = require('express'); +const router = express.Router(); +const GitHubIntegrationService = require('../services/github-integration.service'); +const GitHubOAuthService = require('../services/github-oauth'); +const FileStorageService = require('../services/file-storage.service'); +const database = require('../config/database'); +const fs = require('fs'); +const path = require('path'); + +const githubService = new GitHubIntegrationService(); +const oauthService = new GitHubOAuthService(); +const fileStorageService = new FileStorageService(); + +// Attach GitHub repository to template +router.post('/attach-repository', async (req, res) => { + try { + const { template_id, repository_url, branch_name } = req.body; + + // Validate input + if (!template_id || !repository_url) { + return res.status(400).json({ + success: false, + message: 'Template ID and repository URL are required' + }); + } + + // Check if template exists + const templateQuery = 'SELECT * FROM templates WHERE id = $1 AND is_active = true'; + const templateResult = await database.query(templateQuery, [template_id]); + + if (templateResult.rows.length === 0) { + return res.status(404).json({ + success: false, + message: 'Template not found' + }); + } + + // Parse GitHub URL + const { owner, repo, branch } = githubService.parseGitHubUrl(repository_url); + + // Check repository access + const accessCheck = await githubService.checkRepositoryAccess(owner, repo); + + if (!accessCheck.hasAccess) { + if (accessCheck.requiresAuth) { + // Check if we have OAuth token + const tokenRecord = await oauthService.getToken(); + if (!tokenRecord) { + return res.status(401).json({ + success: false, + message: 'GitHub authentication required for this repository', + requires_auth: true, + auth_url: `/api/github/auth/github` + }); + } + } + + return res.status(404).json({ + success: false, + message: accessCheck.error || 'Repository not accessible' + }); + } + + // Get repository information from GitHub + const repositoryData = await githubService.fetchRepositoryMetadata(owner, repo); + + // Analyze the codebase + const codebaseAnalysis = await githubService.analyzeCodebase(owner, repo, branch || branch_name); + + // Store everything in PostgreSQL + const insertQuery = ` + INSERT INTO github_repositories ( + template_id, repository_url, repository_name, owner_name, + branch_name, is_public, metadata, codebase_analysis, sync_status, + requires_auth + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + RETURNING * + `; + + const insertValues = [ + template_id, + repository_url, + repo, + owner, + branch || branch_name || 'main', + repositoryData.visibility === 'public', + JSON.stringify(repositoryData), + JSON.stringify(codebaseAnalysis), + 'synced', + accessCheck.requiresAuth + ]; + + const insertResult = await database.query(insertQuery, insertValues); + const repositoryRecord = insertResult.rows[0]; + + // Download repository with file storage + console.log('Downloading repository with storage...'); + const downloadResult = await githubService.downloadRepositoryWithStorage( + owner, repo, branch || branch_name || 'main', repositoryRecord.id + ); + + if (!downloadResult.success) { + // If download failed, still return the repository record but mark the storage issue + console.warn('Repository download failed:', downloadResult.error); + } + + // Create feature-codebase mappings + const featureQuery = 'SELECT id FROM template_features WHERE template_id = $1'; + const featureResult = await database.query(featureQuery, [template_id]); + + if (featureResult.rows.length > 0) { + const mappingValues = []; + const mappingParams = []; + let paramIndex = 1; + + for (const feature of featureResult.rows) { + mappingValues.push(`(uuid_generate_v4(), $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++})`); + mappingParams.push( + feature.id, + repositoryRecord.id, + '[]', // Empty paths for now + '{}' // Empty snippets for now + ); + } + + const mappingQuery = ` + INSERT INTO feature_codebase_mappings (id, feature_id, repository_id, code_paths, code_snippets) + VALUES ${mappingValues.join(', ')} + `; + + await database.query(mappingQuery, mappingParams); + } + + // Get storage information + const storageInfo = await githubService.getRepositoryStorage(repositoryRecord.id); + + res.status(201).json({ + success: true, + message: 'Repository attached successfully', + data: { + repository_id: repositoryRecord.id, + template_id: repositoryRecord.template_id, + repository_name: repositoryRecord.repository_name, + owner_name: repositoryRecord.owner_name, + branch_name: repositoryRecord.branch_name, + is_public: repositoryRecord.is_public, + requires_auth: repositoryRecord.requires_auth, + metadata: repositoryData, + codebase_analysis: codebaseAnalysis, + storage_info: storageInfo, + download_result: downloadResult + } + }); + + } catch (error) { + console.error('Error attaching repository:', error); + res.status(500).json({ + success: false, + message: error.message || 'Failed to attach repository' + }); + } +}); + +// Get repository information for a template +router.get('/template/:id/repository', async (req, res) => { + try { + const { id } = req.params; + + const query = ` + SELECT gr.*, rs.local_path, rs.storage_status, rs.total_files_count, + rs.total_directories_count, rs.total_size_bytes, rs.download_completed_at + FROM github_repositories gr + LEFT JOIN repository_storage rs ON gr.id = rs.repository_id + WHERE gr.template_id = $1 + ORDER BY gr.created_at DESC + LIMIT 1 + `; + + const result = await database.query(query, [id]); + + if (result.rows.length === 0) { + return res.status(404).json({ + success: false, + message: 'No repository found for this template' + }); + } + + const repository = result.rows[0]; + + const parseMaybe = (v) => { + if (v == null) return {}; + if (typeof v === 'string') { + try { return JSON.parse(v); } catch { return {}; } + } + return v; // already an object from jsonb + }; + + res.json({ + success: true, + data: { + ...repository, + metadata: parseMaybe(repository.metadata), + codebase_analysis: parseMaybe(repository.codebase_analysis) + } + }); + + } catch (error) { + console.error('Error fetching repository:', error); + res.status(500).json({ + success: false, + message: error.message || 'Failed to fetch repository' + }); + } +}); + +// Get repository file structure +router.get('/repository/:id/structure', async (req, res) => { + try { + const { id } = req.params; + const { path: directoryPath } = req.query; + + // Get repository info + const repoQuery = 'SELECT * FROM github_repositories WHERE id = $1'; + const repoResult = await database.query(repoQuery, [id]); + + if (repoResult.rows.length === 0) { + return res.status(404).json({ + success: false, + message: 'Repository not found' + }); + } + + const structure = await fileStorageService.getRepositoryStructure(id, directoryPath); + + res.json({ + success: true, + data: { + repository_id: id, + directory_path: directoryPath || '', + structure: structure + } + }); + + } catch (error) { + console.error('Error fetching repository structure:', error); + res.status(500).json({ + success: false, + message: error.message || 'Failed to fetch repository structure' + }); + } +}); + +// Get files in a directory +router.get('/repository/:id/files', async (req, res) => { + try { + const { id } = req.params; + const { directory_path = '' } = req.query; + + // Get repository info + const repoQuery = 'SELECT * FROM github_repositories WHERE id = $1'; + const repoResult = await database.query(repoQuery, [id]); + + if (repoResult.rows.length === 0) { + return res.status(404).json({ + success: false, + message: 'Repository not found' + }); + } + + const files = await fileStorageService.getDirectoryFiles(id, directory_path); + + res.json({ + success: true, + data: { + repository_id: id, + directory_path: directory_path, + files: files + } + }); + + } catch (error) { + console.error('Error fetching directory files:', error); + res.status(500).json({ + success: false, + message: error.message || 'Failed to fetch directory files' + }); + } +}); + +// Get file content +router.get('/repository/:id/file-content', async (req, res) => { + try { + const { id } = req.params; + const { file_path } = req.query; + + if (!file_path) { + return res.status(400).json({ + success: false, + message: 'File path is required' + }); + } + + const query = ` + SELECT rf.*, rfc.content_text, rfc.content_preview, rfc.language_detected, + rfc.line_count, rfc.char_count + FROM repository_files rf + LEFT JOIN repository_file_contents rfc ON rf.id = rfc.file_id + WHERE rf.repository_id = $1 AND rf.relative_path = $2 + `; + + const result = await database.query(query, [id, file_path]); + + if (result.rows.length === 0) { + return res.status(404).json({ + success: false, + message: 'File not found' + }); + } + + const file = result.rows[0]; + + res.json({ + success: true, + data: { + file_info: { + id: file.id, + filename: file.filename, + file_extension: file.file_extension, + relative_path: file.relative_path, + file_size_bytes: file.file_size_bytes, + mime_type: file.mime_type, + is_binary: file.is_binary, + language_detected: file.language_detected, + line_count: file.line_count, + char_count: file.char_count + }, + content: file.is_binary ? null : file.content_text, + preview: file.content_preview + } + }); + + } catch (error) { + console.error('Error fetching file content:', error); + res.status(500).json({ + success: false, + message: error.message || 'Failed to fetch file content' + }); + } +}); + +// Search repository files +router.get('/repository/:id/search', async (req, res) => { + try { + const { id } = req.params; + const { q: query } = req.query; + + if (!query) { + return res.status(400).json({ + success: false, + message: 'Search query is required' + }); + } + + const results = await fileStorageService.searchFileContent(id, query); + + res.json({ + success: true, + data: { + repository_id: id, + search_query: query, + results: results, + total_results: results.length + } + }); + + } catch (error) { + console.error('Error searching repository:', error); + res.status(500).json({ + success: false, + message: error.message || 'Failed to search repository' + }); + } +}); + +// List all repositories for a template +router.get('/template/:id/repositories', async (req, res) => { + try { + const { id } = req.params; + + const query = ` + SELECT gr.*, rs.local_path, rs.storage_status, rs.total_files_count, + rs.total_directories_count, rs.total_size_bytes, rs.download_completed_at + FROM github_repositories gr + LEFT JOIN repository_storage rs ON gr.id = rs.repository_id + WHERE gr.template_id = $1 + ORDER BY gr.created_at DESC + `; + + const result = await database.query(query, [id]); + + const repositories = result.rows.map(repo => ({ + ...repo, + metadata: JSON.parse(repo.metadata || '{}'), + codebase_analysis: JSON.parse(repo.codebase_analysis || '{}') + })); + + res.json({ + success: true, + data: repositories + }); + + } catch (error) { + console.error('Error fetching repositories:', error); + res.status(500).json({ + success: false, + message: error.message || 'Failed to fetch repositories' + }); + } +}); + +// Download repository files (legacy endpoint for backward compatibility) +router.post('/download', async (req, res) => { + try { + const { repository_url, branch_name } = req.body; + + if (!repository_url) { + return res.status(400).json({ + success: false, + message: 'Repository URL is required' + }); + } + + const { owner, repo, branch } = githubService.parseGitHubUrl(repository_url); + const targetBranch = branch || branch_name || 'main'; + + const result = await githubService.downloadRepository(owner, repo, targetBranch); + + if (result.success) { + res.json({ + success: true, + message: 'Repository downloaded successfully', + data: result + }); + } else { + res.status(500).json({ + success: false, + message: 'Failed to download repository', + error: result.error + }); + } + + } catch (error) { + console.error('Error downloading repository:', error); + res.status(500).json({ + success: false, + message: error.message || 'Failed to download repository' + }); + } +}); + +// Re-sync repository (re-download and update database) +router.post('/repository/:id/sync', async (req, res) => { + try { + const { id } = req.params; + + // Get repository info + const repoQuery = 'SELECT * FROM github_repositories WHERE id = $1'; + const repoResult = await database.query(repoQuery, [id]); + + if (repoResult.rows.length === 0) { + return res.status(404).json({ + success: false, + message: 'Repository not found' + }); + } + + const repository = repoResult.rows[0]; + const { owner, repo, branch } = githubService.parseGitHubUrl(repository.repository_url); + + // Clean up existing storage + await githubService.cleanupRepositoryStorage(id); + + // Re-download with storage + const downloadResult = await githubService.downloadRepositoryWithStorage( + owner, repo, branch || repository.branch_name, id + ); + + // Update sync status + await database.query( + 'UPDATE github_repositories SET sync_status = $1, updated_at = NOW() WHERE id = $2', + [downloadResult.success ? 'synced' : 'error', id] + ); + + res.json({ + success: downloadResult.success, + message: downloadResult.success ? 'Repository synced successfully' : 'Failed to sync repository', + data: downloadResult + }); + + } catch (error) { + console.error('Error syncing repository:', error); + res.status(500).json({ + success: false, + message: error.message || 'Failed to sync repository' + }); + } +}); + +// Remove repository from template +router.delete('/repository/:id', async (req, res) => { + try { + const { id } = req.params; + + // Get repository info before deletion + const getQuery = 'SELECT * FROM github_repositories WHERE id = $1'; + const getResult = await database.query(getQuery, [id]); + + if (getResult.rows.length === 0) { + return res.status(404).json({ + success: false, + message: 'Repository not found' + }); + } + + const repository = getResult.rows[0]; + + // Clean up file storage + await githubService.cleanupRepositoryStorage(id); + + // Delete feature mappings first + await database.query( + 'DELETE FROM feature_codebase_mappings WHERE repository_id = $1', + [id] + ); + + // Delete repository record + await database.query( + 'DELETE FROM github_repositories WHERE id = $1', + [id] + ); + + res.json({ + success: true, + message: 'Repository removed successfully', + data: { + removed_repository: repository.repository_name, + template_id: repository.template_id + } + }); + + } catch (error) { + console.error('Error removing repository:', error); + res.status(500).json({ + success: false, + message: error.message || 'Failed to remove repository' + }); + } +}); + +module.exports = router; diff --git a/services/git-integration/src/routes/github-oauth.js b/services/git-integration/src/routes/github-oauth.js new file mode 100644 index 0000000..43a6346 --- /dev/null +++ b/services/git-integration/src/routes/github-oauth.js @@ -0,0 +1,169 @@ +// routes/github-oauth.js +const express = require('express'); +const router = express.Router(); +const GitHubOAuthService = require('../services/github-oauth'); + +const oauthService = new GitHubOAuthService(); + +// Initiate GitHub OAuth flow +router.get('/auth/github', async (req, res) => { + try { + const state = Math.random().toString(36).substring(7); + const authUrl = oauthService.getAuthUrl(state); + + res.json({ + success: true, + data: { + auth_url: authUrl, + state: state + } + }); + + } catch (error) { + console.error('Error initiating GitHub OAuth:', error); + res.status(500).json({ + success: false, + message: error.message || 'Failed to initiate GitHub authentication' + }); + } +}); + +// Handle GitHub OAuth callback +router.get('/auth/github/callback', async (req, res) => { + try { + const { code, state } = req.query; + + if (!code) { + return res.status(400).json({ + success: false, + message: 'Authorization code missing' + }); + } + + // Exchange code for token + const accessToken = await oauthService.exchangeCodeForToken(code); + + // Get user info from GitHub + const githubUser = await oauthService.getUserInfo(accessToken); + + // Store token + const tokenRecord = await oauthService.storeToken(accessToken, githubUser); + + // Redirect back to frontend if configured + const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000'; + try { + const redirectUrl = `${frontendUrl}/project-builder?github_connected=1&user=${encodeURIComponent(githubUser.login)}`; + return res.redirect(302, redirectUrl); + } catch (e) { + // Fallback to JSON if redirect fails + return res.json({ + success: true, + message: 'GitHub account connected successfully', + data: { + github_username: githubUser.login, + github_user_id: githubUser.id, + connected_at: tokenRecord.created_at + } + }); + } + + } catch (error) { + console.error('Error handling GitHub callback:', error); + res.status(500).json({ + success: false, + message: error.message || 'Failed to connect GitHub account' + }); + } +}); + +// Get GitHub connection status +router.get('/auth/github/status', async (req, res) => { + try { + const tokenRecord = await oauthService.getToken(); + + if (tokenRecord) { + res.json({ + success: true, + data: { + connected: true, + github_username: tokenRecord.github_username, + github_user_id: tokenRecord.github_user_id, + connected_at: tokenRecord.created_at, + scopes: tokenRecord.scopes + } + }); + } else { + res.json({ + success: true, + data: { + connected: false + } + }); + } + + } catch (error) { + console.error('Error checking GitHub status:', error); + res.status(500).json({ + success: false, + message: error.message || 'Failed to check GitHub connection status' + }); + } +}); + +// Disconnect GitHub account +router.delete('/auth/github', async (req, res) => { + try { + await oauthService.revokeToken(); + + res.json({ + success: true, + message: 'GitHub account disconnected successfully' + }); + + } catch (error) { + console.error('Error disconnecting GitHub:', error); + res.status(500).json({ + success: false, + message: error.message || 'Failed to disconnect GitHub account' + }); + } +}); + +// Test repository access +router.post('/test-access', async (req, res) => { + try { + const { repository_url } = req.body; + + if (!repository_url) { + return res.status(400).json({ + success: false, + message: 'Repository URL is required' + }); + } + + const GitHubIntegrationService = require('../services/github-integration.service'); + const githubService = new GitHubIntegrationService(); + + const { owner, repo } = githubService.parseGitHubUrl(repository_url); + const canAccess = await oauthService.canAccessRepository(owner, repo); + + res.json({ + success: true, + data: { + repository_url, + owner, + repo, + can_access: canAccess + } + }); + + } catch (error) { + console.error('Error testing repository access:', error); + res.status(500).json({ + success: false, + message: error.message || 'Failed to test repository access' + }); + } +}); + +module.exports = router; diff --git a/services/git-integration/src/services/file-storage.service.js b/services/git-integration/src/services/file-storage.service.js new file mode 100644 index 0000000..415b567 --- /dev/null +++ b/services/git-integration/src/services/file-storage.service.js @@ -0,0 +1,373 @@ +// services/file-storage.service.js +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); +const database = require('../config/database'); + +class FileStorageService { + constructor() { + this.supportedTextExtensions = new Set([ + '.js', '.ts', '.jsx', '.tsx', '.vue', '.py', '.java', '.cpp', '.c', '.cs', + '.php', '.rb', '.go', '.rs', '.kt', '.swift', '.scala', '.clj', '.hs', + '.elm', '.ml', '.fs', '.vb', '.pas', '.asm', '.sql', '.sh', '.bash', + '.ps1', '.bat', '.cmd', '.html', '.htm', '.xml', '.css', '.scss', '.sass', + '.less', '.json', '.yaml', '.yml', '.toml', '.ini', '.cfg', '.conf', + '.env', '.md', '.txt', '.rst', '.adoc', '.tex', '.r', '.m', '.pl', + '.lua', '.dart', '.jl', '.nim', '.zig', '.v', '.d', '.cr', '.ex', '.exs' + ]); + + this.languageMap = { + '.js': 'javascript', '.ts': 'typescript', '.jsx': 'javascript', '.tsx': 'typescript', + '.vue': 'vue', '.py': 'python', '.java': 'java', '.cpp': 'cpp', '.c': 'c', + '.cs': 'csharp', '.php': 'php', '.rb': 'ruby', '.go': 'go', '.rs': 'rust', + '.kt': 'kotlin', '.swift': 'swift', '.scala': 'scala', '.clj': 'clojure', + '.hs': 'haskell', '.elm': 'elm', '.ml': 'ocaml', '.fs': 'fsharp', + '.vb': 'vbnet', '.html': 'html', '.css': 'css', '.scss': 'scss', + '.json': 'json', '.yaml': 'yaml', '.yml': 'yaml', '.xml': 'xml', + '.sql': 'sql', '.sh': 'bash', '.md': 'markdown' + }; + } + + // Initialize storage record for a repository + async initializeRepositoryStorage(repositoryId, localPath) { + const query = ` + INSERT INTO repository_storage ( + repository_id, local_path, storage_status, download_started_at + ) VALUES ($1, $2, $3, NOW()) + ON CONFLICT (repository_id) + DO UPDATE SET + local_path = $2, + storage_status = $3, + download_started_at = NOW(), + updated_at = NOW() + RETURNING * + `; + + const result = await database.query(query, [repositoryId, localPath, 'downloading']); + return result.rows[0]; + } + + // Process and store directory structure + async processDirectoryStructure(storageId, repositoryId, basePath, currentPath = '', parentDirId = null, level = 0) { + const fullPath = path.join(basePath, currentPath); + + if (!fs.existsSync(fullPath)) { + return null; + } + + const stats = fs.statSync(fullPath); + + if (!stats.isDirectory()) { + return null; + } + + // Insert directory record + const dirName = currentPath === '' ? '.' : path.basename(currentPath); + const relativePath = currentPath === '' ? '' : currentPath; + + const dirQuery = ` + INSERT INTO repository_directories ( + repository_id, storage_id, parent_directory_id, directory_name, + relative_path, absolute_path, level + ) VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING * + `; + + const dirResult = await database.query(dirQuery, [ + repositoryId, storageId, parentDirId, dirName, + relativePath, fullPath, level + ]); + + const directoryRecord = dirResult.rows[0]; + let totalFiles = 0; + let totalSubDirs = 0; + let totalSize = 0; + + try { + const items = fs.readdirSync(fullPath); + + for (const item of items) { + const itemPath = path.join(fullPath, item); + const itemRelativePath = currentPath ? path.join(currentPath, item) : item; + const itemStats = fs.statSync(itemPath); + + if (itemStats.isDirectory()) { + // Recursively process subdirectory + const subDir = await this.processDirectoryStructure( + storageId, repositoryId, basePath, itemRelativePath, + directoryRecord.id, level + 1 + ); + if (subDir) { + totalSubDirs++; + } + } else if (itemStats.isFile()) { + // Process file + const fileRecord = await this.processFile( + storageId, repositoryId, directoryRecord.id, itemPath, itemRelativePath + ); + if (fileRecord) { + totalFiles++; + // IMPORTANT: node-postgres returns BIGINT columns as strings. + // Avoid string concatenation causing huge numeric strings by summing the + // actual filesystem-reported size (a number) for aggregation. + totalSize += Number(itemStats.size) || 0; + } + } + } + } catch (error) { + console.warn(`Error processing directory ${fullPath}:`, error.message); + } + + // Update directory stats + await database.query(` + UPDATE repository_directories + SET files_count = $1, subdirectories_count = $2, total_size_bytes = $3, updated_at = NOW() + WHERE id = $4 + `, [totalFiles, totalSubDirs, totalSize, directoryRecord.id]); + + return directoryRecord; + } + + // Process and store individual file + async processFile(storageId, repositoryId, directoryId, absolutePath, relativePath) { + try { + const stats = fs.statSync(absolutePath); + const filename = path.basename(absolutePath); + const extension = path.extname(filename).toLowerCase(); + + // Calculate file hash + const fileBuffer = fs.readFileSync(absolutePath); + const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex'); + + // Determine if file is binary + const isBinary = !this.supportedTextExtensions.has(extension) || this.isBinaryContent(fileBuffer); + + // Get MIME type (simplified) + const mimeType = this.getMimeType(extension, isBinary); + + // Insert file record + const fileQuery = ` + INSERT INTO repository_files ( + repository_id, storage_id, directory_id, filename, file_extension, + relative_path, absolute_path, file_size_bytes, file_hash, + mime_type, is_binary, encoding + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + RETURNING * + `; + + const fileResult = await database.query(fileQuery, [ + repositoryId, storageId, directoryId, filename, extension, + relativePath, absolutePath, stats.size, hash, + mimeType, isBinary, isBinary ? null : 'utf-8' + ]); + + const fileRecord = fileResult.rows[0]; + + // Process file content if it's a text file and not too large (< 10MB) + if (!isBinary && stats.size < 10 * 1024 * 1024) { + await this.processFileContent(fileRecord.id, absolutePath, extension); + } + + return fileRecord; + + } catch (error) { + console.warn(`Error processing file ${absolutePath}:`, error.message); + return null; + } + } + + // Process and store file content + async processFileContent(fileId, absolutePath, extension) { + try { + const content = fs.readFileSync(absolutePath, 'utf-8'); + const lines = content.split('\n'); + const preview = content.substring(0, 1000); + const language = this.languageMap[extension] || 'text'; + + const contentQuery = ` + INSERT INTO repository_file_contents ( + file_id, content_text, content_preview, language_detected, + line_count, char_count + ) VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (file_id) DO UPDATE SET + content_text = $2, + content_preview = $3, + language_detected = $4, + line_count = $5, + char_count = $6, + updated_at = NOW() + `; + + await database.query(contentQuery, [ + fileId, content, preview, language, lines.length, content.length + ]); + + } catch (error) { + console.warn(`Error processing file content for file ID ${fileId}:`, error.message); + } + } + + // Complete storage process for a repository + async completeRepositoryStorage(storageId) { + // Calculate totals + const statsQuery = ` + SELECT + COUNT(DISTINCT rd.id) as total_directories, + COUNT(rf.id) as total_files, + COALESCE(SUM(rf.file_size_bytes), 0) as total_size + FROM repository_storage rs + LEFT JOIN repository_directories rd ON rs.id = rd.storage_id + LEFT JOIN repository_files rf ON rs.id = rf.storage_id + WHERE rs.id = $1 + `; + + const statsResult = await database.query(statsQuery, [storageId]); + const stats = statsResult.rows[0]; + + // Update storage record + const updateQuery = ` + UPDATE repository_storage + SET + storage_status = 'completed', + total_files_count = $1, + total_directories_count = $2, + total_size_bytes = $3, + download_completed_at = NOW(), + updated_at = NOW() + WHERE id = $4 + RETURNING * + `; + + const result = await database.query(updateQuery, [ + parseInt(stats.total_files), + parseInt(stats.total_directories), + parseInt(stats.total_size), + storageId + ]); + + return result.rows[0]; + } + + // Mark storage as failed + async markStorageFailed(storageId, error) { + const query = ` + UPDATE repository_storage + SET + storage_status = 'error', + updated_at = NOW() + WHERE id = $1 + RETURNING * + `; + + const result = await database.query(query, [storageId]); + return result.rows[0]; + } + + // Get repository file structure + async getRepositoryStructure(repositoryId, directoryPath = null) { + let query = ` + SELECT + rd.*, + COUNT(DISTINCT rdf.id) as files_count, + COUNT(DISTINCT rds.id) as subdirs_count + FROM repository_directories rd + LEFT JOIN repository_files rdf ON rd.id = rdf.directory_id + LEFT JOIN repository_directories rds ON rd.id = rds.parent_directory_id + WHERE rd.repository_id = $1 + `; + + const params = [repositoryId]; + + if (directoryPath !== null) { + query += ` AND rd.relative_path = $2`; + params.push(directoryPath); + } + + query += ` GROUP BY rd.id ORDER BY rd.level, rd.directory_name`; + + const result = await database.query(query, params); + return result.rows; + } + + // Get files in a directory + async getDirectoryFiles(repositoryId, directoryPath = '') { + const query = ` + SELECT rf.*, rfc.language_detected, rfc.line_count + FROM repository_files rf + LEFT JOIN repository_directories rd ON rf.directory_id = rd.id + LEFT JOIN repository_file_contents rfc ON rf.id = rfc.file_id + WHERE rf.repository_id = $1 AND rd.relative_path = $2 + ORDER BY rf.filename + `; + + const result = await database.query(query, [repositoryId, directoryPath]); + return result.rows; + } + + // Search files by content + async searchFileContent(repositoryId, searchQuery) { + const query = ` + SELECT rf.filename, rf.relative_path, rfc.language_detected, + ts_rank_cd(to_tsvector('english', rfc.content_text), plainto_tsquery('english', $2)) as rank + FROM repository_files rf + JOIN repository_file_contents rfc ON rf.id = rfc.file_id + WHERE rf.repository_id = $1 + AND to_tsvector('english', rfc.content_text) @@ plainto_tsquery('english', $2) + ORDER BY rank DESC, rf.filename + LIMIT 50 + `; + + const result = await database.query(query, [repositoryId, searchQuery]); + return result.rows; + } + + // Utility methods + isBinaryContent(buffer) { + // Simple binary detection - check for null bytes in first 1024 bytes + const sample = buffer.slice(0, Math.min(1024, buffer.length)); + return sample.includes(0); + } + + getMimeType(extension, isBinary) { + const mimeTypes = { + '.js': 'application/javascript', + '.ts': 'application/typescript', + '.json': 'application/json', + '.html': 'text/html', + '.css': 'text/css', + '.md': 'text/markdown', + '.txt': 'text/plain', + '.xml': 'application/xml', + '.yml': 'application/x-yaml', + '.yaml': 'application/x-yaml', + '.pdf': 'application/pdf', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.svg': 'image/svg+xml' + }; + + if (mimeTypes[extension]) { + return mimeTypes[extension]; + } + + return isBinary ? 'application/octet-stream' : 'text/plain'; + } + + // Clean up storage for a repository + async cleanupRepositoryStorage(repositoryId) { + const queries = [ + 'DELETE FROM repository_file_contents WHERE file_id IN (SELECT id FROM repository_files WHERE repository_id = $1)', + 'DELETE FROM repository_files WHERE repository_id = $1', + 'DELETE FROM repository_directories WHERE repository_id = $1', + 'DELETE FROM repository_storage WHERE repository_id = $1' + ]; + + for (const query of queries) { + await database.query(query, [repositoryId]); + } + } +} + +module.exports = FileStorageService; diff --git a/services/git-integration/src/services/github-integration.service.js b/services/git-integration/src/services/github-integration.service.js new file mode 100644 index 0000000..4d460d1 --- /dev/null +++ b/services/git-integration/src/services/github-integration.service.js @@ -0,0 +1,402 @@ +// Updated github-integration.js service +const { Octokit } = require('@octokit/rest'); +const fs = require('fs'); +const path = require('path'); +const { exec } = require('child_process'); +const GitHubOAuthService = require('./github-oauth'); +const FileStorageService = require('./file-storage.service'); + +class GitHubIntegrationService { + constructor() { + this.oauthService = new GitHubOAuthService(); + this.fileStorageService = new FileStorageService(); + + // Default unauthenticated instance + this.octokit = new Octokit({ + userAgent: 'CodeNuk-GitIntegration/1.0.0', + }); + } + + // Get authenticated Octokit instance + async getAuthenticatedOctokit() { + return await this.oauthService.getAuthenticatedOctokit(); + } + + // Extract owner, repo, and branch from GitHub URL + parseGitHubUrl(url) { + const regex = /github\.com\/([^\/]+)\/([^\/]+)(?:\/tree\/([^\/]+))?/; + const match = url.match(regex); + + if (!match) { + throw new Error('Invalid GitHub repository URL'); + } + + return { + owner: match[1], + repo: match[2].replace('.git', ''), + branch: match[3] || 'main' + }; + } + + // Check repository access and type + async checkRepositoryAccess(owner, repo) { + try { + const octokit = await this.getAuthenticatedOctokit(); + const { data } = await octokit.repos.get({ owner, repo }); + + return { + exists: true, + isPrivate: data.private, + hasAccess: true, + requiresAuth: data.private + }; + } catch (error) { + if (error.status === 404) { + return { + exists: false, + isPrivate: null, + hasAccess: false, + requiresAuth: true, + error: 'Repository not found or requires authentication' + }; + } + throw error; + } + } + + // Get repository information from GitHub + async fetchRepositoryMetadata(owner, repo) { + const octokit = await this.getAuthenticatedOctokit(); + + const safe = async (fn, fallback) => { + try { + return await fn(); + } catch (error) { + console.warn(`API call failed: ${error.message}`); + return fallback; + } + }; + + const repoData = await safe( + async () => (await octokit.repos.get({ owner, repo })).data, + {} + ); + + const languages = await safe( + async () => (await octokit.repos.listLanguages({ owner, repo })).data, + {} + ); + + const topics = await safe( + async () => (await octokit.repos.getAllTopics({ owner, repo })).data?.names || [], + [] + ); + + return { + full_name: repoData.full_name || `${owner}/${repo}`, + description: repoData.description || null, + language: repoData.language || null, + topics, + languages, + visibility: repoData.private ? 'private' : 'public', + stargazers_count: repoData.stargazers_count || 0, + forks_count: repoData.forks_count || 0, + default_branch: repoData.default_branch || 'main', + size: repoData.size || 0, + updated_at: repoData.updated_at || new Date().toISOString() + }; + } + + // Analyze codebase structure + async analyzeCodebase(owner, repo, branch) { + try { + const octokit = await this.getAuthenticatedOctokit(); + + // Get the commit SHA for the branch + const { data: ref } = await octokit.git.getRef({ + owner, + repo, + ref: `heads/${branch}` + }); + + const commitSha = ref.object.sha; + + // Get the tree recursively + const { data: tree } = await octokit.git.getTree({ + owner, + repo, + tree_sha: commitSha, + recursive: 'true' + }); + + const analysis = { + total_files: 0, + total_size: 0, + languages: {}, + file_types: {}, + directories: [], + last_commit: commitSha, + branch: branch + }; + + tree.tree.forEach(item => { + if (item.type === 'blob') { + analysis.total_files++; + analysis.total_size += item.size || 0; + + const ext = path.extname(item.path).toLowerCase(); + analysis.file_types[ext] = (analysis.file_types[ext] || 0) + 1; + } else if (item.type === 'tree') { + analysis.directories.push(item.path); + } + }); + + return analysis; + } catch (error) { + console.error('Error analyzing codebase:', error); + return { + error: error.message, + total_files: 0, + total_size: 0 + }; + } + } + + // Update GitHub SHAs for files after processing + async updateFileGitHubSHAs(repositoryId, fileMap) { + try { + const database = require('../config/database'); + for (const [relativePath, githubSha] of fileMap.entries()) { + await database.query( + 'UPDATE repository_files SET github_sha = $1 WHERE repository_id = $2 AND relative_path = $3', + [githubSha, repositoryId, relativePath] + ); + } + } catch (error) { + console.warn('Error updating GitHub SHAs:', error.message); + } + } + + // Get repository storage information + async getRepositoryStorage(repositoryId) { + const database = require('../config/database'); + const query = ` + SELECT rs.*, + COUNT(DISTINCT rd.id) as directories_count, + COUNT(rf.id) as files_count + FROM repository_storage rs + LEFT JOIN repository_directories rd ON rs.id = rd.storage_id + LEFT JOIN repository_files rf ON rs.id = rf.storage_id + WHERE rs.repository_id = $1 + GROUP BY rs.id + `; + + const result = await database.query(query, [repositoryId]); + return result.rows[0] || null; + } + + // Clean up repository storage + async cleanupRepositoryStorage(repositoryId) { + return await this.fileStorageService.cleanupRepositoryStorage(repositoryId); + } + + // Download repository files locally and store in database + async downloadRepositoryWithStorage(owner, repo, branch, repositoryId) { + const targetDir = path.join( + process.env.ATTACHED_REPOS_DIR || '/tmp/attached-repos', + `${owner}__${repo}__${branch}` + ); + + // Create target directory + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }); + } + + let storageRecord = null; + + try { + // Initialize storage record + storageRecord = await this.fileStorageService.initializeRepositoryStorage( + repositoryId, + targetDir + ); + + const octokit = await this.getAuthenticatedOctokit(); + + // Get the commit SHA for the branch + const { data: ref } = await octokit.git.getRef({ + owner, + repo, + ref: `heads/${branch}` + }); + + const commitSha = ref.object.sha; + + // Get the tree recursively + const { data: tree } = await octokit.git.getTree({ + owner, + repo, + tree_sha: commitSha, + recursive: 'true' + }); + + let filesWritten = 0; + let totalBytes = 0; + const fileMap = new Map(); // Map to store GitHub SHA for files + + // Process each file + for (const item of tree.tree) { + if (item.type === 'blob') { + try { + const { data: blob } = await octokit.git.getBlob({ + owner, + repo, + file_sha: item.sha + }); + + const filePath = path.join(targetDir, item.path); + const fileDir = path.dirname(filePath); + + // Create directory if it doesn't exist + if (!fs.existsSync(fileDir)) { + fs.mkdirSync(fileDir, { recursive: true }); + } + + // Write file content + const content = Buffer.from(blob.content, 'base64'); + fs.writeFileSync(filePath, content); + + // Store GitHub SHA for later use + fileMap.set(item.path, item.sha); + + filesWritten++; + totalBytes += content.length; + } catch (error) { + console.warn(`Failed to download file ${item.path}:`, error.message); + } + } + } + + // Process directory structure and store in database + console.log('Processing directory structure...'); + await this.fileStorageService.processDirectoryStructure( + storageRecord.id, + repositoryId, + targetDir + ); + + // Update GitHub SHAs for files + await this.updateFileGitHubSHAs(repositoryId, fileMap); + + // Complete storage process + const finalStorage = await this.fileStorageService.completeRepositoryStorage( + storageRecord.id + ); + + console.log(`Repository storage completed: ${finalStorage.total_files_count} files, ${finalStorage.total_directories_count} directories`); + + return { + success: true, + targetDir, + files: filesWritten, + bytes: totalBytes, + storage: finalStorage + }; + + } catch (error) { + console.error('Error downloading repository with storage:', error); + + if (storageRecord) { + await this.fileStorageService.markStorageFailed(storageRecord.id, error.message); + } + + return { + success: false, + error: error.message + }; + } + } + + // Legacy method - download repository files locally (backwards compatibility) + async downloadRepository(owner, repo, branch) { + const targetDir = path.join( + process.env.ATTACHED_REPOS_DIR || '/tmp/attached-repos', + `${owner}__${repo}__${branch}` + ); + + // Create target directory + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }); + } + + try { + const octokit = await this.getAuthenticatedOctokit(); + + // Get the commit SHA for the branch + const { data: ref } = await octokit.git.getRef({ + owner, + repo, + ref: `heads/${branch}` + }); + + const commitSha = ref.object.sha; + + // Get the tree recursively + const { data: tree } = await octokit.git.getTree({ + owner, + repo, + tree_sha: commitSha, + recursive: 'true' + }); + + let filesWritten = 0; + let totalBytes = 0; + + // Process each file + for (const item of tree.tree) { + if (item.type === 'blob') { + try { + const { data: blob } = await octokit.git.getBlob({ + owner, + repo, + file_sha: item.sha + }); + + const filePath = path.join(targetDir, item.path); + const fileDir = path.dirname(filePath); + + // Create directory if it doesn't exist + if (!fs.existsSync(fileDir)) { + fs.mkdirSync(fileDir, { recursive: true }); + } + + // Write file content + const content = Buffer.from(blob.content, 'base64'); + fs.writeFileSync(filePath, content); + + filesWritten++; + totalBytes += content.length; + } catch (error) { + console.warn(`Failed to download file ${item.path}:`, error.message); + } + } + } + + return { + success: true, + targetDir, + files: filesWritten, + bytes: totalBytes + }; + + } catch (error) { + console.error('Error downloading repository:', error); + return { + success: false, + error: error.message + }; + } + } +} + +module.exports = GitHubIntegrationService; diff --git a/services/git-integration/src/services/github-oauth.js b/services/git-integration/src/services/github-oauth.js new file mode 100644 index 0000000..f5f464e --- /dev/null +++ b/services/git-integration/src/services/github-oauth.js @@ -0,0 +1,153 @@ +// github-oauth.js +const { Octokit } = require('@octokit/rest'); +const database = require('../config/database'); + +class GitHubOAuthService { + constructor() { + this.clientId = process.env.GITHUB_CLIENT_ID; + this.clientSecret = process.env.GITHUB_CLIENT_SECRET; + this.redirectUri = process.env.GITHUB_REDIRECT_URI || 'http://localhost:8010/api/github/auth/github/callback'; + + if (!this.clientId || !this.clientSecret) { + console.warn('GitHub OAuth not configured. Only public repositories will be accessible.'); + } + } + + // Generate GitHub OAuth URL + getAuthUrl(state) { + if (!this.clientId) { + throw new Error('GitHub OAuth not configured'); + } + + const params = new URLSearchParams({ + client_id: this.clientId, + redirect_uri: this.redirectUri, + scope: 'repo,user:email', + state: state, + allow_signup: 'false' + }); + + return `https://github.com/login/oauth/authorize?${params.toString()}`; + } + + // Exchange authorization code for access token + async exchangeCodeForToken(code) { + const response = await fetch('https://github.com/login/oauth/access_token', { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + client_id: this.clientId, + client_secret: this.clientSecret, + code: code, + }), + }); + + const data = await response.json(); + + if (data.error) { + throw new Error(`OAuth error: ${data.error_description}`); + } + + return data.access_token; + } + + // Get user info from GitHub + async getUserInfo(accessToken) { + const octokit = new Octokit({ auth: accessToken }); + const { data: user } = await octokit.users.getAuthenticated(); + return user; + } + + // Store GitHub token (no user ID) + async storeToken(accessToken, githubUser) { + const query = ` + INSERT INTO github_user_tokens (access_token, github_username, github_user_id, scopes, expires_at) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (id) + DO UPDATE SET + access_token = $1, + github_username = $2, + github_user_id = $3, + scopes = $4, + expires_at = $5, + updated_at = NOW() + RETURNING * + `; + + const result = await database.query(query, [ + accessToken, + githubUser.login, + githubUser.id, + JSON.stringify(['repo', 'user:email']), + null + ]); + + return result.rows[0]; + } + + // Get stored token + async getToken() { + const query = 'SELECT * FROM github_user_tokens ORDER BY created_at DESC LIMIT 1'; + const result = await database.query(query); + return result.rows[0]; + } + + // Create authenticated Octokit instance + async getAuthenticatedOctokit() { + const tokenRecord = await this.getToken(); + + if (!tokenRecord) { + return new Octokit({ + userAgent: 'CodeNuk-GitIntegration/1.0.0', + }); + } + + return new Octokit({ + auth: tokenRecord.access_token, + userAgent: 'CodeNuk-GitIntegration/1.0.0', + }); + } + + // Check repository access + async canAccessRepository(owner, repo) { + try { + const octokit = await this.getAuthenticatedOctokit(); + await octokit.repos.get({ owner, repo }); + return true; + } catch (error) { + if (error.status === 404) { + return false; + } + throw error; + } + } + + // Revoke token + async revokeToken() { + const tokenRecord = await this.getToken(); + + if (tokenRecord) { + try { + await fetch(`https://api.github.com/applications/${this.clientId}/grant`, { + method: 'DELETE', + headers: { + 'Authorization': `Basic ${Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64')}`, + 'Accept': 'application/vnd.github.v3+json', + }, + body: JSON.stringify({ + access_token: tokenRecord.access_token + }) + }); + } catch (error) { + console.error('Error revoking token on GitHub:', error); + } + + await database.query('DELETE FROM github_user_tokens'); + } + } +} + +module.exports = GitHubOAuthService;