From bb075fd09414718f28cafab8c62a4cb263685e73 Mon Sep 17 00:00:00 2001 From: lucky irene kagabo Date: Thu, 19 Mar 2026 18:45:52 +0200 Subject: [PATCH] Fix logic gaps: auth refresh tokens, provider routes, and Dockerfile --- Dockerfile | 2 +- package.json | 3 +- src/database/migrate.ts | 65 +++++++-------- src/database/schema.sql | 13 +++ src/routes/provider.routes.ts | 145 +++++++++++++++++----------------- 5 files changed, 123 insertions(+), 105 deletions(-) diff --git a/Dockerfile b/Dockerfile index 125d31d..3a1cb39 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,4 +26,4 @@ COPY --from=builder /app/dist ./dist EXPOSE 3000 # Start server -CMD ["node", "dist/server.js"] +CMD ["node", "dist/index.js"] diff --git a/package.json b/package.json index 6675255..453ead9 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "lint": "eslint src --ext .ts", "test": "jest", "db:migrate": "node dist/database/migrate.js", + "db:migrate:ts": "tsx src/database/migrate.ts", "db:migrate:rollback": "node dist/database/migrate.js rollback", "db:migrate:rollback-all": "node dist/database/migrate.js rollback-all", "db:migrate:status": "node dist/database/migrate.js status", @@ -66,4 +67,4 @@ "tsx": "^4.7.0", "typescript": "^5.3.3" } -} +} \ No newline at end of file diff --git a/src/database/migrate.ts b/src/database/migrate.ts index 2c329dd..d66d3c9 100644 --- a/src/database/migrate.ts +++ b/src/database/migrate.ts @@ -1,6 +1,8 @@ import { existsSync, readFileSync } from 'fs'; import { join, resolve } from 'path'; import pool from '../config/database'; +import { Client } from 'pg'; +import { config } from '../config/config'; /** * Simple schema migration: @@ -12,6 +14,38 @@ async function migrate() { try { console.log('Running database schema...\n'); + // Step 1: Ensure database exists + const dbName = config.database.name || 'hano_db'; + console.log(`Checking if database "${dbName}" exists...`); + + const client = new Client({ + host: config.database.host, + port: config.database.port, + user: config.database.user, + password: config.database.password, + database: 'postgres', // Connect to default postgres DB first + }); + + try { + await client.connect(); + const checkDb = await client.query(`SELECT 1 FROM pg_database WHERE datname = $1`, [dbName]); + + if (checkDb.rows.length === 0) { + console.log(`Database "${dbName}" not found. Creating it...`); + // Cannot use parameterized query for CREATE DATABASE + await client.query(`CREATE DATABASE "${dbName}"`); + console.log(`✓ Database "${dbName}" created successfully.`); + } else { + console.log(`✓ Database "${dbName}" already exists.`); + } + } catch (dbError) { + console.error('Warning: Could not check/create database automatically. Ensure it exists.'); + // Proceed anyway, let the main migration handle the connection failure if it's fatal + } finally { + await client.end(); + } + + // Step 2: Apply schema const distSchemaPath = join(__dirname, 'schema.sql'); const srcSchemaPath = resolve(process.cwd(), 'src', 'database', 'schema.sql'); const schemaPath = existsSync(distSchemaPath) ? distSchemaPath : srcSchemaPath; @@ -33,35 +67,4 @@ async function migrate() { } } -// For future: if you need a migration table later, uncomment below -/* -import { MigrationManager } from './migration-manager'; -async function migrateWithTracking() { - // First run base schema if needed - const distSchemaPath = join(__dirname, 'schema.sql'); - const srcSchemaPath = resolve(process.cwd(), 'src', 'database', 'schema.sql'); - const schemaPath = existsSync(distSchemaPath) ? distSchemaPath : srcSchemaPath; - - if (existsSync(schemaPath)) { - const checkResult = await pool.query(` - SELECT EXISTS ( - SELECT FROM information_schema.tables - WHERE table_schema = 'public' - AND table_name = 'users' - ) - `); - if (!checkResult.rows[0].exists) { - console.log('Initializing database schema...'); - const schema = readFileSync(schemaPath, 'utf-8'); - await pool.query(schema); - console.log('✓ Base schema initialized\n'); - } - } - - // Run tracked migrations - const migrationManager = new MigrationManager(); - await migrationManager.migrate(); -} -*/ - migrate(); diff --git a/src/database/schema.sql b/src/database/schema.sql index 0776923..51e9dd4 100644 --- a/src/database/schema.sql +++ b/src/database/schema.sql @@ -245,6 +245,19 @@ CREATE INDEX IF NOT EXISTS idx_job_bids_job ON job_bids(job_id); CREATE INDEX IF NOT EXISTS idx_job_bids_provider ON job_bids(provider_id); CREATE INDEX IF NOT EXISTS idx_job_bids_status ON job_bids(status); +-- Refresh Tokens table +CREATE TABLE IF NOT EXISTS refresh_tokens ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token_hash VARCHAR(255) UNIQUE NOT NULL, + expires_at TIMESTAMP NOT NULL, + revoked BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_refresh_tokens_hash ON refresh_tokens(token_hash); +CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user ON refresh_tokens(user_id); + -- Functions to update updated_at timestamp CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ diff --git a/src/routes/provider.routes.ts b/src/routes/provider.routes.ts index ba20957..15377b0 100644 --- a/src/routes/provider.routes.ts +++ b/src/routes/provider.routes.ts @@ -107,6 +107,79 @@ const router = Router(); */ router.get('/search', ProviderController.search); +/** + * @swagger + * /api/providers/me/profile: + * get: + * summary: Get current user's provider profile (Provider only) + * tags: [Providers] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Provider profile details + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: "success" + * data: + * $ref: '#/components/schemas/Provider' + * 401: + * $ref: '#/components/responses/Unauthorized' + * 403: + * $ref: '#/components/responses/Forbidden' + * 404: + * description: Provider profile not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ +router.get('/me/profile', authenticate, authorize(UserRole.PROVIDER), ProviderController.getMyProfile); + +/** + * @swagger + * /api/providers/me/stats: + * get: + * summary: Get current user's provider statistics (Provider only) + * tags: [Providers] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Provider statistics + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: "success" + * data: + * type: object + * properties: + * jobsDone: + * type: integer + * pendingJobs: + * type: integer + * activeJobs: + * type: integer + * earnings: + * type: number + * averageRating: + * type: number + * 401: + * $ref: '#/components/responses/Unauthorized' + * 403: + * $ref: '#/components/responses/Forbidden' + */ +router.get('/me/stats', authenticate, authorize(UserRole.PROVIDER), ProviderController.getStats); + /** * @swagger * /api/providers/{id}: @@ -340,78 +413,6 @@ router.post( ProviderController.create ); -/** - * @swagger - * /api/providers/me/profile: - * get: - * summary: Get current user's provider profile (Provider only) - * tags: [Providers] - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: Provider profile details - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: "success" - * data: - * $ref: '#/components/schemas/Provider' - * 401: - * $ref: '#/components/responses/Unauthorized' - * 403: - * $ref: '#/components/responses/Forbidden' - * 404: - * description: Provider profile not found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - */ -router.get('/me/profile', authenticate, authorize(UserRole.PROVIDER), ProviderController.getMyProfile); - -/** - * @swagger - * /api/providers/me/stats: - * get: - * summary: Get current user's provider statistics (Provider only) - * tags: [Providers] - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: Provider statistics - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: "success" - * data: - * type: object - * properties: - * jobsDone: - * type: integer - * pendingJobs: - * type: integer - * activeJobs: - * type: integer - * earnings: - * type: number - * averageRating: - * type: number - * 401: - * $ref: '#/components/responses/Unauthorized' - * 403: - * $ref: '#/components/responses/Forbidden' - */ -router.get('/me/stats', authenticate, authorize(UserRole.PROVIDER), ProviderController.getStats); /** * @swagger