إجمالي الفواتير
+ +قيد المعالجة
+ +تم الاعتماد
+ +| اسم الشركة | +الرقم الضريبي | +رقم التسجيل | +تاريخ الإضافة | +
|---|---|---|---|
| + | + | + | + |
| الاسم | +البريد الإلكتروني | +الدور | +
|---|---|---|
| + | + | + + | +
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 + + +
+ + +نظام أتمتة الفواتير الضريبية الذكي
+ليس لديك حساب؟ ابدأ التجربة المجانية
+إجمالي الفواتير
+ +قيد المعالجة
+ +تم الاعتماد
+ +| اسم الشركة | +الرقم الضريبي | +رقم التسجيل | +تاريخ الإضافة | +
|---|---|---|---|
| + | + | + | + |
| الاسم | +البريد الإلكتروني | +الدور | +
|---|---|---|
| + | + | + + | +
سجل شركتك الآن وابدأ أتمتة فواتيرك
+لديك حساب بالفعل؟ تسجيل الدخول
+