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:
304
backend/scripts/create-all-tables.sql
Normal file
304
backend/scripts/create-all-tables.sql
Normal 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);
|
||||
45
backend/scripts/docker-entrypoint.sh
Normal file
45
backend/scripts/docker-entrypoint.sh
Normal 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
|
||||
175
backend/scripts/init-blockchain.js
Normal file
175
backend/scripts/init-blockchain.js
Normal 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);
|
||||
});
|
||||
30
backend/scripts/init-db.sh
Normal file
30
backend/scripts/init-db.sh
Normal 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!"
|
||||
45
backend/scripts/run-migrations.js
Normal file
45
backend/scripts/run-migrations.js
Normal 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();
|
||||
208
backend/scripts/seed-initial-data.sql
Normal file
208
backend/scripts/seed-initial-data.sql
Normal 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;
|
||||
Reference in New Issue
Block a user