From 13bbc29e0ee1ece042c7d0b5d789914f767b7da0 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sun, 3 May 2026 22:35:31 +0300 Subject: [PATCH] Update: 2026-05-03 22:35:31 --- PROJECT_DOCUMENTATION.md | 2225 ++++++++++++++++++++++++++ app/modules_app/companies/create.php | 2 +- app/modules_app/dashboard/stats.php | 30 +- public/shell.php | 6 +- 4 files changed, 2252 insertions(+), 11 deletions(-) create mode 100644 PROJECT_DOCUMENTATION.md diff --git a/PROJECT_DOCUMENTATION.md b/PROJECT_DOCUMENTATION.md new file mode 100644 index 0000000..f30f96a --- /dev/null +++ b/PROJECT_DOCUMENTATION.md @@ -0,0 +1,2225 @@ +# Musadaq Project Documentation + +This file contains the complete source code of the project (excluding dependencies and sensitive data). + +## File: `push.sh` + +```sh +#!/bin/bash + +# Get current timestamp +TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S") + +echo "🚀 Starting Git Push Process..." +echo "📅 Timestamp: $TIMESTAMP" + +# Add all changes +git add . + +# Commit with timestamp +git commit -m "Update: $TIMESTAMP" + +# Push to origin main explicitly +git push origin main + +echo "✅ Done!" + +``` + +## File: `composer.json` + +```json +{ + "name": "musadaq/platform", + "description": "Jordanian E-Invoicing Automation SaaS", + "type": "project", + "license": "proprietary", + "require": { + "php": ">=8.4", + "ext-pdo": "*", + "ext-pdo_mysql": "*", + "ext-openssl": "*", + "ext-sodium": "*", + "ext-curl": "*", + "ext-mbstring": "*", + "ext-json": "*", + "vlucas/phpdotenv": "^5.6", + "monolog/monolog": "^3.5", + "firebase/php-jwt": "^6.10", + "ramsey/uuid": "^4.7", + "nikic/fast-route": "^1.3", + "predis/predis": "^2.2", + "guzzlehttp/guzzle": "^7.9", + "respect/validation": "^2.3", + "league/flysystem": "^3.28", + "symfony/mailer": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "phpstan/phpstan": "^1.12", + "squizlabs/php_codesniffer": "^3.10" + }, + "autoload": { + "psr-4": { "App\\": "app/" } + }, + "config": { + "optimize-autoloader": true, + "sort-packages": true + } +} + +``` + +## File: `app/modules_app/auth/login.php` + +```php + 'required|email', + 'password' => 'required' +]); + +if ($errors) { + json_error('Validation Failed', 422, $errors); +} + +$email = $data['email']; +$password = $data['password']; + +// 2. DB Check (Using hash for lookup since email is encrypted) +$db = Database::getInstance(); +$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'])) { + json_error('بيانات الدخول غير صحيحة', 401); +} + +// 3. Issue Token +$secret = env('JWT_SECRET'); +if (!$secret || strlen($secret) < 32) { + error_log('FATAL: JWT_SECRET is missing or too short in .env'); + json_error('Server configuration error', 500); +} +$payload = [ + 'user_id' => $user['id'], + 'tenant_id' => $user['tenant_id'], + 'role' => $user['role'], + 'exp' => time() + (15 * 60) // 15 minutes +]; + +$token = JWT::encode($payload, $secret); + +// 4. Update Refresh Token (Hashed before storage for security) +$refreshToken = bin2hex(random_bytes(32)); +$refreshTokenHash = hash('sha256', $refreshToken); +$stmt = $db->prepare("UPDATE users SET refresh_token_hash = ? WHERE id = ?"); +$stmt->execute([$refreshTokenHash, $user['id']]); + +json_success([ + 'access_token' => $token, + 'refresh_token' => $refreshToken, + 'user' => [ + 'id' => $user['id'], + 'name' => (App\Core\Encryption::decrypt($user['name']) ?: $user['name']), + 'email' => (App\Core\Encryption::decrypt($user['email']) ?: $user['email']) + ] +], 'تم تسجيل الدخول بنجاح'); + +``` + +## File: `app/modules_app/auth/logout.php` + +```php +prepare("UPDATE users SET refresh_token_hash = NULL WHERE id = ?"); +$stmt->execute([$userId]); + +json_success(null, 'تم تسجيل الخروج بنجاح'); + +``` + +## File: `app/modules_app/auth/refresh.php` + +```php +prepare("SELECT * FROM users WHERE refresh_token_hash = ? LIMIT 1"); +$stmt->execute([$refreshTokenHash]); +$user = $stmt->fetch(); + +if (!$user) { + json_error('Invalid refresh token', 401); +} + +$secret = env('JWT_SECRET'); +if (!$secret || strlen($secret) < 32) { + error_log('FATAL: JWT_SECRET is missing or too short in .env'); + json_error('Server configuration error', 500); +} +$payload = [ + 'user_id' => $user['id'], + 'role' => $user['role'], + 'exp' => time() + (15 * 60) +]; + +$newToken = JWT::encode($payload, $secret); +$newRefreshToken = bin2hex(random_bytes(32)); +$newRefreshTokenHash = hash('sha256', $newRefreshToken); + +$stmt = $db->prepare("UPDATE users SET refresh_token_hash = ? WHERE id = ?"); +$stmt->execute([$newRefreshTokenHash, $user['id']]); + +json_success([ + 'access_token' => $newToken, + 'refresh_token' => $newRefreshToken +], 'تم تجديد الجلسة بنجاح'); + +``` + +## File: `app/modules_app/dashboard/stats.php` + +```php +query("SELECT COUNT(*) FROM invoices"); + $total = $stmt->fetchColumn(); + + // Pending Invoices + $stmt = $db->query("SELECT COUNT(*) FROM invoices WHERE status = 'pending'"); + $pending = $stmt->fetchColumn(); + + // Approved Invoices + $stmt = $db->query("SELECT COUNT(*) FROM invoices WHERE status = 'approved'"); + $approved = $stmt->fetchColumn(); +} catch (\Exception $e) { + // Fallback if table doesn't exist yet + $total = 0; + $pending = 0; + $approved = 0; +} + +json_success([ + 'total' => $total, + 'pending' => $pending, + 'approved' => $approved +]); + +``` + +## File: `app/modules_app/users/index.php` + +```php +prepare("SELECT id, name, email, role, is_active, created_at FROM users"); +$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); + +``` + +## File: `app/modules_app/users/create.php` + +```php + '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 (tenant_id, name, email, email_hash, password_hash, role, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"); + $stmt->execute([ + $decoded['tenant_id'], + $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); +} + +``` + +## File: `app/modules_app/companies/index.php` + +```php +query("SELECT * FROM companies WHERE deleted_at IS NULL"); +} +// 2. Admin sees all companies in their tenant +else if ($decoded['role'] === 'admin') { + $stmt = $db->prepare("SELECT * FROM companies WHERE tenant_id = ? AND deleted_at IS NULL"); + $stmt->execute([$decoded['tenant_id']]); +} +// 3. Others (accountant, etc) see only their assigned company +else { + // Need to get their assigned company_id from users table first + $stmtUser = $db->prepare("SELECT company_id FROM users WHERE id = ?"); + $stmtUser->execute([$decoded['user_id']]); + $assignedCompanyId = $stmtUser->fetchColumn(); + + $stmt = $db->prepare("SELECT * FROM companies WHERE id = ? AND deleted_at IS NULL"); + $stmt->execute([$assignedCompanyId]); +} + +$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); + +``` + +## File: `app/modules_app/companies/create.php` + +```php + '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') + ]); + + $db->commit(); + json_success(null, 'تم إنشاء الشركة بنجاح'); + +} catch (\Exception $e) { + $db->rollBack(); + json_error('حدث خطأ أثناء حفظ البيانات: ' . $e->getMessage(), 500); +} + +``` + +## File: `app/middleware/HmacMiddleware.php` + +```php + $maxAgeSeconds) { + json_error('Request expired. Check your system clock.', 401); + } + + // 4. Build the expected signature + $body = file_get_contents('php://input'); + $payload = $timestamp . '.' . $body; + $secret = env('HMAC_SECRET_KEY'); + + if (!$secret || strlen($secret) < 32) { + error_log('FATAL: HMAC_SECRET_KEY is missing or too short in .env'); + json_error('Server configuration error', 500); + } + + // 5. Verify using constant-time comparison (prevents timing attacks) + if (!Security::verifySignature($payload, $signature, $secret)) { + error_log("HMAC verification failed for " . ($_SERVER['REQUEST_URI'] ?? '')); + json_error('Invalid request signature', 401); + } + } +} + +``` + +## File: `app/middleware/AuthMiddleware.php` + +```php + $ts > ($now - $timeWindow)) + ); + } + } + + if (count($requests) >= $maxRequests) { + flock($fp, LOCK_UN); + fclose($fp); + + header('Retry-After: ' . $timeWindow); + json_error('Too Many Requests. Please slow down.', 429); + } + + // Record this request + $requests[] = $now; + + // Write updated data back + ftruncate($fp, 0); + rewind($fp); + fwrite($fp, json_encode($requests)); + + } finally { + flock($fp, LOCK_UN); + fclose($fp); + } + } +} + +``` + +## File: `app/core/Validator.php` + +```php + $rule) { + if (str_contains($rule, 'required') && (empty($data[$field]) && $data[$field] !== '0')) { + $errors[$field] = "The {$field} field is required."; + } + if (str_contains($rule, 'email') && !empty($data[$field]) && !filter_var($data[$field], FILTER_VALIDATE_EMAIL)) { + $errors[$field] = "The {$field} must be a valid email address."; + } + } + return $errors; + } +} + +``` + +## File: `app/core/Encryption.php` + +```php + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]); + } catch (PDOException $e) { + http_response_code(500); + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'message' => 'Database connection failed']); + exit; + } + } + + return self::$instance; + } +} + +``` + +## File: `app/core/Security.php` + +```php + $value) { + $data[$key] = self::sanitize($value); + } + } else if (is_string($data)) { + $data = htmlspecialchars(strip_tags(trim($data)), ENT_QUOTES, 'UTF-8'); + } + return $data; + } + + public static function generateRandomString(int $length = 64): string + { + return bin2hex(random_bytes($length / 2)); + } + + public static function sign(string $data, string $secret): string + { + return hash_hmac('sha256', $data, $secret); + } + + public static function verifySignature(string $data, string $signature, string $secret): bool + { + $expected = self::sign($data, $secret); + return hash_equals($expected, $signature); + } +} + +``` + +## File: `app/core/JWT.php` + +```php + 'JWT', 'alg' => 'HS256']); + $base64UrlHeader = self::base64UrlEncode($header); + $base64UrlPayload = self::base64UrlEncode(json_encode($payload)); + + $signature = hash_hmac('sha256', $base64UrlHeader . "." . $base64UrlPayload, $secret, true); + $base64UrlSignature = self::base64UrlEncode($signature); + + return $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature; + } + + public static function decode(string $token, string $secret): ?array + { + $parts = explode('.', $token); + if (count($parts) !== 3) return null; + + [$header, $payload, $signature] = $parts; + + $expectedSignature = self::base64UrlEncode(hash_hmac('sha256', $header . "." . $payload, $secret, true)); + + if (!hash_equals($expectedSignature, $signature)) return null; + + $decodedPayload = json_decode(self::base64UrlDecode($payload), true); + + // Check expiry + if (isset($decodedPayload['exp']) && $decodedPayload['exp'] < time()) return null; + + return $decodedPayload; + } +} + +``` + +## File: `app/bootstrap/init.php` + +```php + $success, + 'data' => $data, // Return real data to client + 'message' => $message, + 'timestamp' => date('c') + ], JSON_UNESCAPED_UNICODE); + + exit; +} + +function json_error(string $message, int $code = 400, $errors = null) { + json_response(false, $errors, $message, $code); +} + +function json_success($data = null, ?string $message = 'Success', int $code = 200) { + json_response(true, $data, $message, $code); +} + +``` + +## File: `app/bootstrap/env.php` + +```php + $_ENV['DB_HOST'] ?? '127.0.0.1', + 'port' => $_ENV['DB_PORT'] ?? '3306', + 'database' => $_ENV['DB_DATABASE'] ?? 'musadaqDb', + 'username' => $_ENV['DB_USERNAME'] ?? 'musadaqUser', + 'password' => $_ENV['DB_PASSWORD'] ?? '', + 'charset' => $_ENV['DB_CHARSET'] ?? 'utf8mb4', +]; + +``` + +## File: `app/helpers/helpers.php` + +```php +"; + var_dump($v); + echo ""; + } + die(); + } +} + +``` + +## File: `public/login.php` + +```php + + + + + + تسجيل الدخول | مُصادَق + + + + + + + + + + + + + +
+ +
+
+
م
+

