feat: Goa GEL Blockchain e-Licensing Platform - Full Stack Implementation

Complete implementation of the Goa Government e-Licensing platform with:

Backend:
- NestJS API with JWT authentication
- PostgreSQL database with Knex ORM
- Redis caching and session management
- MinIO document storage
- Hyperledger Besu blockchain integration
- Multi-department workflow system
- Comprehensive API tests (266/282 passing)

Frontend:
- Angular 21 with standalone components
- Angular Material + TailwindCSS UI
- Visual workflow builder
- Document upload with progress tracking
- Blockchain explorer integration
- Role-based dashboards (Admin, Department, Citizen)
- E2E tests with Playwright (37 tests)

Infrastructure:
- Docker Compose orchestration
- Blockscout blockchain explorer
- Development and production configurations
This commit is contained in:
Mahi
2026-02-07 10:23:29 -04:00
commit 80566bf0a2
441 changed files with 102418 additions and 0 deletions

View File

@@ -0,0 +1,304 @@
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- =============================================
-- MIGRATION 1: Initial Schema
-- =============================================
-- Applicants table
CREATE TABLE IF NOT EXISTS applicants (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
digilocker_id VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
phone VARCHAR(20),
wallet_address VARCHAR(42),
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_applicant_digilocker ON applicants(digilocker_id);
CREATE INDEX IF NOT EXISTS idx_applicant_email ON applicants(email);
-- Departments table
CREATE TABLE IF NOT EXISTS departments (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
code VARCHAR(50) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
wallet_address VARCHAR(42) UNIQUE,
api_key_hash VARCHAR(255),
api_secret_hash VARCHAR(255),
webhook_url VARCHAR(500),
webhook_secret_hash VARCHAR(255),
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
description TEXT,
contact_email VARCHAR(255),
contact_phone VARCHAR(20),
last_webhook_at TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_department_code ON departments(code);
CREATE INDEX IF NOT EXISTS idx_department_active ON departments(is_active);
-- Workflows table
CREATE TABLE IF NOT EXISTS workflows (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
workflow_type VARCHAR(100) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
description TEXT,
version INTEGER NOT NULL DEFAULT 1,
definition JSONB NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
created_by UUID,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_workflow_type ON workflows(workflow_type);
CREATE INDEX IF NOT EXISTS idx_workflow_active ON workflows(is_active);
-- License Requests table
CREATE TABLE IF NOT EXISTS license_requests (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
request_number VARCHAR(50) NOT NULL UNIQUE,
token_id BIGINT,
applicant_id UUID NOT NULL REFERENCES applicants(id) ON DELETE CASCADE,
request_type VARCHAR(100) NOT NULL,
workflow_id UUID REFERENCES workflows(id) ON DELETE SET NULL,
status VARCHAR(50) NOT NULL DEFAULT 'DRAFT',
metadata JSONB,
current_stage_id VARCHAR(100),
blockchain_tx_hash VARCHAR(66),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
submitted_at TIMESTAMP,
approved_at TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_request_number ON license_requests(request_number);
CREATE INDEX IF NOT EXISTS idx_request_applicant ON license_requests(applicant_id);
CREATE INDEX IF NOT EXISTS idx_request_status ON license_requests(status);
CREATE INDEX IF NOT EXISTS idx_request_type ON license_requests(request_type);
CREATE INDEX IF NOT EXISTS idx_request_created ON license_requests(created_at);
CREATE INDEX IF NOT EXISTS idx_request_status_type ON license_requests(status, request_type);
-- Documents table
CREATE TABLE IF NOT EXISTS documents (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
request_id UUID NOT NULL REFERENCES license_requests(id) ON DELETE CASCADE,
doc_type VARCHAR(100) NOT NULL,
original_filename VARCHAR(255) NOT NULL,
current_version INTEGER NOT NULL DEFAULT 1,
current_hash VARCHAR(66) NOT NULL,
minio_bucket VARCHAR(100) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_document_request ON documents(request_id);
CREATE INDEX IF NOT EXISTS idx_document_type ON documents(doc_type);
-- Document Versions table
CREATE TABLE IF NOT EXISTS document_versions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
version INTEGER NOT NULL,
hash VARCHAR(66) NOT NULL,
minio_path VARCHAR(500) NOT NULL,
file_size BIGINT NOT NULL,
mime_type VARCHAR(100) NOT NULL,
uploaded_by UUID NOT NULL,
blockchain_tx_hash VARCHAR(66),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(document_id, version)
);
CREATE INDEX IF NOT EXISTS idx_docversion_document ON document_versions(document_id);
-- Approvals table
CREATE TABLE IF NOT EXISTS approvals (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
request_id UUID NOT NULL REFERENCES license_requests(id) ON DELETE CASCADE,
department_id UUID NOT NULL REFERENCES departments(id) ON DELETE CASCADE,
status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
remarks TEXT,
remarks_hash VARCHAR(66),
reviewed_documents JSONB,
blockchain_tx_hash VARCHAR(66),
is_active BOOLEAN NOT NULL DEFAULT true,
invalidated_at TIMESTAMP,
invalidation_reason VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_approval_request ON approvals(request_id);
CREATE INDEX IF NOT EXISTS idx_approval_department ON approvals(department_id);
CREATE INDEX IF NOT EXISTS idx_approval_status ON approvals(status);
CREATE INDEX IF NOT EXISTS idx_approval_request_dept ON approvals(request_id, department_id);
-- Workflow States table
CREATE TABLE IF NOT EXISTS workflow_states (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
request_id UUID NOT NULL UNIQUE REFERENCES license_requests(id) ON DELETE CASCADE,
current_stage_id VARCHAR(100) NOT NULL,
completed_stages JSONB NOT NULL DEFAULT '[]',
pending_approvals JSONB NOT NULL DEFAULT '[]',
execution_log JSONB NOT NULL DEFAULT '[]',
stage_started_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_wfstate_request ON workflow_states(request_id);
-- Webhooks table
CREATE TABLE IF NOT EXISTS webhooks (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
department_id UUID NOT NULL REFERENCES departments(id) ON DELETE CASCADE,
url VARCHAR(500) NOT NULL,
events JSONB NOT NULL,
secret_hash VARCHAR(255) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_webhook_department ON webhooks(department_id);
-- Webhook Logs table
CREATE TABLE IF NOT EXISTS webhook_logs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
webhook_id UUID NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
event_type VARCHAR(100) NOT NULL,
payload JSONB NOT NULL,
response_status INTEGER,
response_body TEXT,
response_time INTEGER,
retry_count INTEGER NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_webhooklog_webhook ON webhook_logs(webhook_id);
CREATE INDEX IF NOT EXISTS idx_webhooklog_event ON webhook_logs(event_type);
CREATE INDEX IF NOT EXISTS idx_webhooklog_status ON webhook_logs(status);
CREATE INDEX IF NOT EXISTS idx_webhooklog_created ON webhook_logs(created_at);
-- Audit Logs table
CREATE TABLE IF NOT EXISTS audit_logs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
entity_type VARCHAR(50) NOT NULL,
entity_id UUID NOT NULL,
action VARCHAR(50) NOT NULL,
actor_type VARCHAR(50) NOT NULL,
actor_id UUID,
old_value JSONB,
new_value JSONB,
ip_address VARCHAR(45),
user_agent TEXT,
correlation_id VARCHAR(100),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_audit_entity ON audit_logs(entity_type, entity_id);
CREATE INDEX IF NOT EXISTS idx_audit_entitytype ON audit_logs(entity_type);
CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_logs(action);
CREATE INDEX IF NOT EXISTS idx_audit_created ON audit_logs(created_at);
CREATE INDEX IF NOT EXISTS idx_audit_correlation ON audit_logs(correlation_id);
-- Blockchain Transactions table
CREATE TABLE IF NOT EXISTS blockchain_transactions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tx_hash VARCHAR(66) NOT NULL UNIQUE,
tx_type VARCHAR(50) NOT NULL,
related_entity_type VARCHAR(50) NOT NULL,
related_entity_id UUID NOT NULL,
from_address VARCHAR(42) NOT NULL,
to_address VARCHAR(42),
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
block_number BIGINT,
gas_used BIGINT,
error_message TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
confirmed_at TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_bctx_hash ON blockchain_transactions(tx_hash);
CREATE INDEX IF NOT EXISTS idx_bctx_type ON blockchain_transactions(tx_type);
CREATE INDEX IF NOT EXISTS idx_bctx_status ON blockchain_transactions(status);
CREATE INDEX IF NOT EXISTS idx_bctx_entity ON blockchain_transactions(related_entity_id);
CREATE INDEX IF NOT EXISTS idx_bctx_created ON blockchain_transactions(created_at);
-- =============================================
-- MIGRATION 2: Users, Wallets, Events, Logs
-- =============================================
-- Users table for email/password authentication
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
role VARCHAR(20) NOT NULL CHECK (role IN ('ADMIN', 'DEPARTMENT', 'CITIZEN')),
department_id UUID REFERENCES departments(id) ON DELETE SET NULL,
wallet_address VARCHAR(42),
wallet_encrypted_key TEXT,
phone VARCHAR(20),
is_active BOOLEAN NOT NULL DEFAULT true,
last_login_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_user_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_user_role ON users(role);
CREATE INDEX IF NOT EXISTS idx_user_department ON users(department_id);
CREATE INDEX IF NOT EXISTS idx_user_active ON users(is_active);
-- Wallets table for storing encrypted private keys
CREATE TABLE IF NOT EXISTS wallets (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
address VARCHAR(42) NOT NULL UNIQUE,
encrypted_private_key TEXT NOT NULL,
owner_type VARCHAR(20) NOT NULL CHECK (owner_type IN ('USER', 'DEPARTMENT')),
owner_id UUID NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_wallet_address ON wallets(address);
CREATE INDEX IF NOT EXISTS idx_wallet_owner ON wallets(owner_type, owner_id);
-- Blockchain events table
CREATE TABLE IF NOT EXISTS blockchain_events (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tx_hash VARCHAR(66) NOT NULL,
event_name VARCHAR(100) NOT NULL,
contract_address VARCHAR(42) NOT NULL,
block_number BIGINT NOT NULL,
log_index INTEGER NOT NULL,
args JSONB NOT NULL,
decoded_args JSONB,
related_entity_type VARCHAR(50),
related_entity_id UUID,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(tx_hash, log_index)
);
CREATE INDEX IF NOT EXISTS idx_event_tx ON blockchain_events(tx_hash);
CREATE INDEX IF NOT EXISTS idx_event_name ON blockchain_events(event_name);
CREATE INDEX IF NOT EXISTS idx_event_contract ON blockchain_events(contract_address);
CREATE INDEX IF NOT EXISTS idx_event_block ON blockchain_events(block_number);
CREATE INDEX IF NOT EXISTS idx_event_created ON blockchain_events(created_at);
CREATE INDEX IF NOT EXISTS idx_event_entity ON blockchain_events(related_entity_type, related_entity_id);
-- Application logs table
CREATE TABLE IF NOT EXISTS application_logs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
level VARCHAR(10) NOT NULL CHECK (level IN ('DEBUG', 'INFO', 'WARN', 'ERROR')),
module VARCHAR(100) NOT NULL,
message TEXT NOT NULL,
context JSONB,
stack_trace TEXT,
user_id UUID,
correlation_id VARCHAR(100),
ip_address VARCHAR(45),
user_agent TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_applog_level ON application_logs(level);
CREATE INDEX IF NOT EXISTS idx_applog_module ON application_logs(module);
CREATE INDEX IF NOT EXISTS idx_applog_user ON application_logs(user_id);
CREATE INDEX IF NOT EXISTS idx_applog_correlation ON application_logs(correlation_id);
CREATE INDEX IF NOT EXISTS idx_applog_created ON application_logs(created_at);

View File

@@ -0,0 +1,45 @@
#!/bin/bash
set -e
echo "🚀 Starting Goa-GEL Backend Initialization..."
# Function to check if this is first boot
is_first_boot() {
if [ ! -f "/app/data/.initialized" ]; then
return 0 # true
else
return 1 # false
fi
}
# Ensure data directory exists
mkdir -p /app/data
# Ensure .env file exists
touch /app/.env
# 1. Wait for and initialize database
echo "📊 Step 1: Database initialization..."
chmod +x /app/scripts/init-db.sh
/app/scripts/init-db.sh
# 2. Initialize blockchain (only on first boot or if not configured)
if is_first_boot || [ -z "$CONTRACT_ADDRESS_LICENSE_NFT" ] || [ "$CONTRACT_ADDRESS_LICENSE_NFT" = "0x0000000000000000000000000000000000000000" ]; then
echo "🔗 Step 2: Blockchain initialization..."
node /app/scripts/init-blockchain.js
# Mark as initialized
touch /app/data/.initialized
echo "✅ Blockchain initialization complete!"
# Reload environment variables
if [ -f "/app/.env" ]; then
export $(grep -v '^#' /app/.env | xargs)
fi
else
echo "⏭️ Step 2: Blockchain already initialized"
fi
# 3. Start the application
echo "🎯 Step 3: Starting NestJS application..."
exec npm run start:prod

View File

@@ -0,0 +1,175 @@
const { ethers } = require('ethers');
const fs = require('fs');
const path = require('path');
/**
* Initialize blockchain infrastructure:
* - Generate platform wallet
* - Deploy smart contracts
* - Update .env file with addresses
*/
async function initBlockchain() {
console.log('🔗 Initializing blockchain infrastructure...');
const provider = new ethers.JsonRpcProvider(
process.env.BESU_RPC_URL || 'http://localhost:8545'
);
// Wait for blockchain to be ready
console.log('⏳ Waiting for blockchain to be ready...');
let retries = 30;
while (retries > 0) {
try {
await provider.getBlockNumber();
console.log('✅ Blockchain is ready!');
break;
} catch (error) {
retries--;
if (retries === 0) {
throw new Error('Blockchain not available after 30 retries');
}
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
// Check if already initialized
const envPath = path.join(__dirname, '../.env');
if (fs.existsSync(envPath)) {
const envContent = fs.readFileSync(envPath, 'utf8');
if (
envContent.includes('CONTRACT_ADDRESS_LICENSE_NFT=0x') &&
!envContent.includes('CONTRACT_ADDRESS_LICENSE_NFT=0x0000000000000000000000000000000000000000')
) {
console.log('✅ Blockchain already initialized, skipping deployment');
return;
}
}
// 1. Generate Platform Wallet
console.log('🔐 Generating platform wallet...');
const platformWallet = ethers.Wallet.createRandom();
console.log('📝 Platform Wallet Address:', platformWallet.address);
console.log('🔑 Platform Wallet Mnemonic:', platformWallet.mnemonic.phrase);
// Fund the platform wallet from the dev network's pre-funded account
console.log('💰 Funding platform wallet...');
const devWallet = new ethers.Wallet(
'0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63',
provider
);
const fundTx = await devWallet.sendTransaction({
to: platformWallet.address,
value: ethers.parseEther('100.0'),
});
await fundTx.wait();
console.log('✅ Platform wallet funded with 100 ETH');
const connectedWallet = platformWallet.connect(provider);
// 2. Deploy Smart Contracts
console.log('📜 Deploying smart contracts...');
const contracts = await deployContracts(connectedWallet);
// 3. Update .env file
console.log('📝 Updating .env file...');
updateEnvFile({
PLATFORM_WALLET_PRIVATE_KEY: platformWallet.privateKey,
PLATFORM_WALLET_ADDRESS: platformWallet.address,
PLATFORM_WALLET_MNEMONIC: platformWallet.mnemonic.phrase,
CONTRACT_ADDRESS_LICENSE_NFT: contracts.licenseNFT,
CONTRACT_ADDRESS_APPROVAL_MANAGER: contracts.approvalManager,
CONTRACT_ADDRESS_DEPARTMENT_REGISTRY: contracts.departmentRegistry,
CONTRACT_ADDRESS_WORKFLOW_REGISTRY: contracts.workflowRegistry,
});
console.log('✅ Blockchain initialization complete!');
console.log('\n📋 Summary:');
console.log(' Platform Wallet:', platformWallet.address);
console.log(' License NFT:', contracts.licenseNFT);
console.log(' Approval Manager:', contracts.approvalManager);
console.log(' Department Registry:', contracts.departmentRegistry);
console.log(' Workflow Registry:', contracts.workflowRegistry);
}
/**
* Deploy all smart contracts
*/
async function deployContracts(wallet) {
// Simple deployment of placeholder contracts
// In production, you would deploy your actual Solidity contracts here
console.log('🚀 Deploying License NFT contract...');
const licenseNFT = await deployPlaceholderContract(wallet, 'LicenseNFT');
console.log('🚀 Deploying Approval Manager contract...');
const approvalManager = await deployPlaceholderContract(wallet, 'ApprovalManager');
console.log('🚀 Deploying Department Registry contract...');
const departmentRegistry = await deployPlaceholderContract(wallet, 'DepartmentRegistry');
console.log('🚀 Deploying Workflow Registry contract...');
const workflowRegistry = await deployPlaceholderContract(wallet, 'WorkflowRegistry');
return {
licenseNFT,
approvalManager,
departmentRegistry,
workflowRegistry,
};
}
/**
* Deploy a placeholder contract (simple storage contract)
*/
async function deployPlaceholderContract(wallet, name) {
// Simple contract that just stores a value
const bytecode = '0x608060405234801561001057600080fd5b5060c78061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060325760003560e01c80632e64cec11460375780636057361d146051575b600080fd5b603d6069565b6040516048919060a2565b60405180910390f35b6067600480360381019060639190606f565b6072565b005b60008054905090565b8060008190555050565b6000813590506079816000ad565b92915050565b6000602082840312156000608257600080fd5b6000608e84828501607c565b91505092915050565b609c8160bb565b82525050565b600060208201905060b560008301846095565b92915050565b600081905091905056fea26469706673582212203a8e2f9c8e98b9f5e8c7d6e5f4c3b2a19087868756463524f3e2d1c0b9a8f76464736f6c63430008110033';
const deployTx = await wallet.sendTransaction({
data: bytecode,
});
const receipt = await deployTx.wait();
const address = receipt.contractAddress;
console.log(`${name} deployed at:`, address);
return address;
}
/**
* Update .env file with generated values
*/
function updateEnvFile(values) {
const envPath = path.join(__dirname, '../.env');
let envContent = '';
if (fs.existsSync(envPath)) {
envContent = fs.readFileSync(envPath, 'utf8');
}
// Update or add each value
for (const [key, value] of Object.entries(values)) {
const regex = new RegExp(`^${key}=.*$`, 'm');
if (regex.test(envContent)) {
envContent = envContent.replace(regex, `${key}=${value}`);
} else {
envContent += `\n${key}=${value}`;
}
}
fs.writeFileSync(envPath, envContent.trim() + '\n');
console.log(`✅ Updated ${envPath}`);
}
// Run initialization
initBlockchain()
.then(() => {
console.log('✅ Blockchain initialization completed successfully!');
process.exit(0);
})
.catch((error) => {
console.error('❌ Blockchain initialization failed:', error);
process.exit(1);
});

View File

@@ -0,0 +1,30 @@
#!/bin/bash
set -e
echo "🔄 Waiting for database to be ready..."
until PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -U "$DATABASE_USER" -d "$DATABASE_NAME" -c '\q' 2>/dev/null; do
echo "⏳ PostgreSQL is unavailable - sleeping"
sleep 2
done
echo "✅ PostgreSQL is up - checking if database is initialized..."
# Check if users table exists (indicating database is already set up)
TABLE_EXISTS=$(PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -U "$DATABASE_USER" -d "$DATABASE_NAME" -tAc "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'users');" 2>/dev/null || echo "f")
if [ "$TABLE_EXISTS" = "t" ]; then
echo "✅ Database already initialized, skipping setup."
else
echo "📦 First time setup - creating tables and seeding data..."
# Run the SQL scripts directly
echo "Creating tables..."
PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -U "$DATABASE_USER" -d "$DATABASE_NAME" -f /app/scripts/create-all-tables.sql
echo "🌱 Seeding initial data..."
PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -U "$DATABASE_USER" -d "$DATABASE_NAME" -f /app/scripts/seed-initial-data.sql
echo "✅ Database initialized successfully!"
fi
echo "✅ Database ready!"

View File

@@ -0,0 +1,45 @@
const knex = require('knex');
const path = require('path');
// Load environment variables
require('dotenv').config({ path: path.join(__dirname, '../.env') });
async function runMigrations() {
const knexConfig = {
client: 'pg',
connection: {
host: process.env.DATABASE_HOST,
port: parseInt(process.env.DATABASE_PORT || '5432', 10),
database: process.env.DATABASE_NAME,
user: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
},
migrations: {
directory: path.join(__dirname, '../src/database/migrations'),
tableName: 'knex_migrations',
loadExtensions: ['.ts'],
},
};
const db = knex(knexConfig);
try {
console.log('🔄 Running database migrations...');
await db.migrate.latest();
console.log('✅ Migrations completed successfully!');
console.log('🌱 Running database seeds...');
await db.seed.run({
directory: path.join(__dirname, '../src/database/seeds'),
loadExtensions: ['.ts'],
});
console.log('✅ Seeds completed successfully!');
} catch (error) {
console.error('❌ Migration failed:', error);
process.exit(1);
} finally {
await db.destroy();
}
}
runMigrations();

View File

@@ -0,0 +1,208 @@
-- =============================================
-- Initial Seed Data for Goa GEL Platform
-- =============================================
-- Insert Departments
INSERT INTO departments (id, code, name, wallet_address, is_active, description, contact_email, contact_phone, created_at, updated_at)
VALUES
(
'11111111-1111-1111-1111-111111111111',
'FIRE_DEPT',
'Fire & Emergency Services Department',
'0x1111111111111111111111111111111111111111',
true,
'Responsible for fire safety inspections and certifications',
'fire@goa.gov.in',
'+91-832-2222222',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
),
(
'22222222-2222-2222-2222-222222222222',
'TOURISM_DEPT',
'Department of Tourism',
'0x2222222222222222222222222222222222222222',
true,
'Manages tourism licenses and hospitality registrations',
'tourism@goa.gov.in',
'+91-832-3333333',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
),
(
'33333333-3333-3333-3333-333333333333',
'MUNICIPALITY',
'Municipal Corporation of Panaji',
'0x3333333333333333333333333333333333333333',
true,
'Local governance and building permits',
'municipality@goa.gov.in',
'+91-832-4444444',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
),
(
'44444444-4444-4444-4444-444444444444',
'HEALTH_DEPT',
'Directorate of Health Services',
'0x4444444444444444444444444444444444444444',
true,
'Health and sanitation inspections',
'health@goa.gov.in',
'+91-832-5555555',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
)
ON CONFLICT (code) DO NOTHING;
-- Insert Demo Users
-- Password hashes are for: Admin@123, Fire@123, Tourism@123, Municipality@123, Citizen@123
INSERT INTO users (id, email, password_hash, name, role, department_id, phone, is_active, created_at, updated_at)
VALUES
(
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'admin@goa.gov.in',
'$2b$10$uTkObgkUNJSVLb0ESwSQqekO4wKJJvjC02VdEb38vxzRT9ib4ByM.',
'System Administrator',
'ADMIN',
NULL,
'+91-9876543210',
true,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
),
(
'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
'fire@goa.gov.in',
'$2b$10$YB1iB3GjHfTwtaULRxSoRudg2eUft4b40V/1YI1iDK8OeAel7OXby',
'Fire Department Officer',
'DEPARTMENT',
'11111111-1111-1111-1111-111111111111',
'+91-9876543211',
true,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
),
(
'cccccccc-cccc-cccc-cccc-cccccccccccc',
'tourism@goa.gov.in',
'$2b$10$MwcPrX91SxlZN09eQxEA4u6ErLOnw7DmrD2f3C7pzEY0pbKRJ.p.e',
'Tourism Department Officer',
'DEPARTMENT',
'22222222-2222-2222-2222-222222222222',
'+91-9876543212',
true,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
),
(
'dddddddd-dddd-dddd-dddd-dddddddddddd',
'municipality@goa.gov.in',
'$2b$10$K4RH4xbduaGQRYMHJeXA3.7Z1eBnBTSDkOQgDLmYVWIUeYFKjp5xm',
'Municipality Officer',
'DEPARTMENT',
'33333333-3333-3333-3333-333333333333',
'+91-9876543213',
true,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
),
(
'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee',
'citizen@example.com',
'$2b$10$94al.IXYDxN6yNIycR4yI.soU00DqS3BwNBXvrLr4v6bB7B94oH6G',
'Demo Citizen',
'CITIZEN',
NULL,
'+91-9876543214',
true,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
),
(
'ffffffff-ffff-ffff-ffff-ffffffffffff',
'citizen2@example.com',
'$2b$10$94al.IXYDxN6yNIycR4yI.soU00DqS3BwNBXvrLr4v6bB7B94oH6G',
'Second Citizen',
'CITIZEN',
NULL,
'+91-9876543215',
true,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
)
ON CONFLICT (email) DO NOTHING;
-- Insert Sample Applicants (linked to citizen users)
INSERT INTO applicants (id, digilocker_id, name, email, phone, is_active, created_at, updated_at)
VALUES
(
'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee',
'DL-GOA-CITIZEN-001',
'Demo Citizen',
'citizen@example.com',
'+91-9876543214',
true,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
),
(
'ffffffff-ffff-ffff-ffff-ffffffffffff',
'DL-GOA-CITIZEN-002',
'Second Citizen',
'citizen2@example.com',
'+91-9876543215',
true,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
)
ON CONFLICT (digilocker_id) DO NOTHING;
-- Insert Sample Workflows
INSERT INTO workflows (id, workflow_type, name, description, version, definition, is_active, created_at, updated_at)
VALUES
(
'ffffffff-ffff-ffff-ffff-ffffffffffff',
'RESORT_LICENSE',
'Resort License Approval Workflow',
'Multi-department approval workflow for resort licenses in Goa',
1,
'{"isActive":true,"stages":[{"stageId":"stage_1_fire","stageName":"Fire Safety Review","stageOrder":1,"executionType":"SEQUENTIAL","requiredApprovals":[{"departmentCode":"FIRE_DEPT","departmentName":"Fire & Emergency Services Department","requiredDocuments":["FIRE_SAFETY_CERTIFICATE","BUILDING_PLAN"],"isMandatory":true}],"completionCriteria":"ALL","timeoutDays":7,"onTimeout":"NOTIFY","onRejection":"FAIL_REQUEST"},{"stageId":"stage_2_parallel","stageName":"Tourism & Municipality Review","stageOrder":2,"executionType":"PARALLEL","requiredApprovals":[{"departmentCode":"TOURISM_DEPT","departmentName":"Department of Tourism","requiredDocuments":["PROPERTY_OWNERSHIP","BUILDING_PLAN"],"isMandatory":true},{"departmentCode":"MUNICIPALITY","departmentName":"Municipal Corporation of Panaji","requiredDocuments":["PROPERTY_OWNERSHIP","TAX_CLEARANCE"],"isMandatory":true}],"completionCriteria":"ALL","timeoutDays":14,"onTimeout":"ESCALATE","onRejection":"FAIL_REQUEST"},{"stageId":"stage_3_health","stageName":"Health & Sanitation Review","stageOrder":3,"executionType":"SEQUENTIAL","requiredApprovals":[{"departmentCode":"HEALTH_DEPT","departmentName":"Directorate of Health Services","requiredDocuments":["HEALTH_CERTIFICATE"],"isMandatory":true}],"completionCriteria":"ALL","timeoutDays":7,"onTimeout":"NOTIFY","onRejection":"FAIL_REQUEST"}]}',
true,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
),
(
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'FIRE_SAFETY_CERT',
'Fire Safety Certificate Workflow',
'Workflow for fire safety certification',
1,
'{"isActive":true,"stages":[{"stageId":"stage_1","stageName":"Fire Safety Review","stageOrder":1,"executionType":"SEQUENTIAL","requiredApprovals":[{"departmentCode":"FIRE_DEPT","departmentName":"Fire & Emergency Services Department","requiredDocuments":["FIRE_SAFETY_CERTIFICATE","BUILDING_PLAN"],"isMandatory":true}],"completionCriteria":"ALL","timeoutDays":7,"onTimeout":"NOTIFY","onRejection":"FAIL_REQUEST"}]}',
true,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
),
(
'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
'TOURISM_LICENSE',
'Tourism License Workflow',
'Workflow for tourism business licenses',
1,
'{"isActive":true,"stages":[{"stageId":"stage_1","stageName":"Tourism Department Review","stageOrder":1,"executionType":"SEQUENTIAL","requiredApprovals":[{"departmentCode":"TOURISM_DEPT","departmentName":"Department of Tourism","requiredDocuments":["PROPERTY_OWNERSHIP"],"isMandatory":true}],"completionCriteria":"ALL","timeoutDays":14,"onTimeout":"NOTIFY","onRejection":"FAIL_REQUEST"}]}',
true,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
),
(
'cccccccc-cccc-cccc-cccc-cccccccccccc',
'TRADE_LICENSE',
'Trade License Workflow',
'Workflow for trade and business licenses',
1,
'{"isActive":true,"stages":[{"stageId":"stage_1","stageName":"Municipality Review","stageOrder":1,"executionType":"SEQUENTIAL","requiredApprovals":[{"departmentCode":"MUNICIPALITY","departmentName":"Municipal Corporation","requiredDocuments":["PROPERTY_OWNERSHIP","TAX_CLEARANCE"],"isMandatory":true}],"completionCriteria":"ALL","timeoutDays":14,"onTimeout":"NOTIFY","onRejection":"FAIL_REQUEST"}]}',
true,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
)
ON CONFLICT (workflow_type) DO NOTHING;