Frontend: - Add runtime configuration service for deployment-time API URL injection - Create docker-entrypoint.sh to generate config.json from environment variables - Update ApiService, ApprovalService, and DocumentViewer to use RuntimeConfigService - Add APP_INITIALIZER to load runtime config before app starts Backend: - Fix init-blockchain.js to properly quote mnemonic phrases in .env file - Improve docker-entrypoint.sh with health checks and better error handling Docker: - Add API_BASE_URL environment variable to frontend container - Update docker-compose.yml with clear documentation for remote deployment - Reorganize .env.example with clear categories (REQUIRED FOR REMOTE, PRODUCTION, AUTO-GENERATED) Workflow fixes: - Fix DepartmentApproval interface to match backend schema - Fix stage transformation for 0-indexed stageOrder - Fix workflow list to show correct stage count from definition.stages Cleanup: - Move development artifacts to .trash directory - Remove root-level package.json (was only for utility scripts) - Add .trash/ to .gitignore
283 lines
12 KiB
TypeScript
283 lines
12 KiB
TypeScript
import type { Knex } from 'knex';
|
|
|
|
export async function up(knex: Knex): Promise<void> {
|
|
// Enable UUID extension
|
|
await knex.raw('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"');
|
|
|
|
// Applicants table
|
|
await knex.schema.createTable('applicants', table => {
|
|
table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()'));
|
|
table.string('digilocker_id', 255).notNullable().unique();
|
|
table.string('name', 255).notNullable();
|
|
table.string('email', 255).notNullable();
|
|
table.string('phone', 20);
|
|
table.string('wallet_address', 42);
|
|
table.boolean('is_active').notNullable().defaultTo(true);
|
|
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
|
|
table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now());
|
|
|
|
table.index('digilocker_id', 'idx_applicant_digilocker');
|
|
table.index('email', 'idx_applicant_email');
|
|
});
|
|
|
|
// Departments table
|
|
await knex.schema.createTable('departments', table => {
|
|
table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()'));
|
|
table.string('code', 50).notNullable().unique();
|
|
table.string('name', 255).notNullable();
|
|
table.string('wallet_address', 42).unique();
|
|
table.string('api_key_hash', 255);
|
|
table.string('api_secret_hash', 255);
|
|
table.string('webhook_url', 500);
|
|
table.string('webhook_secret_hash', 255);
|
|
table.boolean('is_active').notNullable().defaultTo(true);
|
|
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
|
|
table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now());
|
|
|
|
table.index('code', 'idx_department_code');
|
|
table.index('is_active', 'idx_department_active');
|
|
});
|
|
|
|
// Workflows table
|
|
await knex.schema.createTable('workflows', table => {
|
|
table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()'));
|
|
table.string('workflow_type', 100).notNullable().unique();
|
|
table.string('name', 255).notNullable();
|
|
table.text('description');
|
|
table.integer('version').notNullable().defaultTo(1);
|
|
table.jsonb('definition').notNullable();
|
|
table.boolean('is_active').notNullable().defaultTo(true);
|
|
table.uuid('created_by');
|
|
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
|
|
table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now());
|
|
|
|
table.index('workflow_type', 'idx_workflow_type');
|
|
table.index('is_active', 'idx_workflow_active');
|
|
});
|
|
|
|
// License Requests table
|
|
await knex.schema.createTable('license_requests', table => {
|
|
table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()'));
|
|
table.string('request_number', 50).notNullable().unique();
|
|
table.bigInteger('token_id');
|
|
table
|
|
.uuid('applicant_id')
|
|
.notNullable()
|
|
.references('id')
|
|
.inTable('applicants')
|
|
.onDelete('CASCADE');
|
|
table.string('request_type', 100).notNullable();
|
|
table.uuid('workflow_id').references('id').inTable('workflows').onDelete('SET NULL');
|
|
table.string('status', 50).notNullable().defaultTo('DRAFT');
|
|
table.jsonb('metadata');
|
|
table.string('current_stage_id', 100);
|
|
table.string('blockchain_tx_hash', 66);
|
|
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
|
|
table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now());
|
|
table.timestamp('submitted_at');
|
|
table.timestamp('approved_at');
|
|
|
|
table.index('request_number', 'idx_request_number');
|
|
table.index('applicant_id', 'idx_request_applicant');
|
|
table.index('status', 'idx_request_status');
|
|
table.index('request_type', 'idx_request_type');
|
|
table.index('created_at', 'idx_request_created');
|
|
table.index(['status', 'request_type'], 'idx_request_status_type');
|
|
});
|
|
|
|
// Documents table
|
|
await knex.schema.createTable('documents', table => {
|
|
table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()'));
|
|
table
|
|
.uuid('request_id')
|
|
.notNullable()
|
|
.references('id')
|
|
.inTable('license_requests')
|
|
.onDelete('CASCADE');
|
|
table.string('doc_type', 100).notNullable();
|
|
table.string('original_filename', 255).notNullable();
|
|
table.integer('current_version').notNullable().defaultTo(1);
|
|
table.string('current_hash', 66).notNullable();
|
|
table.string('minio_bucket', 100).notNullable();
|
|
table.boolean('is_active').notNullable().defaultTo(true);
|
|
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
|
|
table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now());
|
|
|
|
table.index('request_id', 'idx_document_request');
|
|
table.index('doc_type', 'idx_document_type');
|
|
});
|
|
|
|
// Document Versions table
|
|
await knex.schema.createTable('document_versions', table => {
|
|
table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()'));
|
|
table
|
|
.uuid('document_id')
|
|
.notNullable()
|
|
.references('id')
|
|
.inTable('documents')
|
|
.onDelete('CASCADE');
|
|
table.integer('version').notNullable();
|
|
table.string('hash', 66).notNullable();
|
|
table.string('minio_path', 500).notNullable();
|
|
table.bigInteger('file_size').notNullable();
|
|
table.string('mime_type', 100).notNullable();
|
|
table.uuid('uploaded_by').notNullable();
|
|
table.string('blockchain_tx_hash', 66);
|
|
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
|
|
|
|
table.unique(['document_id', 'version'], { indexName: 'uq_document_version' });
|
|
table.index('document_id', 'idx_docversion_document');
|
|
});
|
|
|
|
// Approvals table
|
|
await knex.schema.createTable('approvals', table => {
|
|
table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()'));
|
|
table
|
|
.uuid('request_id')
|
|
.notNullable()
|
|
.references('id')
|
|
.inTable('license_requests')
|
|
.onDelete('CASCADE');
|
|
table
|
|
.uuid('department_id')
|
|
.notNullable()
|
|
.references('id')
|
|
.inTable('departments')
|
|
.onDelete('CASCADE');
|
|
table.string('status', 50).notNullable().defaultTo('PENDING');
|
|
table.text('remarks');
|
|
table.string('remarks_hash', 66);
|
|
table.jsonb('reviewed_documents');
|
|
table.string('blockchain_tx_hash', 66);
|
|
table.boolean('is_active').notNullable().defaultTo(true);
|
|
table.timestamp('invalidated_at');
|
|
table.string('invalidation_reason', 255);
|
|
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
|
|
table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now());
|
|
|
|
table.index('request_id', 'idx_approval_request');
|
|
table.index('department_id', 'idx_approval_department');
|
|
table.index('status', 'idx_approval_status');
|
|
table.index(['request_id', 'department_id'], 'idx_approval_request_dept');
|
|
});
|
|
|
|
// Workflow States table
|
|
await knex.schema.createTable('workflow_states', table => {
|
|
table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()'));
|
|
table
|
|
.uuid('request_id')
|
|
.notNullable()
|
|
.unique()
|
|
.references('id')
|
|
.inTable('license_requests')
|
|
.onDelete('CASCADE');
|
|
table.string('current_stage_id', 100).notNullable();
|
|
table.jsonb('completed_stages').notNullable().defaultTo('[]');
|
|
table.jsonb('pending_approvals').notNullable().defaultTo('[]');
|
|
table.jsonb('execution_log').notNullable().defaultTo('[]');
|
|
table.timestamp('stage_started_at');
|
|
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
|
|
table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now());
|
|
|
|
table.index('request_id', 'idx_wfstate_request');
|
|
});
|
|
|
|
// Webhooks table
|
|
await knex.schema.createTable('webhooks', table => {
|
|
table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()'));
|
|
table
|
|
.uuid('department_id')
|
|
.notNullable()
|
|
.references('id')
|
|
.inTable('departments')
|
|
.onDelete('CASCADE');
|
|
table.string('url', 500).notNullable();
|
|
table.jsonb('events').notNullable();
|
|
table.string('secret_hash', 255).notNullable();
|
|
table.boolean('is_active').notNullable().defaultTo(true);
|
|
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
|
|
table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now());
|
|
|
|
table.index('department_id', 'idx_webhook_department');
|
|
});
|
|
|
|
// Webhook Logs table
|
|
await knex.schema.createTable('webhook_logs', table => {
|
|
table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()'));
|
|
table.uuid('webhook_id').notNullable().references('id').inTable('webhooks').onDelete('CASCADE');
|
|
table.string('event_type', 100).notNullable();
|
|
table.jsonb('payload').notNullable();
|
|
table.integer('response_status');
|
|
table.text('response_body');
|
|
table.integer('response_time');
|
|
table.integer('retry_count').notNullable().defaultTo(0);
|
|
table.string('status', 20).notNullable().defaultTo('PENDING');
|
|
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
|
|
|
|
table.index('webhook_id', 'idx_webhooklog_webhook');
|
|
table.index('event_type', 'idx_webhooklog_event');
|
|
table.index('status', 'idx_webhooklog_status');
|
|
table.index('created_at', 'idx_webhooklog_created');
|
|
});
|
|
|
|
// Audit Logs table
|
|
await knex.schema.createTable('audit_logs', table => {
|
|
table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()'));
|
|
table.string('entity_type', 50).notNullable();
|
|
table.uuid('entity_id').notNullable();
|
|
table.string('action', 50).notNullable();
|
|
table.string('actor_type', 50).notNullable();
|
|
table.uuid('actor_id');
|
|
table.jsonb('old_value');
|
|
table.jsonb('new_value');
|
|
table.string('ip_address', 45);
|
|
table.text('user_agent');
|
|
table.string('correlation_id', 100);
|
|
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
|
|
|
|
table.index(['entity_type', 'entity_id'], 'idx_audit_entity');
|
|
table.index('entity_type', 'idx_audit_entitytype');
|
|
table.index('action', 'idx_audit_action');
|
|
table.index('created_at', 'idx_audit_created');
|
|
table.index('correlation_id', 'idx_audit_correlation');
|
|
});
|
|
|
|
// Blockchain Transactions table
|
|
await knex.schema.createTable('blockchain_transactions', table => {
|
|
table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()'));
|
|
table.string('tx_hash', 66).notNullable().unique();
|
|
table.string('tx_type', 50).notNullable();
|
|
table.string('related_entity_type', 50).notNullable();
|
|
table.uuid('related_entity_id').notNullable();
|
|
table.string('from_address', 42).notNullable();
|
|
table.string('to_address', 42);
|
|
table.string('status', 20).notNullable().defaultTo('PENDING');
|
|
table.bigInteger('block_number');
|
|
table.bigInteger('gas_used');
|
|
table.text('error_message');
|
|
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
|
|
table.timestamp('confirmed_at');
|
|
|
|
table.index('tx_hash', 'idx_bctx_hash');
|
|
table.index('tx_type', 'idx_bctx_type');
|
|
table.index('status', 'idx_bctx_status');
|
|
table.index('related_entity_id', 'idx_bctx_entity');
|
|
table.index('created_at', 'idx_bctx_created');
|
|
});
|
|
}
|
|
|
|
export async function down(knex: Knex): Promise<void> {
|
|
await knex.schema.dropTableIfExists('blockchain_transactions');
|
|
await knex.schema.dropTableIfExists('audit_logs');
|
|
await knex.schema.dropTableIfExists('webhook_logs');
|
|
await knex.schema.dropTableIfExists('webhooks');
|
|
await knex.schema.dropTableIfExists('workflow_states');
|
|
await knex.schema.dropTableIfExists('approvals');
|
|
await knex.schema.dropTableIfExists('document_versions');
|
|
await knex.schema.dropTableIfExists('documents');
|
|
await knex.schema.dropTableIfExists('license_requests');
|
|
await knex.schema.dropTableIfExists('workflows');
|
|
await knex.schema.dropTableIfExists('departments');
|
|
await knex.schema.dropTableIfExists('applicants');
|
|
}
|