مرحباً بك في مُصادَق

+

نظام أتمتة الفواتير الضريبية الذكي

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

ليس لديك حساب؟ ابدأ التجربة المجانية

+
+
+ + + + + +``` + +## File: `public/index.php` + +```php + [allowed_method, module_file] +$routes = [ + 'v1/auth/login' => ['POST', 'auth/login.php'], + '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'], +]; + +if (isset($routes[$route])) { + [$allowedMethod, $moduleFile] = $routes[$route]; + + // H1 Fix: Enforce HTTP Method + if ($_SERVER['REQUEST_METHOD'] !== $allowedMethod) { + header("Allow: {$allowedMethod}"); + json_error("Method Not Allowed. Use {$allowedMethod}.", 405); + } + + $file = APP_PATH . '/modules_app/' . $moduleFile; + if (file_exists($file)) { + require_once $file; + } else { + json_error("Endpoint file missing: {$route}", 500); + } +} else { + if (str_starts_with($route, 'v1/')) { + json_error("Not Found: {$route}", 404); + } else { + include __DIR__ . '/shell.php'; + exit; + } +} + +``` + +## File: `public/tool-encrypt.php` + +```php + + + + + + أداة التشفير | مُصادَق + + + +
+

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

+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+ استخدم هذه القيم لتحديث مستخدمي النظام الحاليين يدوياً في قاعدة البيانات إذا أردت. +
+
+ + + +``` + +## File: `public/shell.php` + +```php + + + + + + مُصادَق | لوحة التحكم + + + + + + + + + +
+ + + + +
+
+

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

إجمالي الفواتير

+

+
+
+

قيد المعالجة

+

+
+
+

تم الاعتماد

+

+
+
+
+ + +
+
+ + + + + + + + + + + + +
اسم الشركةالرقم الضريبيرقم التسجيلتاريخ الإضافة
+
+
+ + +
+
+ + + + + + + + + + + +
الاسمالبريد الإلكترونيالدور
+
+
+
+
+ + +
+
+

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

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

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

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ + + + + +``` + +## File: `public/register.php` + +```php + + + + + + إنشاء حساب | مُصادَق + + + + + + + + + + + + + +
+
+

ابدأ مع مُصادَق

+

سجل شركتك الآن وابدأ أتمتة فواتيرك

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

لديك حساب بالفعل؟ تسجيل الدخول

+
+
+ + + + + +``` + +## File: `public/api.php` + +```php + +``` + +## File: `scripts/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: `scripts/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"; +} + +// (Table creation logic removed because it is properly handled by schema.sql) + +echo "--- Migration Complete ---\n"; + +``` + diff --git a/app/modules_app/companies/create.php b/app/modules_app/companies/create.php index 9ab3bab..9ee7d27 100644 --- a/app/modules_app/companies/create.php +++ b/app/modules_app/companies/create.php @@ -49,7 +49,7 @@ try { "); $stmt->execute([ - $decoded['user_id'], // Using current admin as tenant_id + $decoded['tenant_id'], // Correctly using tenant_id from JWT $encryptedName, $encryptedNameEn, $data['tax_identification_number'], diff --git a/app/modules_app/dashboard/stats.php b/app/modules_app/dashboard/stats.php index b4a1d88..a80f30f 100644 --- a/app/modules_app/dashboard/stats.php +++ b/app/modules_app/dashboard/stats.php @@ -1,30 +1,46 @@ $tenantId]; + + // If accountant or employee restricted to a company + if (($role === 'accountant' || $role === 'viewer') && $companyId) { + $where .= " AND company_id = :company_id"; + $params[':company_id'] = $companyId; + } + // Total Invoices - $stmt = $db->query("SELECT COUNT(*) FROM invoices"); + $stmt = $db->prepare("SELECT COUNT(*) FROM invoices $where"); + $stmt->execute($params); $total = $stmt->fetchColumn(); // Pending Invoices - $stmt = $db->query("SELECT COUNT(*) FROM invoices WHERE status = 'pending'"); + $stmt = $db->prepare("SELECT COUNT(*) FROM invoices $where AND status = 'pending'"); + $stmt->execute($params); $pending = $stmt->fetchColumn(); // Approved Invoices - $stmt = $db->query("SELECT COUNT(*) FROM invoices WHERE status = 'approved'"); + $stmt = $db->prepare("SELECT COUNT(*) FROM invoices $where AND status = 'approved'"); + $stmt->execute($params); $approved = $stmt->fetchColumn(); + } catch (\Exception $e) { - // Fallback if table doesn't exist yet $total = 0; $pending = 0; $approved = 0; diff --git a/public/shell.php b/public/shell.php index d25a9e8..2d45f6f 100644 --- a/public/shell.php +++ b/public/shell.php @@ -31,7 +31,7 @@
@@ -43,8 +43,8 @@

- - + +