Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ COPY --from=builder /app/dist ./dist
EXPOSE 3000

# Start server
CMD ["node", "dist/server.js"]
CMD ["node", "dist/index.js"]
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -66,4 +67,4 @@
"tsx": "^4.7.0",
"typescript": "^5.3.3"
}
}
}
65 changes: 34 additions & 31 deletions src/database/migrate.ts
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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.`);
Comment on lines +18 to +37
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Unsanitized create database name 🐞 Bug ⛨ Security

src/database/migrate.ts interpolates config.database.name (DB_NAME) directly into a CREATE DATABASE
statement without identifier escaping/validation, so a name containing a double-quote can break the
SQL and potentially append unintended DDL. This can cause migrations to fail or execute unexpected
statements on the admin connection used for DB creation.
Agent Prompt
### Issue description
`src/database/migrate.ts` constructs `CREATE DATABASE "${dbName}"` using `dbName` from `process.env.DB_NAME` (via `config.database.name`) without validating it as a safe PostgreSQL identifier or escaping embedded quotes. This can break the statement and can enable DDL injection in the migration step.

### Issue Context
PostgreSQL DDL like `CREATE DATABASE` can’t be parameterized like normal value parameters, so the fix should be identifier validation/escaping (not `$1` parameters).

### Fix Focus Areas
- src/database/migrate.ts[17-37]
- src/config/config.ts[78-85]

### What to change
- Add strict validation for `dbName` (e.g., allow only `[A-Za-z_][A-Za-z0-9_]*`), and fail fast with a clear error if invalid.
- Alternatively (or additionally), implement proper identifier escaping (double any embedded `"` to `""`) and still validate length/charset.
- Keep the `SELECT 1 FROM pg_database WHERE datname = $1` check as-is (it’s already parameterized).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

} 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;
Expand All @@ -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();
13 changes: 13 additions & 0 deletions src/database/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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 $$
Expand Down
145 changes: 73 additions & 72 deletions src/routes/provider.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}:
Expand Down Expand Up @@ -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
Expand Down