From 089a2b76c0730b5357d03bae5ec0c4bf74c61a33 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sun, 3 May 2026 21:58:11 +0300 Subject: [PATCH] Update: 2026-05-03 21:58:11 --- app/modules_app/auth/login.php | 11 +- app/modules_app/companies/create.php | 78 +++++++++++++ app/modules_app/companies/index.php | 46 ++++++++ app/modules_app/users/create.php | 56 ++++++++++ app/modules_app/users/index.php | 13 ++- public/index.php | 3 + public/shell.php | 155 +++++++++++++++++++++++++- public/tool-encrypt.php | 53 +++++++++ scripts/migrate.php | 105 ++++++++++++++++++ scripts/schema.sql | 157 +++++++++++++++++++++++++++ 10 files changed, 668 insertions(+), 9 deletions(-) create mode 100644 app/modules_app/companies/create.php create mode 100644 app/modules_app/companies/index.php create mode 100644 app/modules_app/users/create.php create mode 100644 public/tool-encrypt.php create mode 100644 scripts/migrate.php create mode 100644 scripts/schema.sql diff --git a/app/modules_app/auth/login.php b/app/modules_app/auth/login.php index f873362..bef8dde 100644 --- a/app/modules_app/auth/login.php +++ b/app/modules_app/auth/login.php @@ -28,10 +28,11 @@ if ($errors) { $email = $data['email']; $password = $data['password']; -// 2. DB Check +// 2. DB Check (Using hash for lookup since email is encrypted) $db = Database::getInstance(); -$stmt = $db->prepare("SELECT * FROM users WHERE email = ? LIMIT 1"); -$stmt->execute([$email]); +$emailHash = hash('sha256', strtolower($email)); +$stmt = $db->prepare("SELECT * FROM users WHERE email_hash = ? LIMIT 1"); +$stmt->execute([$emailHash]); $user = $stmt->fetch(); if (!$user || !password_verify($password, $user['password_hash'])) { @@ -63,7 +64,7 @@ json_success([ 'refresh_token' => $refreshToken, 'user' => [ 'id' => $user['id'], - 'name' => $user['name'], - 'email' => $user['email'] + 'name' => (App\Core\Encryption::decrypt($user['name']) ?: $user['name']), + 'email' => (App\Core\Encryption::decrypt($user['email']) ?: $user['email']) ] ], 'تم تسجيل الدخول بنجاح'); diff --git a/app/modules_app/companies/create.php b/app/modules_app/companies/create.php new file mode 100644 index 0000000..058be0c --- /dev/null +++ b/app/modules_app/companies/create.php @@ -0,0 +1,78 @@ + 'required', + 'tax_identification_number' => 'required' +]); + +if ($errors) { + json_error('Validation Failed', 422, $errors); +} + +$db = Database::getInstance(); + +try { + $db->beginTransaction(); + + // 2. Encrypt sensitive fields + $encryptedName = Encryption::encrypt($data['name']); + $encryptedNameEn = !empty($data['name_en']) ? Encryption::encrypt($data['name_en']) : null; + + // Encrypt JoFotara keys if provided + $jofotaraClientId = !empty($data['jofotara_client_id']) ? Encryption::encrypt($data['jofotara_client_id']) : null; + $jofotaraSecretKey = !empty($data['jofotara_secret_key']) ? Encryption::encrypt($data['jofotara_secret_key']) : null; + + // 3. Save to Database + $stmt = $db->prepare(" + INSERT INTO companies ( + tenant_id, name, name_en, tax_identification_number, commercial_registration_number, + city, address, contact_email, contact_phone, + jofotara_client_id_encrypted, jofotara_secret_key_encrypted, + created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + "); + + $stmt->execute([ + $decoded['user_id'], // Using current admin as tenant_id + $encryptedName, + $encryptedNameEn, + $data['tax_identification_number'], + $data['commercial_registration_number'] ?? null, + $data['city'] ?? null, + $data['address'] ?? null, + $data['contact_email'] ?? null, + $data['contact_phone'] ?? null, + $jofotaraClientId, + $jofotaraSecretKey, + date('Y-m-d H:i:s') + ]); + + $companyId = $db->lastInsertId(); + + // 4. Pivot link + $stmt = $db->prepare("INSERT INTO user_companies (user_id, company_id, role) VALUES (?, ?, ?)"); + $stmt->execute([$decoded['user_id'], $companyId, 'admin']); + + $db->commit(); + json_success(['id' => $companyId], 'تم إنشاء الشركة بنجاح'); + +} catch (\Exception $e) { + $db->rollBack(); + json_error('حدث خطأ أثناء حفظ البيانات: ' . $e->getMessage(), 500); +} diff --git a/app/modules_app/companies/index.php b/app/modules_app/companies/index.php new file mode 100644 index 0000000..75eed8d --- /dev/null +++ b/app/modules_app/companies/index.php @@ -0,0 +1,46 @@ +query("SELECT * FROM companies WHERE deleted_at IS NULL"); +} else { + // 2. Others see only linked companies + $stmt = $db->prepare(" + SELECT c.* FROM companies c + JOIN user_companies uc ON c.id = uc.company_id + WHERE uc.user_id = ? AND c.deleted_at IS NULL + "); + $stmt->execute([$decoded['user_id']]); +} + +$companies = $stmt->fetchAll(); + +// 3. Decrypt fields +foreach ($companies as &$company) { + // Decrypt Name + $decryptedName = Encryption::decrypt($company['name']); + $company['name'] = $decryptedName !== false ? $decryptedName : $company['name']; + + // Decrypt Name EN + if (!empty($company['name_en'])) { + $decryptedNameEn = Encryption::decrypt($company['name_en']); + $company['name_en'] = $decryptedNameEn !== false ? $decryptedNameEn : $company['name_en']; + } + + // Redact JoFotara secrets if returned to UI (or just don't return them) + unset($company['jofotara_client_id_encrypted']); + unset($company['jofotara_secret_key_encrypted']); + unset($company['certificate_password_encrypted']); +} + +json_success($companies); diff --git a/app/modules_app/users/create.php b/app/modules_app/users/create.php new file mode 100644 index 0000000..83b9cbe --- /dev/null +++ b/app/modules_app/users/create.php @@ -0,0 +1,56 @@ + 'required', + 'email' => 'required|email', + 'password' => 'required', + 'role' => 'required' +]); + +if ($errors) { + json_error('Validation Failed', 422, $errors); +} + +$db = Database::getInstance(); + +// 3. Encrypt sensitive data +$encryptedName = Encryption::encrypt($data['name']); +$encryptedEmail = Encryption::encrypt($data['email']); +$emailHash = hash('sha256', strtolower($data['email'])); // For fast lookup during login + +// 4. Save to Database +try { + $stmt = $db->prepare("INSERT INTO users (name, email, email_hash, password_hash, role, created_at) VALUES (?, ?, ?, ?, ?, ?)"); + $stmt->execute([ + $encryptedName, + $encryptedEmail, + $emailHash, + password_hash($data['password'], PASSWORD_DEFAULT), + $data['role'], + date('Y-m-d H:i:s') + ]); + + json_success(null, 'تم إضافة المستخدم بنجاح'); +} catch (\Exception $e) { + if (str_contains($e->getMessage(), 'Duplicate entry')) { + json_error('البريد الإلكتروني مسجل مسبقاً', 409); + } + json_error('حدث خطأ أثناء حفظ البيانات', 500); +} diff --git a/app/modules_app/users/index.php b/app/modules_app/users/index.php index c5ab614..3193e34 100644 --- a/app/modules_app/users/index.php +++ b/app/modules_app/users/index.php @@ -1,9 +1,10 @@ prepare("SELECT id, name, email, role, is_active, created_at FROM u $stmt->execute(); $users = $stmt->fetchAll(); +// 4. Decrypt sensitive data for the UI +foreach ($users as &$user) { + // Try to decrypt. If it fails (e.g. data was plain text), keep original. + $decryptedName = Encryption::decrypt($user['name']); + $user['name'] = $decryptedName !== false ? $decryptedName : $user['name']; + + $decryptedEmail = Encryption::decrypt($user['email']); + $user['email'] = $decryptedEmail !== false ? $decryptedEmail : $user['email']; +} + json_success($users); diff --git a/public/index.php b/public/index.php index d58f3dd..51363a3 100644 --- a/public/index.php +++ b/public/index.php @@ -20,6 +20,9 @@ $routes = [ 'v1/auth/refresh' => ['POST', 'auth/refresh.php'], 'v1/auth/logout' => ['POST', 'auth/logout.php'], 'v1/users' => ['GET', 'users/index.php'], + 'v1/users/create' => ['POST', 'users/create.php'], + 'v1/companies' => ['GET', 'companies/index.php'], + 'v1/companies/create' => ['POST', 'companies/create.php'], 'v1/dashboard/stats' => ['GET', 'dashboard/stats.php'], ]; diff --git a/public/shell.php b/public/shell.php index b5652db..69bf140 100644 --- a/public/shell.php +++ b/public/shell.php @@ -30,6 +30,7 @@
@@ -41,7 +42,11 @@

-
+
+ + +
+
@@ -63,6 +68,32 @@
+ +
+
+ + + + + + + + + + + + +
اسم الشركةالرقم الضريبيرقم التسجيلتاريخ الإضافة
+
+
+
@@ -79,7 +110,9 @@ - + + + @@ -88,6 +121,68 @@
+ + +
+
+

إضافة مستخدم جديد 👤

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+

إنشاء شركة جديدة 🏢

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+

أداة تشفير البيانات

+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+ استخدم هذه القيم لتحديث مستخدمي النظام الحاليين يدوياً في قاعدة البيانات إذا أردت. +
+
+ + diff --git a/scripts/migrate.php b/scripts/migrate.php new file mode 100644 index 0000000..d63e4f0 --- /dev/null +++ b/scripts/migrate.php @@ -0,0 +1,105 @@ +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"; +} + +// 3. Create companies table (Updated to match production schema) +try { + $db->exec("CREATE TABLE IF NOT EXISTS companies ( + id INT AUTO_INCREMENT PRIMARY KEY, + tenant_id INT, + name VARCHAR(255) NOT NULL, + name_en VARCHAR(255), + tax_identification_number VARCHAR(50), + commercial_registration_number VARCHAR(50), + address TEXT, + city VARCHAR(100), + contact_email VARCHAR(255), + contact_phone VARCHAR(50), + jofotara_client_id_encrypted TEXT, + jofotara_secret_key_encrypted TEXT, + jofotara_income_source_sequence VARCHAR(50), + certificate_path VARCHAR(255), + certificate_password_encrypted TEXT, + is_jofotara_linked TINYINT(1) DEFAULT 0, + is_active TINYINT(1) DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at DATETIME DEFAULT NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); + echo "[OK] Companies table synchronized with production schema.\n"; +} catch (\Exception $e) { + echo "[ERROR] Synchronizing companies table: " . $e->getMessage() . "\n"; +} + +// 4. Create user_companies pivot table +try { + $db->exec("CREATE TABLE IF NOT EXISTS user_companies ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + company_id INT NOT NULL, + role VARCHAR(50) DEFAULT 'employee', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY user_company (user_id, company_id), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); + echo "[OK] User_companies table created or exists.\n"; +} catch (\Exception $e) { + echo "[ERROR] Creating user_companies table: " . $e->getMessage() . "\n"; +} + +echo "--- Migration Complete ---\n"; diff --git a/scripts/schema.sql b/scripts/schema.sql new file mode 100644 index 0000000..3252c72 --- /dev/null +++ b/scripts/schema.sql @@ -0,0 +1,157 @@ +-- ════════════════════════════════════════════════════════════ +-- مُصادَق — 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) +); \ No newline at end of file