commit 8e429d8313eeba8525265a8aa7c47c43b4a45180 Author: Hamza-Ayed Date: Thu May 21 00:40:47 2026 +0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..01cc53a --- /dev/null +++ b/.gitignore @@ -0,0 +1,97 @@ +# ========================================== +# 🛠️ General / System Files +# ========================================== +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db +*.log +*.tmp +*.bak + +# ========================================== +# 💻 IDE & Editors +# ========================================== +.idea/ +.vscode/ +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# ========================================== +# 🐘 Backend (PHP / Composer) +# ========================================== +backend/.env +backend/.env.local +backend/.env.development +backend/.env.staging +backend/.env.production +backend/vendor/ +backend/.phpunit.result.cache +# backend/composer.lock (Recommended to commit for applications) + +# ========================================== +# 🟢 Node.js / Frontend / JS +# ========================================== +node_modules/ +frontend/node_modules/ +frontend/dist/ +frontend/build/ +frontend/.next/ +frontend/out/ +frontend/.env +frontend/.env.local +frontend/.env.development.local +frontend/.env.test.local +frontend/.env.production.local +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.eslintcache + +# ========================================== +# 📱 Mobile (Flutter / Dart / iOS / Android) +# ========================================== +# Flutter/Dart +.dart_tool/ +.packages +.pub-cache/ +.pub/ +/build/ +mobile/build/ +mobile/.dart_tool/ +mobile/.packages +mobile/.flutter-plugins +mobile/.flutter-plugins-dependencies +mobile/.metadata +mobile/generated/ + +# Android specific +mobile/android/local.properties +mobile/android/gen/ +mobile/android/out/ +mobile/android/app/src/main/res/raw/ +*.keystore +*.jks +*.ap_ +*.apk +*.aab + +# iOS specific +mobile/ios/Flutter/flutter_export_environment.sh +mobile/ios/Flutter/Generated.xcconfig +mobile/ios/Flutter/flutter_assets/ +mobile/ios/Pods/ +mobile/ios/.symlinks/ +mobile/ios/Flash/ +mobile/ios/DerivedData/ +.xcworkspace/ +*.mode1v3 +*.mode2v3 +*.storyboardc +*.nib diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..0397366 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,18 @@ +# Application Settings +APP_NAME=Nabeh +APP_ENV=development +APP_DEBUG=true +APP_URL=http://localhost:8000 + +# Main Master Database Configuration +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=nabeh_master +DB_USERNAME=root +DB_PASSWORD= + +# AI Model Configuration +GEMINI_API_KEY= + +# Messaging Gateway Settings +WHATSAPP_GATEWAY_URL=http://localhost:3722 diff --git a/backend/app/Controllers/BaseController.php b/backend/app/Controllers/BaseController.php new file mode 100644 index 0000000..ed52c1d --- /dev/null +++ b/backend/app/Controllers/BaseController.php @@ -0,0 +1,49 @@ + 'required|email']) + * @return array Empty if valid, otherwise contains error messages + */ + protected function validate(Request $request, array $rules): array + { + $errors = []; + $data = $request->getBody(); + + foreach ($rules as $field => $constraints) { + $value = $data[$field] ?? null; + $constraintsArray = explode('|', $constraints); + + foreach ($constraintsArray as $constraint) { + if ($constraint === 'required') { + if ($value === null || $value === '') { + $errors[$field][] = "The {$field} field is required."; + } + } elseif ($constraint === 'email') { + if ($value !== null && $value !== '' && !filter_var($value, FILTER_VALIDATE_EMAIL)) { + $errors[$field][] = "The {$field} must be a valid email address."; + } + } elseif (strpos($constraint, 'min:') === 0) { + $min = (int) substr($constraint, 4); + if ($value !== null && strlen((string)$value) < $min) { + $errors[$field][] = "The {$field} must be at least {$min} characters."; + } + } + } + } + + return $errors; + } +} diff --git a/backend/app/Core/Database.php b/backend/app/Core/Database.php new file mode 100644 index 0000000..06d24ff --- /dev/null +++ b/backend/app/Core/Database.php @@ -0,0 +1,99 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]; + + try { + self::$instance = new PDO($dsn, $username, $password, $options); + } catch (PDOException $e) { + // Log the exact error internally but hide sensitive DSN on production + error_log("Database Connection Error: " . $e->getMessage()); + throw new PDOException("Could not connect to the database. Check database settings."); + } + } + + return self::$instance; + } + + /** + * Shorthand execute statement with parameters + * + * @param string $sql + * @param array $params + * @return \PDOStatement + */ + public static function query(string $sql, array $params = []): \PDOStatement + { + $pdo = self::getConnection(); + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + return $stmt; + } + + /** + * Retrieve all matching records + */ + public static function select(string $sql, array $params = []): array + { + return self::query($sql, $params)->fetchAll(); + } + + /** + * Retrieve single matching record + */ + public static function selectOne(string $sql, array $params = []) + { + return self::query($sql, $params)->fetch() ?: null; + } + + /** + * Insert record and return last inserted ID + */ + public static function insert(string $sql, array $params = []): string + { + $pdo = self::getConnection(); + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + return $pdo->lastInsertId(); + } + + /** + * Execute generic non-query SQL (Update/Delete) and return affected rows + */ + public static function execute(string $sql, array $params = []): int + { + return self::query($sql, $params)->rowCount(); + } +} diff --git a/backend/app/Core/Env.php b/backend/app/Core/Env.php new file mode 100644 index 0000000..0ca3daf --- /dev/null +++ b/backend/app/Core/Env.php @@ -0,0 +1,86 @@ +method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET'); + + // Extract clean path without query parameters + $uri = $_SERVER['REQUEST_URI'] ?? '/'; + $path = explode('?', $uri)[0]; + $this->path = '/' . trim($path, '/'); + + $this->queryParams = $_GET; + $this->headers = $this->extractHeaders(); + $this->bodyParams = $this->parseBody(); + } + + public function getMethod(): string + { + return $this->method; + } + + public function getPath(): string + { + return $this->path; + } + + public function getQueryParams(): array + { + return $this->queryParams; + } + + public function getQuery(string $key, $default = null) + { + return $this->queryParams[$key] ?? $default; + } + + public function getBody(): array + { + return $this->bodyParams; + } + + public function get(string $key, $default = null) + { + return $this->bodyParams[$key] ?? ($this->queryParams[$key] ?? $default); + } + + public function getHeaders(): array + { + return $this->headers; + } + + public function getHeader(string $key, $default = null): ?string + { + $keyLower = strtolower($key); + return $this->headers[$keyLower] ?? $default; + } + + private function extractHeaders(): array + { + $headers = []; + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) == 'HTTP_') { + $headers[strtolower(str_replace('_', '-', substr($name, 5)))] = $value; + } elseif ($name == 'CONTENT_TYPE') { + $headers['content-type'] = $value; + } elseif ($name == 'CONTENT_LENGTH') { + $headers['content-length'] = $value; + } + } + return $headers; + } + + private function parseBody(): array + { + if ($this->method === 'GET') { + return []; + } + + $contentType = $this->getHeader('content-type', ''); + + if (strpos($contentType, 'application/json') !== false) { + $input = file_get_contents('php://input'); + $data = json_decode($input, true); + return is_array($data) ? $data : []; + } + + return $_POST; + } +} diff --git a/backend/app/Core/Response.php b/backend/app/Core/Response.php new file mode 100644 index 0000000..ddeac34 --- /dev/null +++ b/backend/app/Core/Response.php @@ -0,0 +1,93 @@ +statusCode = $code; + return $this; + } + + public function getStatusCode(): int + { + return $this->statusCode; + } + + public function setHeader(string $name, string $value): self + { + $this->headers[$name] = $value; + return $this; + } + + /** + * Send JSON response and terminate execution + * + * @param mixed $data + * @param int $code + * @return void + */ + public function json($data, int $code = 200): void + { + $this->setStatusCode($code); + $this->setHeader('Content-Type', 'application/json; charset=utf-8'); + + // Setup base CORS headers for our API + $this->setHeader('Access-Control-Allow-Origin', '*'); + $this->setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + $this->setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With'); + + $this->sendHeaders(); + http_response_code($this->statusCode); + + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + exit; + } + + /** + * Send success JSON response + */ + public function success(string $message, array $data = [], int $code = 200): void + { + $this->json([ + 'status' => 'success', + 'message' => $message, + 'data' => $data + ], $code); + } + + /** + * Send error JSON response + */ + public function error(string $message, int $code = 400, array $errors = []): void + { + $response = [ + 'status' => 'error', + 'message' => $message + ]; + + if (!empty($errors)) { + $response['errors'] = $errors; + } + + $this->json($response, $code); + } + + private function sendHeaders(): void + { + if (headers_sent()) { + return; + } + + foreach ($this->headers as $name => $value) { + header("{$name}: {$value}"); + } + } +} diff --git a/backend/app/Core/Router.php b/backend/app/Core/Router.php new file mode 100644 index 0000000..400e182 --- /dev/null +++ b/backend/app/Core/Router.php @@ -0,0 +1,127 @@ +addRoute('GET', $path, $handler, $middleware); + } + + /** + * Define a POST route + */ + public function post(string $path, $handler, array $middleware = []): void + { + $this->addRoute('POST', $path, $handler, $middleware); + } + + /** + * Define a PUT route + */ + public function put(string $path, $handler, array $middleware = []): void + { + $this->addRoute('PUT', $path, $handler, $middleware); + } + + /** + * Define a DELETE route + */ + public function delete(string $path, $handler, array $middleware = []): void + { + $this->addRoute('DELETE', $path, $handler, $middleware); + } + + /** + * Add global middleware applied to all routes + */ + public function use($middleware): void + { + $this->globalMiddleware[] = $middleware; + } + + private function addRoute(string $method, string $path, $handler, array $middleware): void + { + // Convert path matching expressions: /api/tickets/{id} -> regex + $pattern = preg_replace('/\{([a-zA-Z0-9_]+)\}/', '(?P<$1>[^/]+)', $path); + $pattern = '#^' . $pattern . '$#'; + + $this->routes[] = [ + 'method' => $method, + 'path' => $path, + 'pattern' => $pattern, + 'handler' => $handler, + 'middleware' => $middleware + ]; + } + + /** + * Match current request and execute middleware and controller action + */ + public function dispatch(Request $request, Response $response): void + { + $method = $request->getMethod(); + $path = $request->getPath(); + + // Handle CORS Preflight Preemptively + if ($method === 'OPTIONS') { + $response->setHeader('Access-Control-Allow-Origin', '*'); + $response->setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + $response->setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With'); + $response->setStatusCode(200); + exit; + } + + foreach ($this->routes as $route) { + if ($route['method'] === $method && preg_match($route['pattern'], $path, $matches)) { + // Filter named captures from regex match + $params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY); + + // Run global middleware first + foreach ($this->globalMiddleware as $mw) { + $mwInstance = new $mw(); + $mwInstance->handle($request, $response); + } + + // Run route specific middleware + foreach ($route['middleware'] as $mw) { + $mwInstance = new $mw(); + $mwInstance->handle($request, $response); + } + + // Execute Controller + $handler = $route['handler']; + if (is_array($handler) && count($handler) === 2) { + list($controllerClass, $action) = $handler; + if (class_exists($controllerClass)) { + $controller = new $controllerClass(); + if (method_exists($controller, $action)) { + // Call action with Request, Response and URI dynamic parameters + call_user_func_array([$controller, $action], array_merge([$request, $response], $params)); + return; + } + } + } elseif (is_callable($handler)) { + call_user_func_array($handler, array_merge([$request, $response], $params)); + return; + } + + $response->error("Handler error for route: {$path}", 500); + return; + } + } + + // Route not found + $response->error("Route not found: [{$method}] {$path}", 404); + } +} diff --git a/backend/app/Models/BaseModel.php b/backend/app/Models/BaseModel.php new file mode 100644 index 0000000..8f65cb7 --- /dev/null +++ b/backend/app/Models/BaseModel.php @@ -0,0 +1,90 @@ + $id]); + } + + /** + * Insert a new record + * + * @param array $data Assocation array of columns and values + * @return string Last inserted primary key ID + */ + public static function create(array $data): string + { + $table = static::$table; + $columns = implode(', ', array_keys($data)); + $placeholders = ':' . implode(', :', array_keys($data)); + + $sql = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})"; + return Database::insert($sql, $data); + } + + /** + * Update an existing record + * + * @param mixed $id Primary key ID + * @param array $data Associative array of changes + * @return int Affected rows count + */ + public static function update($id, array $data): int + { + $table = static::$table; + $primaryKey = static::$primaryKey; + + $sets = []; + foreach (array_keys($data) as $column) { + $sets[] = "{$column} = :{$column}"; + } + $setSql = implode(', ', $sets); + + $sql = "UPDATE {$table} SET {$setSql} WHERE {$primaryKey} = :_id"; + + $params = $data; + $params['_id'] = $id; + + return Database::execute($sql, $params); + } + + /** + * Delete record by ID + */ + public static function delete($id): int + { + $table = static::$table; + $primaryKey = static::$primaryKey; + + $sql = "DELETE FROM {$table} WHERE {$primaryKey} = :id"; + return Database::execute($sql, ['id' => $id]); + } +} diff --git a/backend/app/bootstrap.php b/backend/app/bootstrap.php new file mode 100644 index 0000000..d234978 --- /dev/null +++ b/backend/app/bootstrap.php @@ -0,0 +1,48 @@ +getMessage()); +} + +// 3. Configure Error Reporting based on environment +$isDebug = filter_var(getenv('APP_DEBUG') ?: true, FILTER_VALIDATE_BOOLEAN); + +if ($isDebug) { + ini_set('display_errors', '1'); + ini_set('display_startup_errors', '1'); + error_reporting(E_ALL); +} else { + ini_set('display_errors', '0'); + error_reporting(0); +} diff --git a/backend/composer.json b/backend/composer.json new file mode 100644 index 0000000..142dcc7 --- /dev/null +++ b/backend/composer.json @@ -0,0 +1,18 @@ +{ + "name": "nabeh/backend", + "description": "Nabeh Customer Service AI Platform Backend API", + "type": "project", + "license": "proprietary", + "require": { + "php": ">=8.1" + }, + "autoload": { + "psr-4": { + "App\\": "app/" + } + }, + "config": { + "optimize-autoloader": true, + "sort-packages": true + } +} diff --git a/backend/public/index.php b/backend/public/index.php new file mode 100644 index 0000000..8cc7996 --- /dev/null +++ b/backend/public/index.php @@ -0,0 +1,34 @@ +get('/api/health', function ($request, $response) { + $response->json([ + 'status' => 'success', + 'message' => 'Nabeh API is healthy', + 'details' => [ + 'app_name' => getenv('APP_NAME') ?: 'Nabeh', + 'environment' => getenv('APP_ENV') ?: 'development', + 'php_version' => PHP_VERSION, + 'time' => date('Y-m-d H:i:s') + ] + ]); +}); + +// 4. Dispatch the request +$router->dispatch($request, $response); diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..b0832b3 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# --- Configuration --- +SERVER_USER="root" +SERVER_IP="194.163.173.157" +SERVER_PATH="/home/intaleqapp-nabeh/htdocs/nabeh.intaleqapp.com" +GIT_BRANCH="main" + +# Colors for terminal styling +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +echo -e "${GREEN}🚀 Starting Nabeh Sync & Deploy...${NC}" + +# 1. Commit local changes automatically (with date/time or custom message) +if [ -n "$(git status --porcelain)" ]; then + COMMIT_MSG=${1:-"Deploy: $(date '+%Y-%m-%d %H:%M:%S')"} + echo -e "${YELLOW}Staging and committing changes with message: ${COMMIT_MSG}${NC}" + git add . + git commit -m "$COMMIT_MSG" + echo -e "${GREEN}✅ Local commit created successfully!${NC}" +else + echo -e "ℹ️ No local changes to commit." +fi + +# 2. Push to Git Remote +echo -e "${GREEN}📤 Pushing changes to remote repository (${GIT_BRANCH})...${NC}" +git push origin "$GIT_BRANCH" + +if [ $? -ne 0 ]; then + echo -e "${RED}❌ Git Push failed! Deployment aborted.${NC}" + exit 1 +fi +echo -e "${GREEN}✅ Successfully pushed to remote repository!${NC}" + +# 3. Connect to server via SSH and pull updates +echo -e "${GREEN}🌐 Connecting to server and pulling updates...${NC}" +ssh -o ConnectTimeout=5 "${SERVER_USER}@${SERVER_IP}" " + cd ${SERVER_PATH} && \ + git pull origin ${GIT_BRANCH} && \ + if [ -f 'backend/composer.json' ]; then + echo '📦 Updating composer dependencies on server...' && \ + cd backend && \ + composer install --no-dev --optimize-autoloader + fi +" + +if [ $? -eq 0 ]; then + echo -e "${GREEN}==========================================${NC}" + echo -e "✨ Nabeh deployment synced perfectly! " + echo -e "==========================================${NC}" +else + echo -e "${RED}❌ Server update failed! Check your connection or SSH setup.${NC}" + exit 1 +fi diff --git a/implementation_plan.md b/implementation_plan.md new file mode 100644 index 0000000..69259f9 --- /dev/null +++ b/implementation_plan.md @@ -0,0 +1,234 @@ +# خطة تنفيذ المعمارية الخلفية ونظام النشر المؤتمت — تطبيق نبيه (Nabeh) + +
+ +توضح هذه الخطة التفاصيل الهيكلية والخطوات العملية لبناء النواة البرمجية للخلفية (Backend Base Architecture) باستخدام **Pure PHP OOP** وبناء نظام نشر تلقائي آمن عبر **Git/SSH** إلى السيرفر. + +--- + +## أولاً: تقسيم المشروع إلى مراحل (Milestones) + +من أجل ضمان الانتقال السلس وبناء أساسات متينة وقابلة للتطوير، سنقسم العمل على الخلفية والنشر إلى المراحل التالية: + +### المرحلة 1: بناء نواة الـ MVC وتهيئة الهيكل البرمجي +- بناء هيكل المجلدات المنظم وفصل الملفات العامة (Public) عن الكود البرمجي الأساسي. +- إعداد نظام التحميل التلقائي للملفات (Autoloader) المتوافق مع معيار PSR-4. +- برمجة موجه الطلبات (Router) المتوافق مع خادم Nginx. +- برمجة قارئ ملفات البيئة المتطور والآمن (`.env` Reader) مع تخزينه خارج المسار العام لضمان أعلى مستويات الحماية. + +### المرحلة 2: تصميم قاعدة البيانات ونواة الاتصال (Database & Core) +- تصميم ملف تهيئة الاتصال بقاعدة البيانات باستخدام PDO. +- بناء الموديل الرئيسي (Base Model) والمتحكم الرئيسي (Base Controller) لإدارة المدخلات والمخرجات (JSON Responses). +- بناء مخطط الجداول الأساسية لقاعدة البيانات (الهيكل متعدد المستأجرين - Multi-Tenant Prep). + +### المرحلة 3: نظام النشر التلقائي للسيرفر (`deploy.sh`) +- كتابة سكربت النشر الآمن لرفع التحديثات لـ Git وعمل Pull تلقائي وإعادة بناء التهيئات على السيرفر. +- إعداد البنية التحتية للمجلدات على السيرفر وربط الـ Subdomain. + +--- + +## ثانياً: معمارية مجلدات الخلفية المقترحة (Directory Structure) + +سنقوم بتنظيم كود الخلفية داخل مجلد `backend/` لحماية كود التطبيق وملف البيانات الحساسة (`.env`) من الوصول المباشر من المتصفح، حيث سيكون مجلد `public/` هو المجلد الوحيد المتاح للعامة والمربوط بالـ Document Root في Nginx. + +الهيكل المستهدف للمجلدات هو كالتالي: + +
+ +``` +nabeh/ +├── backend/ +│ ├── app/ +│ │ ├── Controllers/ # متحكمات معالجة الطلبات +│ │ │ ├── BaseController.php +│ │ │ └── AuthController.php +│ │ ├── Models/ # موديلات قواعد البيانات +│ │ │ ├── BaseModel.php +│ │ │ └── Tenant.php +│ │ ├── Core/ # نواة التطبيق البرمجية +│ │ │ ├── Router.php +│ │ │ ├── Database.php +│ │ │ ├── Request.php +│ │ │ ├── Response.php +│ │ │ └── Env.php +│ │ └── Middlewares/ # فلاتر التحقق والوساطة +│ │ └── AuthMiddleware.php +│ ├── config/ # ملفات الإعدادات العامة +│ │ └── app.php +│ ├── public/ # المجلد الوحيد المتاح للمتصفح +│ │ └── index.php # نقطة الدخول الموحدة (Front Controller) +│ ├── .env # ملف البيئة الحساس (مخفي تماماً) +│ ├── .env.example +│ └── composer.json # لإدارة التحميل التلقائي PSR-4 وحزم التطبيق +``` + +
+ +--- + +## ثالثاً: تصميم النواة البرمجية (Core Architecture) + +### 1. نقطة الدخول الموحدة `backend/public/index.php` +يقوم هذا الملف بتهيئة بيئة العمل، تحميل الملفات تلقائياً، قراءة متغيرات البيئة، ثم تمرير الطلب إلى الـ Router. + +### 2. قارئ البيئة الآمن `backend/app/Core/Env.php` +يقرأ ملف `.env` المتواجد خارج المجلد العام ويقوم بتحميل المتغيرات داخل `$_ENV` و `getenv()` بأمان تام. + +### 3. نظام توجيه الطلبات `backend/app/Core/Router.php` +نظام توجيه خفيف وقوي يدعم تعبيرات Regex، ويتيح تعريف مسارات من نوع GET و POST و PUT و DELETE وتمرير البارامترات للمتحكمات (Controllers) وتطبيق فلاتر الوساطة (Middlewares). + +--- + +## رابعاً: نظام النشر المؤتمت (`deploy.sh`) + +سنقوم بإنشاء سكربت النشر الذكي في الجذر الرئيسي للمشروع لتبسيط عملية الدفع والتحديث. سيقوم السكربت بالخطوات التالية بشكل تفاعلي وآمن: + +1. التأكد من كتابة تعليق الالتزام (Commit Message). +2. إضافة كافة التغييرات محلياً والدفع إلى الفرع الرئيسي على GitHub/GitLab. +3. الاتصال بالسيرفر عبر بروتوكول SSH بشكل آمن. +4. التوجه إلى مجلد المشروع على السيرفر وسحب الكود البرمجي المحدث (`git pull origin main`). +5. تطبيق أي أوامر تهيئة مثل تثبيت حزم الملحقات (`composer install`) أو تنظيف الكاش. + +محتوى سكربت `deploy.sh` المقترح: + +
+ +```bash +#!/bin/bash + +# ========================================== +# سكريبت النشر الآلي وتحديث السيرفر - تطبيق نبيه +# ========================================== + +# 1. إعدادات السيرفر والمتغيرات الأساسية +SERVER_USER="hamza" # اسم مستخدم السيرفر +SERVER_IP="your-server-ip" # عنوان السيرفر +SERVER_PATH="/var/www/nabeh" # مسار المشروع على السيرفر +GIT_BRANCH="main" # الفرع الرئيسي للنشر + +echo "==========================================" +echo "🚀 بدء عملية النشر والتحديث لتطبيق (نبيه)..." +echo "==========================================" + +# 2. التأكد من حالة التغييرات المحلية +if [ -z "$(git status --porcelain)" ]; then + echo "ℹ️ لا توجد تغييرات برمجية غير محفوظة للنشر." +else + # طلب وصف للتحديثات (Commit Message) + echo "✍️ الرجاء إدخال رسالة الالتزام (Commit Message):" + read commit_msg + + if [ -z "$commit_msg" ]; then + commit_msg="Update: Automatic deployment via deploy.sh" + fi + + # الإضافة والالتزام البرمجي + git add . + git commit -m "$commit_msg" +fi + +# 3. الدفع إلى Git Remote +echo "📤 دفع التحديثات إلى المستودع البعيد (Git Push)..." +git push origin $GIT_BRANCH + +if [ $? -ne 0 ]; then + echo "❌ فشلت عملية الدفع البرمجي (Git Push). تم إلغاء النشر." + exit 1 +fi + +echo "✅ تم رفع الكود بنجاح إلى المستودع البرمجي." + +# 4. الاتصال بالسيرفر وتحديث الكود (Git Pull) +echo "🌐 الاتصال بالسيرفر وسحب الكود المحدث (Git Pull)..." +ssh ${SERVER_USER}@${SERVER_IP} << EOF + cd ${SERVER_PATH} + echo "📁 الانتقال إلى مجلد المشروع: ${SERVER_PATH}" + + echo "📥 سحب التحديثات من الفرع ${GIT_BRANCH}..." + git pull origin ${GIT_BRANCH} + + if [ -f "backend/composer.json" ]; then + echo "📦 تحديث الحزم البرمجية والتحميل التلقائي (Composer)..." + cd backend + composer install --no-dev --optimize-autoloader + cd .. + fi + + echo "🎉 تم تحديث السيرفر وتثبيت التحديثات بنجاح!" +EOF + +echo "==========================================" +echo "✨ تمت عملية النشر والتحديث بالكامل بنجاح!" +echo "==========================================" +``` + +
+ +--- + +## خامساً: خطة إعداد الخادم والـ Subdomain + +لتشغيل نظام الـ Front Controller والمسارات الديناميكية بشكل متوافق مع **Nginx** وبدون الحاجة لملفات `.htaccess` الخاصة بـ Apache، يجب إعداد ملف تهيئة الـ Subdomain (مثال: `api.nabeh.sa` أو `app.nabeh.sa`) ليوجه الطلبات إلى المجلد العام `backend/public/`: + +إعداد Nginx المقترح للمخدم: + +
+ +```nginx +server { + listen 80; + listen [::]:80; + + server_name api.nabeh.sa; # النطاق الفرعي للتطبيق + + # تحديد مسار المجلد العام فقط + root /var/www/nabeh/backend/public; + index index.php index.html; + + charset utf-8; + + # توجيه كافة الطلبات إلى index.php لدعم الـ Router + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location = /favicon.ico { access_log off; log_not_found off; } + location = /robots.txt { access_log off; log_not_found off; } + + error_page 404 /index.php; + + # تشغيل ملفات PHP وتكاملها مع PHP-FPM + location ~ \.php$ { + fastcgi_pass unix:/var/run/php/php8.2-fpm.sock; # حسب إصدار PHP المثبت + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + include fastcgi_params; + } + + # منع الوصول تماماً لأي ملفات حساسة أو مخفية + location ~ /\.(?!well-known).* { + deny all; + } +} +``` + +
+ +--- + +## سادساً: خطة التحقق والتدقيق (Verification Plan) + +### التحقق الذاتي والمحلي: +1. **التحقق من المسارات (Router Testing)**: إنشاء طلبات محلية للتحقق من تشغيل المسارات المختلفة (GET/POST) وإرجاع بيانات JSON صحيحة. +2. **التحقق من حماية متغيرات البيئة (`.env` Security)**: محاولة استعراض ملف `.env` عبر المسار المباشر للتأكد من حظر الوصول العام إليه، وقراءته بنجاح من داخل تطبيق PHP. +3. **فحص التحميل التلقائي**: التحقق من استدعاء كلاسات الـ Controllers والموديلات دون الحاجة لكتابة `require_once` يدوياً لكل ملف. + +--- + +## أسئلة للنقاش والمراجعة البرمجية + +> [!NOTE] +> 1. **عنوان خادم الـ SSH والمسار**: هل تود ملء بيانات السيرفر الخاصة بك (اسم المستخدم، عنوان IP، المسار) مباشرة داخل ملف `deploy.sh` لتسهيل الاستخدام الفوري، أم نضعها كمتغيرات في ملف `.env` على أن يقرأها السكريبت؟ +> 2. **النطاق الفرعي (Subdomain)**: ما هو الاسم المقترح للنطاق الفرعي لـ Backend؟ (مثال: `api.nabeh.sa` أو `core.nabeh.sa`)؟ +> 3. **إصدار PHP على السيرفر**: ما هو إصدار PHP المفعل على خادم Nginx الحالي لديك للتأكد من تطابق التهيئة ودعم الـ Socket الصحيح؟ + +