This commit is contained in:
Hamza-Ayed
2026-05-01 00:47:30 +03:00
parent 0c6a4f9491
commit 312608de93
9 changed files with 394 additions and 23 deletions

View File

@@ -0,0 +1,48 @@
<?php
/**
* Admin/auth/approve_admin.php
* الموافقة على أو رفض طلبات انضمام المشرفين
* مسموح فقط للسوبر أدمن
*/
require_once __DIR__ . '/../../connect.php';
if ($role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Forbidden. Super Admin access required.']);
exit;
}
$targetId = filterRequest('admin_id');
$action = filterRequest('action'); // approved, rejected, suspended
if (empty($targetId) || empty($action)) {
jsonError("Admin ID and action are required.");
exit;
}
if (!in_array($action, ['approved', 'rejected', 'suspended'])) {
jsonError("Invalid action.");
exit;
}
try {
$con = Database::get('main');
$sql = "UPDATE adminUser SET status = :status, approved_by = :by, approved_at = NOW() WHERE id = :id";
$stmt = $con->prepare($sql);
$stmt->execute([
':status' => $action,
':by' => $user_id, // السوبر أدمن الحالي
':id' => $targetId
]);
if ($stmt->rowCount() > 0) {
printSuccess(null, "Admin status updated to $action.");
} else {
jsonError("Admin not found or status already updated.");
}
} catch (Exception $e) {
error_log("[Approve Admin Error] " . $e->getMessage());
jsonError("Server Error: " . $e->getMessage());
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* Admin/auth/list_pending.php
* عرض قائمة المشرفين الذين ينتظرون الموافقة
* مسموح فقط للسوبر أدمن
*/
require_once __DIR__ . '/../../connect.php';
// التحقق من الصلاحيات
if ($role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Forbidden. Super Admin access required.']);
exit;
}
try {
$con = Database::get('main');
$stmt = $con->prepare("SELECT id, name, phone, created_at FROM adminUser WHERE status = 'pending' ORDER BY created_at DESC");
$stmt->execute();
$pending = $stmt->fetchAll(PDO::FETCH_ASSOC);
// فك تشفير الأسماء
foreach ($pending as &$admin) {
$admin['name'] = $encryptionHelper->decryptData($admin['name']) ?: $admin['name'];
}
printSuccess($pending);
} catch (Exception $e) {
error_log("[List Pending Admins Error] " . $e->getMessage());
jsonError("Server Error: " . $e->getMessage());
}

View File

@@ -24,25 +24,50 @@ try {
$admin = $stmt->fetch(PDO::FETCH_ASSOC);
if ($admin) {
// التحقق من كلمة المرور الهاش
// 1. التحقق من حالة الحساب
if ($admin['status'] === 'pending') {
jsonError("حسابك قيد المراجعة حالياً. يرجى الانتظار للموافقة.");
exit;
} elseif ($admin['status'] === 'suspended') {
jsonError("هذا الحساب معلق. يرجى التواصل مع المدير.");
exit;
} elseif ($admin['status'] === 'rejected') {
jsonError("تم رفض طلب الانضمام لهذا الحساب.");
exit;
}
// 2. التحقق من كلمة المرور
if (password_verify($password, $admin['password'])) {
// فك تشفير الاسم للعرض في التطبيق
$admin['name'] = $encryptionHelper->decryptData($admin['name']) ?: $admin['name'];
unset($admin['password']);
$jwtService = new JwtService($redis);
$role = $admin['role'] ?? 'admin';
// 3. توليد رمز تحقق OTP وإرساله عبر WhatsApp
$otp = rand(10000, 99999);
$phone = $admin['phone'] ?? ''; // تأكد من وجود حقل الهاتف في الجدول
// توليد توكن الدخول مع ربطه ببصمة الجهاز
$jwt = $jwtService->generateAccessToken($admin['id'], $role, $audience, $fingerprint);
if (empty($phone)) {
// Fallback للأرقام المسموح لها إذا لم يكن الرقم مسجلاً في الجدول
// (قد نحتاج لتحسين هذه النقطة لاحقاً)
jsonError("رقم الهاتف غير مسجل لهذا الحساب. يرجى مراجعة الإدارة.");
exit;
}
printSuccess([
"message" => "Login successful",
"admin" => $admin,
"jwt" => $jwt,
"expires_in" => 3600
]);
$messageBody = "رمز التحقق الخاص بك للدخول إلى لوحة الإدارة هو: $otp";
$success = sendWhatsAppFromServer($phone, $messageBody);
if ($success) {
// حفظ الرمز في قاعدة البيانات للتحقق لاحقاً
$stmt = $con->prepare("INSERT INTO token_verification_admin (phone_number, token, expiration_time)
VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 10 MINUTE))
ON DUPLICATE KEY UPDATE token = VALUES(token), expiration_time = VALUES(expiration_time)");
$stmt->execute([$phone, $otp]);
printSuccess([
"status" => "otp_required",
"message" => "تم إرسال رمز التحقق إلى WhatsApp الخاص بك.",
"phone" => $phone
]);
} else {
jsonError("فشل في إرسال رمز التحقق عبر WhatsApp.");
}
} else {
jsonError("كلمة المرور غير صحيحة.");
}

View File

@@ -14,19 +14,17 @@ use Firebase\JWT\JWT;
$jwtService = new JwtService($redis ?? null);
$admin = $jwtService->authenticate();
if ($admin->role !== 'admin') {
if ($admin->role !== 'admin' && $admin->role !== 'super_admin') {
jsonError("Unauthorized. Admin access required.");
exit;
}
try {
// جلب المفتاح المشترك لسيرفر المحفظة
// الأولوية لملف المفتاح المخصص للمدفوعات إن وجد، وإلا نستخدم الـ env
$payKeyPath = '/home/intaleq-api/.secret_key_pay';
$payKey = file_exists($payKeyPath) ? trim(file_get_contents($payKeyPath)) : getenv('SECRET_KEY_PAY');
if (empty($payKey)) {
// Fallback للمفتاح الرئيسي إذا لم يتوفر مفتاح خاص بالدفع (يجب التأكد من تطابقه مع سيرفر المحفظة)
$payKey = trim(@file_get_contents('/home/intaleq-api/.secret_key'));
}
@@ -39,21 +37,34 @@ try {
$audience = 'Tripz-Wallet';
$hmacSecret = getenv('SECRET_KEY_HMAC') ?: '';
$ttl = 3600; // ساعة واحدة
$ttl = 600; // 10 دقائق
$iat = time();
$exp = $iat + $ttl;
$jti = bin2hex(random_bytes(16));
// محتوى التوكن (Payload)
$payload = [
'iss' => $issuer,
'aud' => $audience,
'user_id' => $admin->user_id,
'role' => 'admin',
'role' => $admin->role, // استخدام الـ role الحالي (admin أو super_admin)
'iat' => $iat,
'exp' => $exp,
'jti' => bin2hex(random_bytes(16))
'jti' => $jti
];
// إلغاء التوكن القديم إذا وجد في Redis
if ($redis) {
$oldJtiKey = "wallet_jti:" . $admin->user_id;
$oldJti = $redis->get($oldJtiKey);
if ($oldJti) {
// إضافة التوكن القديم للقائمة السوداء
$redis->setex("jwt:blacklist:$oldJti", $ttl + 60, '1');
}
// تخزين الـ JTI الجديد
$redis->setex($oldJtiKey, $ttl, $jti);
}
// إضافة بصمة الجهاز للتوكن لزيادة الأمان
$fpHeader = $_SERVER['HTTP_X_DEVICE_FP'] ?? null;
$fpPepper = getenv('FP_PEPPER');
@@ -64,8 +75,7 @@ try {
// توليد التوكن
$jwt = JWT::encode($payload, $payKey, 'HS256');
// حساب الـ HMAC Hash المطلوب لسيرفر المحفظة للتحقق
// بناءً على authenticateJWT المرسل: hash_hmac('sha256', $userId, $hmacSecret)
// حساب الـ HMAC Hash المطلوب لسيرفر المحفظة
$hmacHash = hash_hmac('sha256', (string)$admin->user_id, $hmacSecret);
printSuccess([

28
Admin/auth/migrate_db.php Normal file
View File

@@ -0,0 +1,28 @@
<?php
require_once __DIR__ . '/../../core/bootstrap.php';
try {
$con = Database::get('main');
// Check if columns already exist to avoid errors
$check = $con->query("SHOW COLUMNS FROM adminUser LIKE 'status'");
if ($check->rowCount() == 0) {
$sql = "ALTER TABLE adminUser
ADD COLUMN status ENUM('pending', 'approved', 'suspended', 'rejected') NOT NULL DEFAULT 'pending' AFTER role,
ADD COLUMN phone VARCHAR(50) DEFAULT NULL AFTER name,
ADD COLUMN email VARCHAR(255) DEFAULT NULL AFTER phone,
ADD COLUMN approved_by VARCHAR(64) DEFAULT NULL AFTER status,
ADD COLUMN approved_at DATETIME DEFAULT NULL AFTER approved_by";
$con->exec($sql);
// Update existing admins to approved and super_admin
$con->exec("UPDATE adminUser SET status = 'approved', role = 'super_admin' WHERE id IS NOT NULL");
echo json_encode(["status" => "success", "message" => "Migration completed successfully."]);
} else {
echo json_encode(["status" => "success", "message" => "Columns already exist."]);
}
} catch (Exception $e) {
echo json_encode(["status" => "error", "message" => $e->getMessage()]);
}

59
Admin/auth/register.php Normal file
View File

@@ -0,0 +1,59 @@
<?php
/**
* Admin/auth/register.php
* التسجيل الذاتي للمشرفين - الحساب يكون بحالة pending بانتظار موافقة السوبر أدمن
*/
require_once __DIR__ . '/../../core/bootstrap.php';
$name = filterRequest('name');
$phone = filterRequest('phone');
$password = filterRequest('password');
$fingerprint = filterRequest('fingerprint');
if (empty($name) || empty($phone) || empty($password) || empty($fingerprint)) {
jsonError("All fields are required.");
exit;
}
try {
$con = Database::get('main');
// 1. التحقق من عدم وجود الحساب مسبقاً (عن طريق الهاتف أو البصمة)
$fpHash = hash('sha256', $fingerprint);
$check = $con->prepare("SELECT id FROM adminUser WHERE phone = ? OR fingerprint_hash = ? LIMIT 1");
$check->execute([$phone, $fpHash]);
if ($check->rowCount() > 0) {
jsonError("هذا الحساب أو الجهاز مسجل مسبقاً.");
exit;
}
// 2. تجهيز البيانات
$id = bin2hex(random_bytes(16));
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$encName = $encryptionHelper->encryptData($name);
$encFp = $encryptionHelper->encryptData($fingerprint);
// 3. الإدخال في قاعدة البيانات (الحالة الافتراضية هي pending)
$sql = "INSERT INTO adminUser (id, name, phone, password, fingerprint, fingerprint_hash, role, status, created_at)
VALUES (:id, :name, :phone, :pass, :fp, :fp_hash, 'admin', 'pending', NOW())";
$stmt = $con->prepare($sql);
$stmt->execute([
':id' => $id,
':name' => $encName,
':phone' => $phone,
':pass' => $hashedPassword,
':fp' => $encFp,
':fp_hash' => $fpHash
]);
printSuccess([
"status" => "pending",
"message" => "تم تقديم طلب التسجيل بنجاح. يرجى انتظار موافقة الإدارة."
]);
} catch (Exception $e) {
error_log("[Admin Register Error] " . $e->getMessage());
jsonError("خطأ في السيرفر: " . $e->getMessage());
}

View File

@@ -0,0 +1,83 @@
<?php
/**
* Admin/auth/verify_login.php
* الخطوة الثانية من تسجيل الدخول: التحقق من الـ OTP وإصدار التوكن النهائي
*/
require_once __DIR__ . '/../../core/bootstrap.php';
$phone = filterRequest('phone');
$otp = filterRequest('otp');
$fingerprint = filterRequest('fingerprint'); // مطلوب لربط التوكن بالجهاز
$audience = filterRequest('aud') ?? 'admin';
if (empty($phone) || empty($otp) || empty($fingerprint)) {
jsonError("Phone, OTP and fingerprint are required.");
exit;
}
try {
$con = Database::get('main');
// 1. التحقق من الـ OTP
$stmt = $con->prepare("SELECT * FROM token_verification_admin
WHERE phone_number = ? AND token = ?
AND expiration_time >= NOW()");
$stmt->execute([$phone, $otp]);
if ($stmt->rowCount() === 0) {
jsonError("رمز التحقق غير صالح أو منتهي الصلاحية.");
exit;
}
// حذف الرمز بعد استخدامه لمرة واحدة
$con->prepare("DELETE FROM token_verification_admin WHERE phone_number = ?")->execute([$phone]);
// 2. جلب بيانات المسؤول
$stmt = $con->prepare("SELECT * FROM adminUser WHERE phone = ? LIMIT 1");
$stmt->execute([$phone]);
$admin = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$admin) {
jsonError("المسؤول غير موجود.");
exit;
}
// 3. التحقق من البصمة (اختياري لكن مفضل لزيادة الأمان)
$fpHash = hash('sha256', $fingerprint);
if ($admin['fingerprint_hash'] !== $fpHash) {
// إذا كانت البصمة مختلفة، ربما يحاول تسجيل الدخول من جهاز آخر
// يمكننا إما المنع أو السماح وتحديث البصمة (حسب السياسة)
// هنا سنقوم بالمنع لزيادة الأمان
jsonError("عذراً، لا يمكن تسجيل الدخول من هذا الجهاز.");
exit;
}
// 4. إصدار التوكن النهائي
$jwtService = new JwtService($redis);
$role = $admin['role'] ?? 'admin';
// إلغاء التوكن القديم إذا وجد في Redis (Token Revocation)
if ($redis) {
$oldJti = $redis->get("active_jti:" . $admin['id']);
if ($oldJti) {
$jwtService->revokeToken($oldJti, 3600);
}
}
$jwt = $jwtService->generateAccessToken($admin['id'], $role, $audience, $fingerprint);
// فك تشفير الاسم للعرض
$admin['name'] = $encryptionHelper->decryptData($admin['name']) ?: $admin['name'];
unset($admin['password']);
printSuccess([
"message" => "Login successful",
"admin" => $admin,
"jwt" => $jwt,
"expires_in" => 3600
]);
} catch (Exception $e) {
error_log("[Admin Verify OTP Error] " . $e->getMessage());
jsonError("خطأ في السيرفر: " . $e->getMessage());
}

View File

@@ -1,6 +1,13 @@
<?php
require_once __DIR__ . '/../connect.php';
// التحقق من الصلاحيات: مسموح فقط للأدمن والسوبر أدمن
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized access. Admin role required.']);
exit;
}
$sql = "
SELECT
-- العدادات العامة