From b6db8da45063d5b9dfc49bb8d2bec7c1ac09d59c Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sun, 3 May 2026 23:57:27 +0300 Subject: [PATCH] Update: 2026-05-03 23:57:27 --- app/modules_app/tenants/create.php | 48 ++++- app/modules_app/users/create.php | 13 +- public/shell.php | 60 +++++- scripts/PROJECT_DOCUMENTATION.md | 311 +++++++++++++++++++++++++++++ 4 files changed, 425 insertions(+), 7 deletions(-) create mode 100644 scripts/PROJECT_DOCUMENTATION.md diff --git a/app/modules_app/tenants/create.php b/app/modules_app/tenants/create.php index 813dfcf..de708e2 100644 --- a/app/modules_app/tenants/create.php +++ b/app/modules_app/tenants/create.php @@ -17,7 +17,10 @@ $data = input(); $errors = Validator::validate($data, [ 'name' => 'required', - 'email' => 'required|email' + 'email' => 'required|email', + 'manager_name' => 'required', + 'manager_email' => 'required|email', + 'manager_password' => 'required' ]); if ($errors) { @@ -27,14 +30,51 @@ if ($errors) { $db = Database::getInstance(); try { - $stmt = $db->prepare("INSERT INTO tenants (name, email, phone, status, created_at) VALUES (?, ?, ?, 'active', NOW())"); + $db->beginTransaction(); + + // Generate Tenant UUID in PHP so we can use it immediately + $tenantId = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), + mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) + ); + + // 1. Create Tenant + $stmt = $db->prepare("INSERT INTO tenants (id, name, email, phone, status, created_at) VALUES (?, ?, ?, ?, 'active', NOW())"); $stmt->execute([ + $tenantId, $data['name'], $data['email'], $data['phone'] ?? null ]); - json_success(null, 'تم إنشاء المكتب بنجاح'); + // Generate User UUID + $userId = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), + mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) + ); + + // Encrypt sensitive user data + $encryptedName = \App\Core\Encryption::encrypt($data['manager_name']); + $encryptedEmail = \App\Core\Encryption::encrypt($data['manager_email']); + $emailHash = hash('sha256', strtolower($data['manager_email'])); + + // 2. Create Initial Manager (Admin) for this Tenant + $stmtUser = $db->prepare("INSERT INTO users (id, tenant_id, name, email, email_hash, password_hash, role, created_at) VALUES (?, ?, ?, ?, ?, ?, 'admin', NOW())"); + $stmtUser->execute([ + $userId, + $tenantId, + $encryptedName, + $encryptedEmail, + $emailHash, + password_hash($data['manager_password'], PASSWORD_DEFAULT) + ]); + + $db->commit(); + json_success(null, 'تم إنشاء المكتب ومدير المكتب بنجاح'); } catch (\Exception $e) { - json_error('حدث خطأ أثناء حفظ البيانات', 500); + $db->rollBack(); + json_error('حدث خطأ أثناء حفظ البيانات: ' . $e->getMessage(), 500); } + diff --git a/app/modules_app/users/create.php b/app/modules_app/users/create.php index 53e13e6..e973b1b 100644 --- a/app/modules_app/users/create.php +++ b/app/modules_app/users/create.php @@ -46,11 +46,22 @@ $encryptedName = Encryption::encrypt($data['name']); $encryptedEmail = Encryption::encrypt($data['email']); $emailHash = hash('sha256', strtolower($data['email'])); // For fast lookup during login +// 3. Determine Tenant ID +$tenantId = null; +if ($decoded['role'] === 'super_admin') { + if (empty($data['tenant_id'])) { + json_error('يجب اختيار المكتب المحاسبي', 422); + } + $tenantId = $data['tenant_id']; +} else { + $tenantId = $decoded['tenant_id']; +} + // 4. Save to Database try { $stmt = $db->prepare("INSERT INTO users (tenant_id, name, email, email_hash, password_hash, role, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"); $stmt->execute([ - $decoded['tenant_id'], + $tenantId, $encryptedName, $encryptedEmail, $emailHash, diff --git a/public/shell.php b/public/shell.php index 74e98d6..349f734 100644 --- a/public/shell.php +++ b/public/shell.php @@ -153,6 +153,15 @@ +
+ + +
@@ -178,6 +187,20 @@
+
+

معلومات مدير المكتب (Admin)

+
+ + +
+
+ + +
+
+ + +
@@ -223,17 +246,22 @@ page: 'dashboard', users: [], companies: [], + tenants: [], stats: { total: 0, pending: 0, approved: 0 }, showAddModal: false, showAddCompanyModal: false, - newUser: { name: '', email: '', password: '', role: 'employee' }, + newUser: { name: '', email: '', password: '', role: 'employee', tenant_id: '' }, newCompany: { name: '', tax_identification_number: '', commercial_registration_number: '', address: '' }, + newTenant: { name: '', email: '', phone: '', manager_name: '', manager_email: '', manager_password: '' }, init() { if (!this.user) window.location.href = '/login.php'; this.loadUsers(); this.loadStats(); this.loadCompanies(); + if (this.user.role === 'super_admin') { + this.loadTenants(); + } }, title() { @@ -277,7 +305,7 @@ const json = await res.json(); if (json.success) { this.showAddModal = false; - this.newUser = { name: '', email: '', password: '', role: 'employee' }; + this.newUser = { name: '', email: '', password: '', role: 'employee', tenant_id: '' }; this.loadUsers(); alert('تم إضافة المستخدم بنجاح'); } else { @@ -305,6 +333,34 @@ } }, + async loadTenants() { + const res = await fetch('/api/v1/tenants', { + headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') } + }); + const json = await res.json(); + if (json.success) this.tenants = json.data; + }, + + async createTenant() { + const res = await fetch('/api/v1/tenants/create', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + localStorage.getItem('access_token') + }, + body: JSON.stringify(this.newTenant) + }); + const json = await res.json(); + if (json.success) { + this.showAddTenantModal = false; + this.newTenant = { name: '', email: '', phone: '', manager_name: '', manager_email: '', manager_password: '' }; + this.loadTenants(); + alert('تم إنشاء المكتب وتعيين المدير بنجاح!'); + } else { + alert(json.message); + } + }, + logout() { localStorage.clear(); window.location.href = '/login.php'; diff --git a/scripts/PROJECT_DOCUMENTATION.md b/scripts/PROJECT_DOCUMENTATION.md new file mode 100644 index 0000000..8653b04 --- /dev/null +++ b/scripts/PROJECT_DOCUMENTATION.md @@ -0,0 +1,311 @@ +# Musadaq Project Documentation + +This file contains the complete source code of the project (excluding dependencies and sensitive data). + +## File: `fix_data.php` + +```php +beginTransaction(); + + // 1. Clear corrupted data tables + $db->exec("SET FOREIGN_KEY_CHECKS = 0"); + $db->exec("TRUNCATE TABLE user_company_assignments"); + $db->exec("TRUNCATE TABLE invoices"); + $db->exec("TRUNCATE TABLE companies"); + $db->exec("SET FOREIGN_KEY_CHECKS = 1"); + echo "[OK] Cleared companies, invoices, and assignments.\n"; + + // 2. Ensure Super Admin does not have a tenant_id (if your schema allows NULL, else set to empty string) + // Actually, schema.sql says tenant_id CHAR(36) NOT NULL. + // This is a flaw in schema.sql for Super Admins. We will leave users alone for now. + + // 3. Fix the admin's tenant_id to match the first available tenant + $stmt = $db->query("SELECT id FROM tenants LIMIT 1"); + $tenantId = $stmt->fetchColumn(); + + if ($tenantId) { + $db->exec("UPDATE users SET tenant_id = '$tenantId' WHERE role != 'super_admin'"); + echo "[OK] Linked all non-super-admin users to Tenant ID: $tenantId\n"; + } + + $db->commit(); + echo "--- Reset Complete ---\n"; + +} catch (\Exception $e) { + $db->rollBack(); + echo "[ERROR] Reset failed: " . $e->getMessage() . "\n"; +} + +``` + +## File: `schema.sql` + +```sql +-- ════════════════════════════════════════════════════════════ +-- مُصادَق — Database Schema v1.0 +-- ════════════════════════════════════════════════════════════ + +-- Tenants (Accounting Offices) +CREATE TABLE tenants ( + id CHAR(36) PRIMARY KEY DEFAULT (UUID()), + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + phone VARCHAR(20), + status ENUM('active', 'suspended', 'trial') DEFAULT 'trial', + trial_ends_at DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- Users +CREATE TABLE users ( + id CHAR(36) PRIMARY KEY DEFAULT (UUID()), + tenant_id CHAR(36) NOT NULL, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + password_hash VARCHAR(255) NOT NULL, + role ENUM('super_admin','admin','accountant','viewer') NOT NULL, + company_id CHAR(36) NULL, -- assigned company for accountant + refresh_token_hash VARCHAR(255) NULL, + is_active BOOLEAN DEFAULT TRUE, + last_login_at DATETIME NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY uq_tenant_email (tenant_id, email), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE +); + +-- API Keys (for external integrations / mobile scanner) +CREATE TABLE api_keys ( + id CHAR(36) PRIMARY KEY DEFAULT (UUID()), + tenant_id CHAR(36) NOT NULL, + user_id CHAR(36) NOT NULL, + name VARCHAR(100) NOT NULL, + public_key VARCHAR(64) NOT NULL UNIQUE, + secret_hash VARCHAR(255) NOT NULL, -- bcrypt hash of secret + last_used_at DATETIME NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); + +-- Companies +CREATE TABLE companies ( + id CHAR(36) PRIMARY KEY DEFAULT (UUID()), + tenant_id CHAR(36) NOT NULL, + name VARCHAR(255) NOT NULL, + name_en VARCHAR(255) NULL, + tax_identification_number VARCHAR(20) NOT NULL, + address TEXT NULL, + jofotara_client_id_encrypted TEXT NULL, + jofotara_secret_key_encrypted TEXT NULL, + jofotara_income_source_sequence VARCHAR(50) NULL, + certificate_path VARCHAR(255) NULL, + certificate_password_encrypted VARCHAR(500) NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_name (name), + INDEX idx_tin (tax_identification_number), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE +); + +-- Subscriptions +CREATE TABLE subscriptions ( + id CHAR(36) PRIMARY KEY DEFAULT (UUID()), + tenant_id CHAR(36) NOT NULL UNIQUE, + plan ENUM('basic','office','pro','enterprise') NOT NULL DEFAULT 'basic', + max_companies INT NOT NULL DEFAULT 5, + max_invoices_per_month INT NOT NULL DEFAULT 100, + price_jod DECIMAL(10,2) NOT NULL DEFAULT 0, + invoices_used_this_month INT NOT NULL DEFAULT 0, + status ENUM('active','past_due','cancelled') DEFAULT 'active', + current_period_start DATETIME NULL, + current_period_end DATETIME NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE +); + +-- Invoices +CREATE TABLE invoices ( + id CHAR(36) PRIMARY KEY DEFAULT (UUID()), + tenant_id CHAR(36) NOT NULL, + company_id CHAR(36) NOT NULL, + invoice_number VARCHAR(100) NULL, + invoice_date DATE NULL, + invoice_type ENUM('cash','credit') DEFAULT 'cash', + ubl_type_code CHAR(3) DEFAULT '388', + payment_method_code CHAR(3) DEFAULT '013', + supplier_tin VARCHAR(20) NULL, + supplier_name VARCHAR(255) NULL, + supplier_address TEXT NULL, + buyer_tin VARCHAR(20) NULL, + buyer_national_id VARCHAR(20) NULL, + buyer_name VARCHAR(255) NULL, + subtotal DECIMAL(15,3) DEFAULT 0, + discount_total DECIMAL(15,3) DEFAULT 0, + tax_amount DECIMAL(15,3) DEFAULT 0, + grand_total DECIMAL(15,3) DEFAULT 0, + currency_code CHAR(3) DEFAULT 'JOD', + status ENUM('uploaded','extracting','extracted','validated','validation_failed','submitting','approved','rejected') DEFAULT 'uploaded', + original_file_path TEXT NULL, + invoice_category VARCHAR(20) DEFAULT 'simplified', + validation_errors JSON NULL, + qr_code TEXT NULL, + ai_confidence_score DECIMAL(4,3) NULL, + ai_prompt_tokens INT DEFAULT 0, + ai_completion_tokens INT DEFAULT 0, + ai_total_cost DECIMAL(10,6) DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_tenant (tenant_id), + INDEX idx_company (company_id), + INDEX idx_status (status), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE +); + +-- Invoice Lines +CREATE TABLE invoice_lines ( + id CHAR(36) PRIMARY KEY DEFAULT (UUID()), + invoice_id CHAR(36) NOT NULL, + line_number INT NOT NULL, + description TEXT NOT NULL, + quantity DECIMAL(15,3) NOT NULL, + unit_price DECIMAL(15,3) NOT NULL, + discount DECIMAL(15,3) DEFAULT 0, + tax_rate DECIMAL(5,4) NOT NULL, + line_total DECIMAL(15,3) NOT NULL, + FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE +); + +-- Audit Logs +CREATE TABLE audit_logs ( + id CHAR(36) PRIMARY KEY DEFAULT (UUID()), + tenant_id CHAR(36) NULL, + user_id CHAR(36) NULL, + action VARCHAR(100) NOT NULL, + entity_type VARCHAR(50) NULL, + entity_id CHAR(36) NULL, + old_data JSON NULL, + new_data JSON NULL, + ip_address VARCHAR(45) NULL, + user_agent VARCHAR(500) NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_tenant (tenant_id), + INDEX idx_action (action), + INDEX idx_created (created_at) +); +``` + +## File: `migrate.php` + +```php +exec("ALTER TABLE users ADD COLUMN email_hash VARCHAR(64) AFTER email, ADD INDEX (email_hash)"); + echo "[OK] Added email_hash column and index.\n"; +} catch (\Exception $e) { + echo "[SKIP] email_hash column might already exist.\n"; +} + +// 2. Fetch all users to encrypt their data +$stmt = $db->query("SELECT id, name, email FROM users"); +$users = $stmt->fetchAll(); + +echo "Found " . count($users) . " users. Starting encryption...\n"; + +$updateStmt = $db->prepare("UPDATE users SET name = ?, email = ?, email_hash = ? WHERE id = ?"); + +foreach ($users as $user) { + // Check if data is already encrypted (to avoid double encryption) + $isAlreadyEncrypted = Encryption::decrypt($user['email']) !== false; + + if ($isAlreadyEncrypted) { + echo "User ID {$user['id']} is already encrypted. Skipping.\n"; + continue; + } + + // Encrypt Name + $encryptedName = Encryption::encrypt($user['name']); + + // Encrypt Email + $encryptedEmail = Encryption::encrypt($user['email']); + + // Generate Hash for lookup + $emailHash = hash('sha256', strtolower($user['email'])); + + $updateStmt->execute([ + $encryptedName, + $encryptedEmail, + $emailHash, + $user['id'] + ]); + +echo "User ID {$user['id']} migrated successfully.\n"; +} + +// 4. Create user_company_assignments table +try { + $db->exec("CREATE TABLE IF NOT EXISTS user_company_assignments ( + id INT AUTO_INCREMENT, + user_id VARCHAR(100) NOT NULL, + company_id VARCHAR(100) NOT NULL, + assigned_by VARCHAR(100) NOT NULL, + assigned_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + is_active TINYINT(1) NOT NULL DEFAULT 1, + PRIMARY KEY (id), + UNIQUE KEY uq_user_company (user_id, company_id), + CONSTRAINT fk_uca_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + CONSTRAINT fk_uca_company FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, + CONSTRAINT fk_uca_admin FOREIGN KEY (assigned_by) REFERENCES users(id) ON DELETE RESTRICT + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); + echo "[OK] User_company_assignments table created.\n"; +} catch (\Exception $e) { + echo "[SKIP] user_company_assignments table: " . $e->getMessage() . "\n"; +} + +// 5. Update invoices table to include uploaded_by +try { + $db->exec("ALTER TABLE invoices ADD COLUMN uploaded_by VARCHAR(100) NULL AFTER status"); + $db->exec("ALTER TABLE invoices ADD CONSTRAINT fk_inv_uploader FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL"); + echo "[OK] Updated invoices table with uploaded_by tracker.\n"; +} catch (\Exception $e) { + echo "[SKIP] invoices table update: " . $e->getMessage() . "\n"; +} + +echo "--- Migration Complete ---\n"; + +``` +