702 lines
34 KiB
JavaScript
702 lines
34 KiB
JavaScript
const mysql = require('mysql2/promise');
|
||
require('dotenv').config();
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const NODE_ENV = process.env.NODE_ENV || 'development';
|
||
|
||
const getSSLConfig = () => {
|
||
const useSSL = String(process.env.DB_SSL || '').toLowerCase() === 'true';
|
||
const caPath = process.env.DB_SSL_CA_PATH;
|
||
if (!useSSL) return undefined;
|
||
|
||
try {
|
||
if (caPath) {
|
||
const resolved = path.resolve(caPath);
|
||
if (fs.existsSync(resolved)) {
|
||
console.log('🔐 (createDb) Loading DB CA certificate from:', resolved);
|
||
return {
|
||
ca: fs.readFileSync(resolved),
|
||
rejectUnauthorized: false
|
||
};
|
||
} else {
|
||
console.warn('⚠️ (createDb) CA file not found at:', resolved, '- proceeding with rejectUnauthorized:false');
|
||
}
|
||
} else {
|
||
console.warn('⚠️ (createDb) DB_SSL_CA_PATH not set - proceeding with rejectUnauthorized:false');
|
||
}
|
||
} catch (e) {
|
||
console.warn('⚠️ (createDb) Failed to load CA file:', e.message, '- proceeding with rejectUnauthorized:false');
|
||
}
|
||
return { rejectUnauthorized: false };
|
||
};
|
||
|
||
let dbConfig;
|
||
if (NODE_ENV === 'development') {
|
||
dbConfig = {
|
||
host: process.env.DEV_DB_HOST || 'localhost',
|
||
port: Number(process.env.DEV_DB_PORT) || 3306,
|
||
user: process.env.DEV_DB_USER || 'root',
|
||
password: process.env.DEV_DB_PASSWORD || '', // XAMPP default: no password
|
||
database: process.env.DEV_DB_NAME || 'profitplanet_centralserver',
|
||
ssl: undefined
|
||
};
|
||
} else {
|
||
dbConfig = {
|
||
host: process.env.DB_HOST,
|
||
port: Number(process.env.DB_PORT) || 3306,
|
||
user: process.env.DB_USER,
|
||
password: process.env.DB_PASSWORD,
|
||
database: process.env.DB_NAME,
|
||
ssl: getSSLConfig()
|
||
};
|
||
}
|
||
|
||
const allowCreateDb = String(process.env.DB_ALLOW_CREATE_DB || 'false').toLowerCase() === 'true';
|
||
|
||
// --- Performance Helpers (added) ---
|
||
async function ensureIndex(conn, table, indexName, indexDDL) {
|
||
const [rows] = await conn.query(`SHOW INDEX FROM \`${table}\` WHERE Key_name = ?`, [indexName]);
|
||
if (!rows.length) {
|
||
await conn.query(`CREATE INDEX \`${indexName}\` ON \`${table}\` (${indexDDL})`);
|
||
console.log(`🆕 Created index ${indexName} ON ${table}`);
|
||
} else {
|
||
console.log(`ℹ️ Index ${indexName} already exists on ${table}`);
|
||
}
|
||
}
|
||
|
||
async function createDatabase() {
|
||
console.log('🚀 Starting MySQL database initialization...');
|
||
console.log('📍 Database host:', process.env.DB_HOST);
|
||
console.log('📍 Database name:', process.env.DB_NAME);
|
||
|
||
let connection;
|
||
try {
|
||
if (allowCreateDb) {
|
||
// Connect without specifying a database to create it if it doesn't exist
|
||
connection = await mysql.createConnection({
|
||
host: dbConfig.host,
|
||
port: dbConfig.port,
|
||
user: dbConfig.user,
|
||
password: dbConfig.password,
|
||
ssl: dbConfig.ssl
|
||
});
|
||
|
||
await connection.query(`CREATE DATABASE IF NOT EXISTS \`${dbConfig.database}\`;`);
|
||
console.log(`✅ Database "${dbConfig.database}" created/verified`);
|
||
await connection.end();
|
||
} else {
|
||
console.log('ℹ️ Skipping database creation (DB_ALLOW_CREATE_DB=false)');
|
||
}
|
||
|
||
// Reconnect with the database specified
|
||
connection = await mysql.createConnection(dbConfig);
|
||
console.log('✅ Connected to MySQL database');
|
||
|
||
// --- Core Tables ---
|
||
|
||
// 1. users table: Central user authentication and common attributes
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS users (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
email VARCHAR(255) UNIQUE NOT NULL,
|
||
password VARCHAR(255) NOT NULL,
|
||
user_type ENUM('personal', 'company') NOT NULL,
|
||
role ENUM('user', 'admin', 'super_admin') DEFAULT 'user',
|
||
iban VARCHAR(34),
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
last_login_at TIMESTAMP NULL,
|
||
INDEX idx_email (email),
|
||
INDEX idx_user_type (user_type),
|
||
INDEX idx_role (role)
|
||
);
|
||
`);
|
||
console.log('✅ Users table created/verified');
|
||
|
||
// 2. personal_profiles table: Details specific to personal users
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS personal_profiles (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT NOT NULL,
|
||
first_name VARCHAR(255) NOT NULL,
|
||
last_name VARCHAR(255) NOT NULL,
|
||
phone VARCHAR(255) NULL,
|
||
date_of_birth DATE,
|
||
nationality VARCHAR(255),
|
||
address TEXT,
|
||
zip_code VARCHAR(20),
|
||
city VARCHAR(100), -- Added city column
|
||
country VARCHAR(100),
|
||
phone_secondary VARCHAR(255),
|
||
emergency_contact_name VARCHAR(255),
|
||
emergency_contact_phone VARCHAR(255),
|
||
account_holder_name VARCHAR(255), -- Added column
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||
UNIQUE KEY unique_user_profile (user_id)
|
||
);
|
||
`);
|
||
console.log('✅ Personal profiles table updated');
|
||
|
||
// 3. company_profiles table: Details specific to company users
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS company_profiles (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT NOT NULL,
|
||
company_name VARCHAR(255) NOT NULL,
|
||
registration_number VARCHAR(255) UNIQUE, -- allow NULL
|
||
phone VARCHAR(255) NULL,
|
||
address TEXT,
|
||
zip_code VARCHAR(20),
|
||
city VARCHAR(100),
|
||
country VARCHAR(100), -- Added country column after city
|
||
branch VARCHAR(255),
|
||
number_of_employees INT,
|
||
business_type VARCHAR(255),
|
||
contact_person_name VARCHAR(255),
|
||
contact_person_phone VARCHAR(255),
|
||
account_holder_name VARCHAR(255), -- Added column
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||
UNIQUE KEY unique_user_profile (user_id),
|
||
UNIQUE KEY unique_registration_number (registration_number)
|
||
);
|
||
`);
|
||
console.log('✅ Company profiles table updated');
|
||
|
||
// Ensure registration_number allows NULL if table already existed
|
||
try {
|
||
await connection.query(`ALTER TABLE company_profiles MODIFY COLUMN registration_number VARCHAR(255) UNIQUE NULL`);
|
||
console.log('🔧 Ensured registration_number allows NULL');
|
||
} catch (e) {
|
||
console.log('ℹ️ registration_number column already allows NULL or ALTER not required');
|
||
}
|
||
|
||
// Ensure phone columns are nullable if tables already existed
|
||
try {
|
||
await connection.query(`ALTER TABLE personal_profiles MODIFY COLUMN phone VARCHAR(255) NULL`);
|
||
await connection.query(`ALTER TABLE company_profiles MODIFY COLUMN phone VARCHAR(255) NULL`);
|
||
console.log('🔧 Ensured phone columns are nullable');
|
||
} catch (e) {
|
||
console.log('ℹ️ Phone columns already nullable or ALTER not required');
|
||
}
|
||
|
||
// 4. user_status table: Comprehensive tracking of user verification and completion steps
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS user_status (
|
||
user_id INT PRIMARY KEY,
|
||
status ENUM('inactive', 'pending', 'active', 'suspended', 'archived') DEFAULT 'pending',
|
||
previous_status ENUM('inactive', 'pending', 'active', 'suspended', 'archived') NULL,
|
||
email_verified BOOLEAN DEFAULT FALSE,
|
||
email_verified_at TIMESTAMP NULL,
|
||
profile_completed BOOLEAN DEFAULT FALSE,
|
||
profile_completed_at TIMESTAMP NULL,
|
||
documents_uploaded BOOLEAN DEFAULT FALSE,
|
||
documents_uploaded_at TIMESTAMP NULL,
|
||
contract_signed BOOLEAN DEFAULT FALSE,
|
||
contract_signed_at TIMESTAMP NULL,
|
||
is_admin_verified BOOLEAN DEFAULT FALSE,
|
||
admin_verified_at TIMESTAMP NULL,
|
||
registration_completed BOOLEAN DEFAULT FALSE,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||
);
|
||
`);
|
||
console.log('✅ User status table created/verified');
|
||
|
||
// Modify existing ENUM columns to add 'archived' status (for existing databases)
|
||
try {
|
||
await connection.query(`
|
||
ALTER TABLE user_status
|
||
MODIFY COLUMN status ENUM('inactive', 'pending', 'active', 'suspended', 'archived') DEFAULT 'pending'
|
||
`);
|
||
console.log('✅ Updated status column to include archived');
|
||
} catch (err) {
|
||
console.warn('⚠️ Could not modify status column:', err.message);
|
||
}
|
||
|
||
try {
|
||
await connection.query(`
|
||
ALTER TABLE user_status
|
||
MODIFY COLUMN previous_status ENUM('inactive', 'pending', 'active', 'suspended', 'archived') NULL
|
||
`);
|
||
console.log('✅ Updated previous_status column to include archived');
|
||
} catch (err) {
|
||
console.warn('⚠️ Could not modify previous_status column:', err.message);
|
||
}
|
||
|
||
// --- Authentication & Verification Tables ---
|
||
|
||
// 5. refresh_tokens table: For refresh token authentication
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS refresh_tokens (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT NOT NULL,
|
||
token VARCHAR(255) UNIQUE NOT NULL,
|
||
expires_at DATETIME NOT NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
revoked_at TIMESTAMP NULL,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||
INDEX idx_user_id (user_id),
|
||
INDEX idx_expires_at (expires_at)
|
||
);
|
||
`);
|
||
console.log('✅ Refresh tokens table created/verified');
|
||
|
||
// 6. email_verifications table: For email verification codes
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS email_verifications (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT NOT NULL,
|
||
verification_code VARCHAR(6) NOT NULL,
|
||
expires_at DATETIME NOT NULL,
|
||
verified_at TIMESTAMP NULL,
|
||
attempts INT DEFAULT 0,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||
INDEX idx_user_code (user_id, verification_code),
|
||
INDEX idx_expires_at (expires_at)
|
||
);
|
||
`);
|
||
console.log('✅ Email verifications table created/verified');
|
||
|
||
// 7. password_resets table: For password reset tokens
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS password_resets (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT NOT NULL,
|
||
token VARCHAR(255) UNIQUE NOT NULL,
|
||
expires_at DATETIME NOT NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
used_at TIMESTAMP NULL,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||
INDEX idx_user_token (user_id, token),
|
||
INDEX idx_expires_at (expires_at)
|
||
);
|
||
`);
|
||
console.log('✅ Password resets table created/verified');
|
||
|
||
// --- Document & Logging Tables ---
|
||
|
||
// 8. user_documents table: Stores object storage IDs
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS user_documents (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT NOT NULL,
|
||
document_type ENUM('personal_id', 'company_id', 'signature', 'contract', 'other') NOT NULL,
|
||
object_storage_id VARCHAR(255) UNIQUE NOT NULL,
|
||
original_filename VARCHAR(255),
|
||
file_size INT,
|
||
mime_type VARCHAR(100),
|
||
upload_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
verified_by_admin BOOLEAN DEFAULT FALSE,
|
||
admin_verified_at TIMESTAMP NULL,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||
INDEX idx_user_document_type (user_id, document_type),
|
||
INDEX idx_object_storage_id (object_storage_id)
|
||
);
|
||
`);
|
||
console.log('✅ User documents table created/verified');
|
||
|
||
// 8c. document_templates table: Stores template metadata and object storage keys
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS document_templates (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
name VARCHAR(255) NOT NULL,
|
||
type VARCHAR(100) NOT NULL,
|
||
storageKey VARCHAR(255) NOT NULL,
|
||
description TEXT,
|
||
lang VARCHAR(10) NOT NULL,
|
||
user_type ENUM('personal','company','both') DEFAULT 'both', -- NEW COLUMN
|
||
version INT DEFAULT 1,
|
||
state ENUM('active','inactive') DEFAULT 'inactive', -- Added state column
|
||
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||
);
|
||
`);
|
||
console.log('✅ Document templates table created/verified');
|
||
|
||
// Ensure version column exists if table already existed
|
||
try {
|
||
await connection.query(`ALTER TABLE document_templates ADD COLUMN version INT DEFAULT 1`);
|
||
console.log('🔧 Ensured version column exists');
|
||
} catch (e) {
|
||
console.log('ℹ️ Version column already exists or ALTER not required');
|
||
}
|
||
// Ensure state column exists if table already existed
|
||
try {
|
||
await connection.query(`ALTER TABLE document_templates ADD COLUMN state ENUM('active','inactive') DEFAULT 'inactive'`);
|
||
console.log('🔧 Ensured state column exists');
|
||
} catch (e) {
|
||
console.log('ℹ️ State column already exists or ALTER not required');
|
||
}
|
||
// Ensure user_type column exists
|
||
try {
|
||
await connection.query(`ALTER TABLE document_templates ADD COLUMN user_type ENUM('personal','company','both') DEFAULT 'both'`);
|
||
console.log('🔧 Ensured user_type column exists');
|
||
} catch (e) {
|
||
console.log('ℹ️ user_type column already exists or ALTER not required');
|
||
}
|
||
|
||
// 8b. user_id_documents table: Stores ID-specific metadata (front/back object storage IDs)
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS user_id_documents (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT NOT NULL,
|
||
document_type ENUM('personal_id', 'company_id') NOT NULL,
|
||
front_object_storage_id VARCHAR(255) NOT NULL,
|
||
back_object_storage_id VARCHAR(255) NULL,
|
||
original_filename_front VARCHAR(255), -- NEW COLUMN
|
||
original_filename_back VARCHAR(255), -- NEW COLUMN
|
||
id_type VARCHAR(50),
|
||
id_number VARCHAR(100),
|
||
expiry_date DATE,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE
|
||
);
|
||
`);
|
||
console.log('✅ User ID documents table created/verified');
|
||
|
||
// 9. user_action_logs table: For detailed user activity logging
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS user_action_logs (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
affected_user_id INT NULL,
|
||
action VARCHAR(100) NOT NULL,
|
||
performed_by_user_id INT NULL,
|
||
details JSON,
|
||
ip_address VARCHAR(45),
|
||
user_agent TEXT,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (affected_user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||
FOREIGN KEY (performed_by_user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||
INDEX idx_affected_user (affected_user_id),
|
||
INDEX idx_performed_by (performed_by_user_id),
|
||
INDEX idx_action (action),
|
||
INDEX idx_created_at (created_at)
|
||
);
|
||
`);
|
||
console.log('✅ User action logs table created/verified');
|
||
|
||
// --- Email & Registration Flow Tables ---
|
||
|
||
// 10. email_attempts table: For tracking email sending attempts
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS email_attempts (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT NOT NULL,
|
||
attempt_type ENUM('verification', 'password_reset', 'registration_completion', 'notification', 'other') NOT NULL,
|
||
email_address VARCHAR(255) NOT NULL,
|
||
success BOOLEAN DEFAULT FALSE,
|
||
error_message TEXT,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||
INDEX idx_user_attempts (user_id, attempt_type),
|
||
INDEX idx_created_at (created_at)
|
||
);
|
||
`);
|
||
console.log('✅ Email attempts table created/verified');
|
||
|
||
// --- Referral Tables ---
|
||
|
||
// 12. referral_tokens table: Manages referral codes
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS referral_tokens (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
token VARCHAR(64) UNIQUE NOT NULL,
|
||
created_by_user_id INT NOT NULL,
|
||
expires_at DATETIME NOT NULL,
|
||
max_uses INT DEFAULT -1,
|
||
uses_remaining INT DEFAULT -1,
|
||
status ENUM('active', 'inactive', 'expired', 'exhausted') DEFAULT 'active',
|
||
deactivation_reason VARCHAR(50),
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||
INDEX idx_token (token),
|
||
INDEX idx_status_expires (status, expires_at),
|
||
INDEX idx_created_by (created_by_user_id)
|
||
);
|
||
`);
|
||
console.log('✅ Referral tokens table created/verified');
|
||
|
||
// Generated label columns (virtual) to display 'unlimited' instead of -1 in UI tools
|
||
try {
|
||
await connection.query(`
|
||
ALTER TABLE referral_tokens
|
||
ADD COLUMN max_uses_label VARCHAR(20)
|
||
GENERATED ALWAYS AS (CASE WHEN max_uses = -1 THEN 'unlimited' ELSE CAST(max_uses AS CHAR) END) VIRTUAL
|
||
`);
|
||
console.log('🆕 Added virtual column referral_tokens.max_uses_label');
|
||
} catch (e) {
|
||
console.log('ℹ️ max_uses_label already exists or cannot add:', e.message);
|
||
}
|
||
try {
|
||
await connection.query(`
|
||
ALTER TABLE referral_tokens
|
||
ADD COLUMN uses_remaining_label VARCHAR(20)
|
||
GENERATED ALWAYS AS (CASE WHEN uses_remaining = -1 THEN 'unlimited' ELSE CAST(uses_remaining AS CHAR) END) VIRTUAL
|
||
`);
|
||
console.log('🆕 Added virtual column referral_tokens.uses_remaining_label');
|
||
} catch (e) {
|
||
console.log('ℹ️ uses_remaining_label already exists or cannot add:', e.message);
|
||
}
|
||
|
||
// Normalized view now sources the generated label columns (still handy for joins)
|
||
try {
|
||
await connection.query(`
|
||
CREATE OR REPLACE VIEW referral_tokens_normalized AS
|
||
SELECT
|
||
rt.*,
|
||
rt.max_uses_label AS max_uses_display,
|
||
rt.uses_remaining_label AS uses_remaining_display
|
||
FROM referral_tokens rt;
|
||
`);
|
||
console.log('🆕 referral_tokens_normalized view created/updated');
|
||
} catch (e) {
|
||
console.warn('⚠️ Could not create referral_tokens_normalized view:', e.message);
|
||
}
|
||
|
||
// 13. referral_token_usage table: Tracks each use of a referral token
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS referral_token_usage (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
referral_token_id INT NOT NULL,
|
||
used_by_user_id INT NOT NULL,
|
||
used_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (referral_token_id) REFERENCES referral_tokens(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||
FOREIGN KEY (used_by_user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||
UNIQUE KEY unique_token_user_usage (referral_token_id, used_by_user_id),
|
||
INDEX idx_token_usage (referral_token_id),
|
||
INDEX idx_user_usage (used_by_user_id)
|
||
);
|
||
`);
|
||
console.log('✅ Referral token usage table created/verified');
|
||
|
||
// --- Authorization Tables ---
|
||
|
||
// 14. permissions table: Defines granular permissions
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS permissions (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
name VARCHAR(100) UNIQUE NOT NULL,
|
||
description VARCHAR(255),
|
||
is_active BOOLEAN DEFAULT TRUE,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Added
|
||
created_by INT NULL, -- Added
|
||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||
);
|
||
`);
|
||
console.log('✅ Permissions table created/verified');
|
||
|
||
// 15. user_permissions join table: Assigns specific permissions to users
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS user_permissions (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT NOT NULL,
|
||
permission_id INT NOT NULL,
|
||
granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
granted_by INT NULL, -- Added column
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||
FOREIGN KEY (granted_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE, -- FK constraint
|
||
UNIQUE KEY unique_user_permission (user_id, permission_id)
|
||
);
|
||
`);
|
||
console.log('✅ User permissions table created/verified');
|
||
|
||
// --- User Settings Table ---
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS user_settings (
|
||
user_id INT PRIMARY KEY,
|
||
theme ENUM('light', 'dark') DEFAULT 'light',
|
||
font_size ENUM('normal', 'large') DEFAULT 'normal',
|
||
high_contrast_mode BOOLEAN DEFAULT FALSE,
|
||
two_factor_auth_enabled BOOLEAN DEFAULT FALSE,
|
||
account_visibility ENUM('public', 'private') DEFAULT 'public',
|
||
show_email BOOLEAN DEFAULT TRUE,
|
||
show_phone BOOLEAN DEFAULT TRUE,
|
||
data_export_requested BOOLEAN DEFAULT FALSE,
|
||
last_data_export_at TIMESTAMP NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE
|
||
);
|
||
`);
|
||
console.log('✅ User settings table created/verified');
|
||
|
||
// --- Rate Limiting Table ---
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS rate_limit (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
rate_key VARCHAR(255) NOT NULL,
|
||
window_start DATETIME NOT NULL,
|
||
count INT DEFAULT 0,
|
||
window_seconds INT NOT NULL,
|
||
max INT NOT NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
UNIQUE KEY unique_key_window (rate_key, window_start)
|
||
);
|
||
`);
|
||
console.log('✅ Rate limit table created/verified');
|
||
|
||
// --- NEW: company_stamps table (for company/admin managed stamps) ---
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS company_stamps (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
company_id INT NOT NULL,
|
||
label VARCHAR(100) NULL,
|
||
mime_type VARCHAR(50) NOT NULL,
|
||
image_base64 LONGTEXT NOT NULL,
|
||
is_active BOOLEAN DEFAULT FALSE,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (company_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||
UNIQUE KEY unique_company_label (company_id, label),
|
||
INDEX idx_company_active (company_id, is_active)
|
||
);
|
||
`);
|
||
console.log('✅ Company stamps table created/verified');
|
||
|
||
// --- Matrix: Global 5-ary tree config and relations ---
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS matrix_config (
|
||
id TINYINT PRIMARY KEY DEFAULT 1,
|
||
master_top_user_id INT NOT NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
CONSTRAINT fk_matrix_config_master FOREIGN KEY (master_top_user_id) REFERENCES users(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||
CONSTRAINT chk_matrix_singleton CHECK (id = 1)
|
||
);
|
||
`);
|
||
console.log('✅ Matrix config table created/verified');
|
||
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS user_tree_edges (
|
||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||
parent_user_id INT NOT NULL,
|
||
child_user_id INT NOT NULL,
|
||
position TINYINT NOT NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
CONSTRAINT fk_edges_parent FOREIGN KEY (parent_user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||
CONSTRAINT fk_edges_child FOREIGN KEY (child_user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||
CONSTRAINT uq_child UNIQUE (child_user_id),
|
||
CONSTRAINT uq_parent_position UNIQUE (parent_user_id, position),
|
||
CONSTRAINT chk_position CHECK (position BETWEEN 1 AND 5)
|
||
);
|
||
`);
|
||
console.log('✅ User tree edges table created/verified');
|
||
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS user_tree_closure (
|
||
ancestor_user_id INT NOT NULL,
|
||
descendant_user_id INT NOT NULL,
|
||
depth INT NOT NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
CONSTRAINT pk_closure PRIMARY KEY (ancestor_user_id, descendant_user_id),
|
||
CONSTRAINT fk_closure_ancestor FOREIGN KEY (ancestor_user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||
CONSTRAINT fk_closure_descendant FOREIGN KEY (descendant_user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||
CONSTRAINT chk_depth_nonneg CHECK (depth >= 0)
|
||
);
|
||
`);
|
||
console.log('✅ User tree closure table created/verified');
|
||
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS user_matrix_metadata (
|
||
root_user_id INT PRIMARY KEY,
|
||
ego_activated_at TIMESTAMP NULL,
|
||
last_bfs_fill_at TIMESTAMP NULL,
|
||
immediate_children_count INT DEFAULT 0,
|
||
first_free_position TINYINT NULL,
|
||
name VARCHAR(255) NULL, -- NEW: matrix display name
|
||
is_active BOOLEAN DEFAULT TRUE, -- NEW: activation flag
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
CONSTRAINT fk_matrix_meta_root FOREIGN KEY (root_user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||
CONSTRAINT chk_first_free_position CHECK (first_free_position IS NULL OR (first_free_position BETWEEN 1 AND 5))
|
||
);
|
||
`);
|
||
console.log('✅ User matrix metadata table created/verified');
|
||
|
||
// Ensure new columns exist if table already existed
|
||
try {
|
||
await connection.query(`ALTER TABLE user_matrix_metadata ADD COLUMN name VARCHAR(255) NULL`);
|
||
console.log('🔧 Ensured user_matrix_metadata.name exists');
|
||
} catch (e) {
|
||
console.log('ℹ️ user_matrix_metadata.name already exists or ALTER not required');
|
||
}
|
||
try {
|
||
await connection.query(`ALTER TABLE user_matrix_metadata ADD COLUMN is_active BOOLEAN DEFAULT TRUE`);
|
||
console.log('🔧 Ensured user_matrix_metadata.is_active exists');
|
||
} catch (e) {
|
||
console.log('ℹ️ user_matrix_metadata.is_active already exists or ALTER not required');
|
||
}
|
||
|
||
// --- Added Index Optimization Section ---
|
||
try {
|
||
// Core / status
|
||
await ensureIndex(connection, 'users', 'idx_users_created_at', 'created_at');
|
||
await ensureIndex(connection, 'user_status', 'idx_user_status_status', 'status');
|
||
await ensureIndex(connection, 'user_status', 'idx_user_status_registration_completed', 'registration_completed');
|
||
|
||
// Tokens & auth
|
||
await ensureIndex(connection, 'refresh_tokens', 'idx_refresh_user_expires', 'user_id, expires_at');
|
||
await ensureIndex(connection, 'email_verifications', 'idx_email_verifications_user_expires', 'user_id, expires_at');
|
||
await ensureIndex(connection, 'password_resets', 'idx_password_resets_user_expires', 'user_id, expires_at');
|
||
|
||
// Documents
|
||
await ensureIndex(connection, 'user_documents', 'idx_user_documents_upload_at', 'upload_at');
|
||
await ensureIndex(connection, 'user_documents', 'idx_user_documents_verified', 'verified_by_admin');
|
||
await ensureIndex(connection, 'user_id_documents', 'idx_user_id_docs_user_type', 'user_id, document_type');
|
||
|
||
// Activity logs (composite for common filtered ordering)
|
||
await ensureIndex(connection, 'user_action_logs', 'idx_user_action_logs_action_created', 'action, created_at');
|
||
|
||
// Referrals
|
||
await ensureIndex(connection, 'referral_token_usage', 'idx_referral_token_usage_used_at', 'used_at');
|
||
|
||
// Permissions
|
||
await ensureIndex(connection, 'permissions', 'idx_permissions_is_active', 'is_active');
|
||
await ensureIndex(connection, 'user_permissions', 'idx_user_permissions_granted_by', 'granted_by');
|
||
|
||
// Settings
|
||
await ensureIndex(connection, 'user_settings', 'idx_user_settings_theme', 'theme');
|
||
|
||
// Rate limit (for queries only on rate_key)
|
||
await ensureIndex(connection, 'rate_limit', 'idx_rate_limit_rate_key', 'rate_key');
|
||
await ensureIndex(connection, 'document_templates', 'idx_document_templates_user_type', 'user_type');
|
||
await ensureIndex(connection, 'document_templates', 'idx_document_templates_state_user_type', 'state, user_type'); // NEW composite index
|
||
await ensureIndex(connection, 'company_stamps', 'idx_company_stamps_company', 'company_id');
|
||
await ensureIndex(connection, 'company_stamps', 'idx_company_stamps_active', 'is_active');
|
||
|
||
// Matrix indexes
|
||
await ensureIndex(connection, 'user_tree_edges', 'idx_user_tree_edges_parent', 'parent_user_id');
|
||
// child_user_id already has a UNIQUE constraint; extra index not needed
|
||
await ensureIndex(connection, 'user_tree_closure', 'idx_user_tree_closure_ancestor_depth', 'ancestor_user_id, depth');
|
||
await ensureIndex(connection, 'user_tree_closure', 'idx_user_tree_closure_descendant', 'descendant_user_id');
|
||
await ensureIndex(connection, 'user_matrix_metadata', 'idx_user_matrix_is_active', 'is_active'); // NEW
|
||
|
||
console.log('🚀 Performance indexes created/verified');
|
||
} catch (e) {
|
||
console.warn('⚠️ Index optimization phase encountered an issue:', e.message);
|
||
}
|
||
|
||
console.log('🎉 Normalized database schema created/updated successfully!');
|
||
|
||
await connection.end();
|
||
return true;
|
||
|
||
} catch (error) {
|
||
console.error('💥 Error during database initialization:', error.message);
|
||
if (connection) {
|
||
await connection.end();
|
||
}
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
module.exports = { createDatabase };
|