Update: 2026-08-07 19:10:47
This commit is contained in:
@@ -101,3 +101,7 @@ GoogleService-Info.plist
|
|||||||
# --- Audit/Scan Output ---
|
# --- Audit/Scan Output ---
|
||||||
semgrep_*.json
|
semgrep_*.json
|
||||||
nuclei_results.txt
|
nuclei_results.txt
|
||||||
|
|
||||||
|
# سجل لوحة المفاتيح — محلي للسيرفر، لا يُدفع
|
||||||
|
backend/tools/panel/panel_audit.log
|
||||||
|
*.env.bak.*
|
||||||
|
|||||||
@@ -23,82 +23,10 @@ $envFile = getenv('ENV_FILE_PATH') ?: "$root/docker/.env";
|
|||||||
$command = $argv[1] ?? 'audit';
|
$command = $argv[1] ?? 'audit';
|
||||||
$dryRun = in_array('--dry-run', $argv, true);
|
$dryRun = in_array('--dry-run', $argv, true);
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// التصنيف كله في env_registry.php — مصدر واحد تشترك فيه هذه الأداة
|
||||||
// التصنيف
|
// واللوحة. نسختان منه كانتا ستتباعدان، وأخطر تباعد هو قائمة المجمّدة:
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// مفتاح يسقط منها في نسخة يعني لوحةً تعرضه قابلاً للتعديل.
|
||||||
|
require_once __DIR__ . '/env_registry.php';
|
||||||
/** أسرار داخلية: طرفاها كلاهما لنا. توليدها وتدويرها آمنان. */
|
|
||||||
const GENERATED = [
|
|
||||||
'S2S_SHARED_KEY' => 32, // خادم ↔ خادم داخلي
|
|
||||||
'WEBHOOK_SECRET_KEY' => 32, // هاتف الرسائل ↔ الويبهوك
|
|
||||||
'INTERNAL_SOCKET_KEY' => 32, // الباك إند ↔ خوادم السوكيت
|
|
||||||
'BOT_SECRET_KEY' => 24, // بوت الأسعار
|
|
||||||
'FCM_INTERNAL_API_KEY' => 32,
|
|
||||||
'MIGRATION_ADMIN_KEY' => 24,
|
|
||||||
'PAYMENT_KEY' => 32,
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* تدويرها يبطل كل الجلسات القائمة: كل سائق وكل راكب سيُخرَج ويُطالَب
|
|
||||||
* بتسجيل دخول جديد. ليس عطلاً لكنه حدث تشغيلي — لا يُفعل وقت الذروة.
|
|
||||||
*/
|
|
||||||
const SESSION_SCOPED = [
|
|
||||||
'JWT_SECRET_KEY' => 64,
|
|
||||||
'SECRET_KEY_HMAC' => 32,
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ⛔ مجمّدة. هذه ليست «مفاتيح يمكن تغييرها من عندنا» رغم أنها ملكنا:
|
|
||||||
*
|
|
||||||
* ENC_KEY / ENCRYPTION_KEY_PATH
|
|
||||||
* تُشفَّر به أسماء الركّاب وهواتفهم وبرديهم ولوحات السيارات
|
|
||||||
* **المخزَّنة في القاعدة**. تغييره لا يعيد تشفير شيء — يجعل كل
|
|
||||||
* صف قديم رطانةً لا تُفكّ. لا استرجاع، ولا نسخة احتياطية تنفع
|
|
||||||
* لأن النسخة نفسها مشفّرة بالمفتاح القديم.
|
|
||||||
*
|
|
||||||
* BLIND_INDEX_PEPPER
|
|
||||||
* أعمدة `*_bidx` محسوبة به ومخزَّنة. تغييره يعني أن كل بحث
|
|
||||||
* بالهاتف أو البريد يفشل — لا يجد المستخدم حسابه، ويُنشأ له
|
|
||||||
* حساب مكرر عند أول تسجيل.
|
|
||||||
*
|
|
||||||
* TRACKING_SECRET_SALT / FP_PEPPER
|
|
||||||
* قيم مشتقّة منها مخزَّنة أيضاً.
|
|
||||||
*
|
|
||||||
* تدويرها ممكن نظرياً لكنه **ترحيل بيانات** لا تغيير إعداد: فكّ بالقديم
|
|
||||||
* وإعادة تشفير بالجديد على كل صف، بتوقّف مخطَّط ونسخة احتياطية أولاً.
|
|
||||||
*/
|
|
||||||
const FROZEN = [
|
|
||||||
'ENC_KEY', 'ENCRYPTION_KEY_PATH', 'ENCRYPTION_MODE',
|
|
||||||
'BLIND_INDEX_PEPPER', 'TRACKING_SECRET_SALT', 'FP_PEPPER',
|
|
||||||
];
|
|
||||||
|
|
||||||
/** لا تُولَّد — تُنسخ من لوحة المزوّد. */
|
|
||||||
const EXTERNAL = [
|
|
||||||
'GEMINI_API_KEY', 'NABEH_API_KEY', 'NABEH_PASSWORD', 'NABEH_OTP_APP_KEY',
|
|
||||||
'ELEVENLABS_API_KEY', 'CREATOMATE_API_KEY', 'SOCIAL_BOT_TOKEN',
|
|
||||||
'MAIL_PASSWORD', 'SIRO_SMTP_PASSWORD', 'SMS_PASSWORD_EGYPT',
|
|
||||||
'VOICE_CALL_API_KEY', 'RASEEL_DRIVER_ACCESS_TOKEN',
|
|
||||||
'FIREBASE_SERVICE_ACCOUNT_PATH', 'SERVICE_ACCOUNT_FILE_PATH',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* كلمات مرور قواعد البيانات — صنف قائم بذاته.
|
|
||||||
*
|
|
||||||
* تغييرها في .env **لا يغيّر شيئاً في MySQL**. متغيّرات صورة mysql
|
|
||||||
* (ومنها MYSQL_ROOT_PASSWORD) تُقرأ مرة واحدة فقط عند تهيئة مجلّد
|
|
||||||
* البيانات الفارغ أول مرة. بعد ذلك كلمة المرور تعيش داخل جدول
|
|
||||||
* mysql.user لا في .env — فتعديل .env وحده يعني تطبيقاً يحمل كلمة
|
|
||||||
* لا يقبلها الخادم: انقطاع كامل بلا أي تغيير في القاعدة نفسها.
|
|
||||||
*
|
|
||||||
* الترتيب الصحيح: ALTER USER في MySQL أولاً، ثم .env، ثم إعادة تشغيل
|
|
||||||
* حاويات php (لا mysql، ولا إعادة بناء، ولا مساس بالبيانات إطلاقاً).
|
|
||||||
*/
|
|
||||||
const DB_CREDENTIALS = [
|
|
||||||
'DB_PRIMARY_PASS_V2', 'DB_PRIMARY_PASS', 'DB_TRACKING_PASS',
|
|
||||||
'DB_RIDE_PASS', 'DB_TRANSIT_PASS', 'DB_FOOD_PASS', 'DB_PAYMENT_PASS',
|
|
||||||
'MYSQL_ROOT_PASSWORD', 'REDIS_PASSWORD', 'REDIS_MAIN_PASSWORD',
|
|
||||||
'REDIS_LOCATION_PASSWORD', 'REDIS_AUTH', 'REDIS_MAIN_AUTH',
|
|
||||||
];
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// القراءة
|
// القراءة
|
||||||
@@ -148,11 +76,11 @@ $report = static function (string $title, array $keys, array $present,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$report('أسرار داخلية (تُولَّد وتُدوَّر بحرية)', GENERATED, $present, $isPlaceholder, $toSeed, true);
|
$report('أسرار داخلية (تُولَّد وتُدوَّر بحرية)', ENV_GENERATED, $present, $isPlaceholder, $toSeed, true);
|
||||||
$report('مرتبطة بالجلسات (تدويرها يُخرج الجميع)', SESSION_SCOPED, $present, $isPlaceholder, $toSeed, true);
|
$report('مرتبطة بالجلسات (تدويرها يُخرج الجميع)', ENV_SESSION_SCOPED, $present, $isPlaceholder, $toSeed, true);
|
||||||
$report('⛔ مجمّدة — لا تُدوَّر (بيانات مخزَّنة مشتقّة منها)', FROZEN, $present, $isPlaceholder);
|
$report('⛔ مجمّدة — لا تُدوَّر (بيانات مخزَّنة مشتقّة منها)', ENV_FROZEN, $present, $isPlaceholder);
|
||||||
$report('اعتماد قواعد البيانات (يُغيَّر في MySQL أولاً لا هنا)', DB_CREDENTIALS, $present, $isPlaceholder);
|
$report('اعتماد قواعد البيانات (يُغيَّر في MySQL أولاً لا هنا)', ENV_DB_CREDENTIALS, $present, $isPlaceholder);
|
||||||
$report('طرف ثالث (تُنسخ من لوحة المزوّد)', EXTERNAL, $present, $isPlaceholder);
|
$report('طرف ثالث (تُنسخ من لوحة المزوّد)', ENV_EXTERNAL, $present, $isPlaceholder);
|
||||||
|
|
||||||
echo "\n" . str_repeat('─', 62) . "\n";
|
echo "\n" . str_repeat('─', 62) . "\n";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* tools/env_registry.php — تصنيف مفاتيح البيئة وقراءة/كتابة .env
|
||||||
|
* ─────────────────────────────────────────────────────────────
|
||||||
|
* مصدر واحد للحقيقة تستعمله الأداة السطرية (env_keys.php) واللوحة
|
||||||
|
* (panel/index.php) معاً. نسختان من التصنيف كانتا ستتباعدان، وأخطر ما
|
||||||
|
* فيهما التباعد هو قائمة «المجمّدة» — مفتاح يسقط منها في نسخة يعني
|
||||||
|
* لوحةً تعرضه قابلاً للتعديل.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// الأصناف
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** أسرار داخلية: طرفاها كلاهما لنا. التوليد والتدوير آمنان. */
|
||||||
|
const ENV_GENERATED = [
|
||||||
|
'S2S_SHARED_KEY' => 32,
|
||||||
|
'WEBHOOK_SECRET_KEY' => 32,
|
||||||
|
'INTERNAL_SOCKET_KEY' => 32,
|
||||||
|
'BOT_SECRET_KEY' => 24,
|
||||||
|
'FCM_INTERNAL_API_KEY' => 32,
|
||||||
|
'MIGRATION_ADMIN_KEY' => 24,
|
||||||
|
'PAYMENT_KEY' => 32,
|
||||||
|
];
|
||||||
|
|
||||||
|
/** تدويرها يبطل كل الجلسات القائمة — حدث تشغيلي واعٍ لا روتين. */
|
||||||
|
const ENV_SESSION_SCOPED = [
|
||||||
|
'JWT_SECRET_KEY' => 64,
|
||||||
|
'SECRET_KEY_HMAC' => 32,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ⛔ مجمّدة. بيانات مخزَّنة في القاعدة مشتقّة منها:
|
||||||
|
* ENC_KEY — أسماء الركّاب وهواتفهم ولوحات السيارات مشفّرة به
|
||||||
|
* BLIND_INDEX_PEPPER — أعمدة *_bidx محسوبة به ومخزَّنة
|
||||||
|
* TRACKING_SECRET_SALT / FP_PEPPER — قيم مشتقّة مخزَّنة
|
||||||
|
*
|
||||||
|
* تغييرها لا يعيد تشفير شيء: يجعل الصفوف القديمة غير قابلة للقراءة
|
||||||
|
* إلى الأبد، ولا نسخة احتياطية تنفع لأنها مشفّرة بالمفتاح القديم.
|
||||||
|
* تدويرها ترحيلُ بيانات لا تغييرُ إعداد.
|
||||||
|
*
|
||||||
|
* اللوحة تعرض حالتها فقط — لا قيمتها ولا حقل تعديل.
|
||||||
|
*/
|
||||||
|
const ENV_FROZEN = [
|
||||||
|
'ENC_KEY', 'ENCRYPTION_KEY_PATH', 'ENCRYPTION_MODE',
|
||||||
|
'BLIND_INDEX_PEPPER', 'TRACKING_SECRET_SALT', 'FP_PEPPER',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** لا تُولَّد — تُنسخ من لوحة المزوّد. قابلة للتحرير يدوياً. */
|
||||||
|
const ENV_EXTERNAL = [
|
||||||
|
'GEMINI_API_KEY', 'NABEH_API_KEY', 'NABEH_PASSWORD', 'NABEH_OTP_APP_KEY',
|
||||||
|
'ELEVENLABS_API_KEY', 'CREATOMATE_API_KEY', 'SOCIAL_BOT_TOKEN',
|
||||||
|
'MAIL_PASSWORD', 'SIRO_SMTP_PASSWORD', 'SMS_PASSWORD_EGYPT',
|
||||||
|
'VOICE_CALL_API_KEY', 'RASEEL_DRIVER_ACCESS_TOKEN',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* كلمات مرور القواعد ← اسم المستخدم ومتغيّر اسم القاعدة.
|
||||||
|
*
|
||||||
|
* تغييرها في .env وحده لا يفعل شيئاً: متغيّرات صورة mysql تُقرأ مرة
|
||||||
|
* واحدة عند تهيئة مجلّد بيانات فارغ، وبعدها كلمة المرور تعيش في
|
||||||
|
* mysql.user. الترتيب الصحيح ALTER USER ثم .env — واللوحة تفعله
|
||||||
|
* بهذا الترتيب وتتحقّق قبل أن تكتب.
|
||||||
|
*/
|
||||||
|
const ENV_DB_CREDENTIALS = [
|
||||||
|
'DB_PRIMARY_PASS_V2' => ['user' => 'DB_PRIMARY_USER_V2', 'db' => 'DB_PRIMARY_NAME_V2'],
|
||||||
|
'DB_PRIMARY_PASS' => ['user' => 'DB_PRIMARY_USER', 'db' => 'DB_PRIMARY_NAME'],
|
||||||
|
'DB_TRACKING_PASS' => ['user' => 'DB_TRACKING_USER', 'db' => 'DB_TRACKING_NAME'],
|
||||||
|
'DB_RIDE_PASS' => ['user' => 'DB_RIDE_USER', 'db' => 'DB_RIDE_NAME'],
|
||||||
|
'DB_TRANSIT_PASS' => ['user' => 'DB_TRANSIT_USER', 'db' => 'DB_TRANSIT_NAME'],
|
||||||
|
'DB_FOOD_PASS' => ['user' => 'DB_FOOD_USER', 'db' => 'DB_FOOD_NAME'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/** يقرأ .env إلى مصفوفة. */
|
||||||
|
function envRead(string $file): array
|
||||||
|
{
|
||||||
|
if (!is_readable($file)) return [];
|
||||||
|
$out = [];
|
||||||
|
foreach (file($file, FILE_IGNORE_NEW_LINES) as $line) {
|
||||||
|
if (preg_match('/^\s*([A-Z0-9_]+)\s*=\s*(.*)$/', $line, $m)) {
|
||||||
|
$out[$m[1]] = trim($m[2], "\"' ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** قيمة نائبة: موجودة شكلاً وغائبة فعلاً. */
|
||||||
|
function envIsPlaceholder(?string $v): bool
|
||||||
|
{
|
||||||
|
if ($v === null || $v === '') return true;
|
||||||
|
return (bool) preg_match('/^(<?CHANGE_ME|changeme|your[_-]|xxx+|TODO|REPLACE)/i', $v);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* يكتب مفاتيح في .env مع نسخة احتياطية.
|
||||||
|
*
|
||||||
|
* الاستبدال في المكان يبقي التعليقات المجاورة مفيدة، والمفقود يُلحق
|
||||||
|
* في النهاية.
|
||||||
|
*
|
||||||
|
* @return string|null مسار النسخة الاحتياطية، أو null عند الفشل.
|
||||||
|
*/
|
||||||
|
function envWrite(string $file, array $changes): ?string
|
||||||
|
{
|
||||||
|
if (empty($changes)) return null;
|
||||||
|
|
||||||
|
// .env التالف يوقف كل شيء، وهو خارج git بطبيعته فلا مصدر لاستعادته.
|
||||||
|
$backup = $file . '.bak.' . date('Ymd_His');
|
||||||
|
if (!copy($file, $backup)) return null;
|
||||||
|
@chmod($backup, 0600);
|
||||||
|
|
||||||
|
$out = rtrim((string) file_get_contents($file), "\n") . "\n";
|
||||||
|
$append = [];
|
||||||
|
|
||||||
|
foreach ($changes as $name => $value) {
|
||||||
|
$line = "$name=$value";
|
||||||
|
if (preg_match('/^\s*' . preg_quote($name, '/') . '\s*=/m', $out)) {
|
||||||
|
$out = preg_replace(
|
||||||
|
'/^\s*' . preg_quote($name, '/') . '\s*=.*$/m',
|
||||||
|
str_replace('$', '\\$', $line),
|
||||||
|
$out,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$append[] = $line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($append) {
|
||||||
|
$out .= "\n# ── " . date('Y-m-d H:i') . " ──\n" . implode("\n", $append) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_put_contents($file, $out) === false) return null;
|
||||||
|
@chmod($file, 0600);
|
||||||
|
|
||||||
|
return $backup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** سرّ عشوائي بصيغة hex. */
|
||||||
|
function envGenerate(int $bytes = 32): string
|
||||||
|
{
|
||||||
|
return bin2hex(random_bytes($bytes));
|
||||||
|
}
|
||||||
@@ -0,0 +1,526 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* tools/panel/index.php — لوحة إدارة مفاتيح البيئة
|
||||||
|
* ─────────────────────────────────────────────────────────────
|
||||||
|
* ⚠️ هذه أعلى نقطة قيمةً في المنظومة: من يصل إليها يملك كل شيء دفعة
|
||||||
|
* واحدة. لذلك القيود التالية ليست تزييناً، وإزالة أيٍّ منها تُبطل
|
||||||
|
* الباقي:
|
||||||
|
*
|
||||||
|
* ١. الاستماع على 127.0.0.1 فقط — لا تُفتح إلا عبر نفق SSH:
|
||||||
|
* ssh -L 8099:127.0.0.1:8099 root@server
|
||||||
|
* ثم http://127.0.0.1:8099 في متصفّحك.
|
||||||
|
*
|
||||||
|
* ٢. عبارة مرور منفصلة تماماً عن أي حساب آخر (ENV_PANEL_PASSPHRASE
|
||||||
|
* مخزَّنة كتجزئة password_hash، لا نصّاً).
|
||||||
|
*
|
||||||
|
* ٣. المفاتيح المجمّدة تُعرض حالتها فقط — لا قيمتها ولا حقل تعديل.
|
||||||
|
* لا يوجد مسار في هذا الملف يكتبها، لا بواجهة ولا بطلب مصنوع.
|
||||||
|
*
|
||||||
|
* ٤. كل تغيير يُسجَّل: من أين، ومتى، وأي مفتاح (لا قيمته).
|
||||||
|
*
|
||||||
|
* التشغيل (على المضيف، لا داخل الحاوية — يحتاج الوصول إلى docker):
|
||||||
|
* cd backend/tools/panel && php -S 127.0.0.1:8099
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../env_registry.php';
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// القيد ١ — لا شيء غير المضيف المحلي
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
$remote = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||||
|
if (!in_array($remote, ['127.0.0.1', '::1'], true)) {
|
||||||
|
http_response_code(403);
|
||||||
|
exit('Forbidden — local access only.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ترويسات الوكيل لا تُصدَّق: لو وُضعت اللوحة خلف nginx يوماً، فإن
|
||||||
|
// X-Forwarded-For تجعل REMOTE_ADDR محلياً دائماً وتُبطل القيد أعلاه.
|
||||||
|
// وجودها يعني أننا خلف وكيل — وهذا ما لا نريده أصلاً.
|
||||||
|
foreach (['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_FORWARDED'] as $h) {
|
||||||
|
if (!empty($_SERVER[$h])) {
|
||||||
|
http_response_code(403);
|
||||||
|
exit('Forbidden — panel must not be proxied.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$root = dirname(__DIR__, 3);
|
||||||
|
$envFile = getenv('ENV_FILE_PATH') ?: "$root/docker/.env";
|
||||||
|
$logFile = __DIR__ . '/panel_audit.log';
|
||||||
|
|
||||||
|
session_start([
|
||||||
|
'cookie_httponly' => true,
|
||||||
|
'cookie_samesite' => 'Strict',
|
||||||
|
'use_strict_mode' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** يسجّل حدثاً. القيم لا تُسجَّل أبداً — أسماء المفاتيح فقط. */
|
||||||
|
function auditLog(string $file, string $event, string $detail = ''): void
|
||||||
|
{
|
||||||
|
$line = sprintf("[%s] %s | %s | %s\n",
|
||||||
|
date('Y-m-d H:i:s'), $_SERVER['REMOTE_ADDR'] ?? '?', $event, $detail);
|
||||||
|
@file_put_contents($file, $line, FILE_APPEND);
|
||||||
|
@chmod($file, 0600);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// القيد ٢ — عبارة المرور
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
$env = envRead($envFile);
|
||||||
|
$hash = $env['ENV_PANEL_PASSPHRASE'] ?? '';
|
||||||
|
|
||||||
|
if ($hash === '' || !str_starts_with($hash, '$2y$')) {
|
||||||
|
http_response_code(503);
|
||||||
|
echo '<meta charset="utf-8"><pre style="font-family:monospace;padding:2rem">';
|
||||||
|
echo "اللوحة معطّلة: لم تُضبط ENV_PANEL_PASSPHRASE.\n\n";
|
||||||
|
echo "ولّد تجزئة وضعها في docker/.env:\n\n";
|
||||||
|
echo " php -r 'echo password_hash(\"عبارتك\", PASSWORD_BCRYPT), PHP_EOL;'\n\n";
|
||||||
|
echo "ثم: ENV_PANEL_PASSPHRASE='\$2y\$...'\n\n";
|
||||||
|
echo "اجعلها عبارة لا تشبه أي كلمة مرور أخرى عندك.\n";
|
||||||
|
echo '</pre>';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = '';
|
||||||
|
|
||||||
|
if (($_POST['action'] ?? '') === 'login') {
|
||||||
|
// تأخير ثابت: يجعل تخمين العبارة بطيئاً بما يكفي، وbcrypt نفسه
|
||||||
|
// يتكفّل بالباقي.
|
||||||
|
usleep(400_000);
|
||||||
|
if (password_verify((string) ($_POST['passphrase'] ?? ''), $hash)) {
|
||||||
|
session_regenerate_id(true);
|
||||||
|
$_SESSION['ok'] = true;
|
||||||
|
$_SESSION['csrf'] = bin2hex(random_bytes(16));
|
||||||
|
auditLog($logFile, 'LOGIN_OK');
|
||||||
|
} else {
|
||||||
|
$error = 'عبارة غير صحيحة.';
|
||||||
|
auditLog($logFile, 'LOGIN_FAIL');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($_GET['logout'] ?? '') === '1') {
|
||||||
|
session_destroy();
|
||||||
|
header('Location: ?');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$authed = !empty($_SESSION['ok']);
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// العمليات
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
$notice = '';
|
||||||
|
|
||||||
|
if ($authed && ($_POST['action'] ?? '') !== '' && ($_POST['action'] ?? '') !== 'login') {
|
||||||
|
if (!hash_equals((string) ($_SESSION['csrf'] ?? ''), (string) ($_POST['csrf'] ?? ''))) {
|
||||||
|
http_response_code(400);
|
||||||
|
exit('CSRF mismatch');
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = (string) ($_POST['key'] ?? '');
|
||||||
|
$action = (string) $_POST['action'];
|
||||||
|
|
||||||
|
// ── الحارس الجامع ──
|
||||||
|
// قبل أي فرع: المجمّدة ممنوعة. الفحص هنا لا في كل فرع، فلا يمكن
|
||||||
|
// لفرع جديد يُضاف لاحقاً أن ينساه.
|
||||||
|
if (in_array($key, ENV_FROZEN, true)) {
|
||||||
|
http_response_code(403);
|
||||||
|
auditLog($logFile, 'BLOCKED_FROZEN', $key);
|
||||||
|
exit('هذا المفتاح مجمّد ولا يمكن تعديله من اللوحة.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$rotatable = ENV_GENERATED + ENV_SESSION_SCOPED;
|
||||||
|
|
||||||
|
if ($action === 'rotate' && isset($rotatable[$key])) {
|
||||||
|
$backup = envWrite($envFile, [$key => envGenerate($rotatable[$key])]);
|
||||||
|
if ($backup) {
|
||||||
|
auditLog($logFile, 'ROTATE', $key);
|
||||||
|
$notice = "دُوِّر $key. نسخة احتياطية: " . basename($backup)
|
||||||
|
. ' — أعد تشغيل حاويات php.';
|
||||||
|
} else {
|
||||||
|
$error = 'تعذّرت الكتابة.';
|
||||||
|
}
|
||||||
|
|
||||||
|
} elseif ($action === 'set' && in_array($key, ENV_EXTERNAL, true)) {
|
||||||
|
$value = trim((string) ($_POST['value'] ?? ''));
|
||||||
|
if ($value === '') {
|
||||||
|
$error = 'القيمة فارغة.';
|
||||||
|
} else {
|
||||||
|
$backup = envWrite($envFile, [$key => $value]);
|
||||||
|
if ($backup) {
|
||||||
|
auditLog($logFile, 'SET_EXTERNAL', $key);
|
||||||
|
$notice = "حُدِّث $key.";
|
||||||
|
} else {
|
||||||
|
$error = 'تعذّرت الكتابة.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} elseif ($action === 'rotate_db' && isset(ENV_DB_CREDENTIALS[$key])) {
|
||||||
|
[$notice, $error] = rotateDbPassword($envFile, $logFile, $env, $key);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
$error = 'عملية غير معروفة أو مفتاح غير قابل للتعديل.';
|
||||||
|
}
|
||||||
|
|
||||||
|
$env = envRead($envFile); // إعادة القراءة بعد أي تغيير
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* يدوّر كلمة مرور مستخدم قاعدة بيانات بالترتيب الصحيح.
|
||||||
|
*
|
||||||
|
* الترتيب هو كل شيء: ALTER USER أولاً، ثم **تحقّق باتصال فعلي**، ثم
|
||||||
|
* كتابة .env. لو كتبنا .env أولاً وفشل ALTER، لصار التطبيق يحمل كلمة
|
||||||
|
* لا يقبلها الخادم — انقطاع كامل. ولو نجح ALTER وفشلت الكتابة، نرجع
|
||||||
|
* كلمة المرور القديمة فوراً بدل ترك الطرفين متنافرين.
|
||||||
|
*
|
||||||
|
* ولا تُلمس البيانات في أي خطوة: هذا تغيير في mysql.user لا غير.
|
||||||
|
*/
|
||||||
|
function rotateDbPassword(string $envFile, string $logFile, array $env, string $key): array
|
||||||
|
{
|
||||||
|
$map = ENV_DB_CREDENTIALS[$key];
|
||||||
|
$user = $env[$map['user']] ?? '';
|
||||||
|
$dbName = $env[$map['db']] ?? '';
|
||||||
|
$rootPass = $env['MYSQL_ROOT_PASSWORD'] ?? '';
|
||||||
|
$oldPass = $env[$key] ?? '';
|
||||||
|
|
||||||
|
if ($user === '' || $dbName === '' || $rootPass === '') {
|
||||||
|
return ['', "ينقص {$map['user']} أو {$map['db']} أو MYSQL_ROOT_PASSWORD."];
|
||||||
|
}
|
||||||
|
|
||||||
|
// كلمة مرور بلا رموز تحتاج هروباً في الشل — hex آمنة في كل سياق.
|
||||||
|
$newPass = envGenerate(20);
|
||||||
|
|
||||||
|
$alter = sprintf(
|
||||||
|
"ALTER USER %s@'%%' IDENTIFIED BY %s; FLUSH PRIVILEGES;",
|
||||||
|
quoteIdent($user), quoteLiteral($newPass)
|
||||||
|
);
|
||||||
|
|
||||||
|
[$code] = runMysql($rootPass, $alter);
|
||||||
|
if ($code !== 0) {
|
||||||
|
auditLog($logFile, 'DB_ROTATE_FAIL_ALTER', $key);
|
||||||
|
return ['', 'فشل ALTER USER — لم يتغيّر شيء.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// تحقّق: هل تعمل الكلمة الجديدة فعلاً؟ الثقة بأن ALTER نجح لا تكفي
|
||||||
|
// (قد يكون المستخدم معرَّفاً بمضيف آخر مثلاً).
|
||||||
|
if (!verifyDbLogin($user, $newPass, $dbName)) {
|
||||||
|
runMysql($rootPass, sprintf(
|
||||||
|
"ALTER USER %s@'%%' IDENTIFIED BY %s; FLUSH PRIVILEGES;",
|
||||||
|
quoteIdent($user), quoteLiteral($oldPass)
|
||||||
|
));
|
||||||
|
auditLog($logFile, 'DB_ROTATE_ROLLBACK', $key);
|
||||||
|
return ['', 'الكلمة الجديدة لم تعمل — أُعيدت القديمة.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$backup = envWrite($envFile, [$key => $newPass]);
|
||||||
|
if (!$backup) {
|
||||||
|
runMysql($rootPass, sprintf(
|
||||||
|
"ALTER USER %s@'%%' IDENTIFIED BY %s; FLUSH PRIVILEGES;",
|
||||||
|
quoteIdent($user), quoteLiteral($oldPass)
|
||||||
|
));
|
||||||
|
auditLog($logFile, 'DB_ROTATE_ROLLBACK_WRITE', $key);
|
||||||
|
return ['', 'تعذّرت كتابة .env — أُعيدت كلمة المرور القديمة.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
auditLog($logFile, 'DB_ROTATE', "$key ($user@$dbName)");
|
||||||
|
return ["دُوِّرت كلمة مرور $user. أعد تشغيل حاويات php الآن.", ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** هروب معرّف MySQL بالعلامة الخلفية. */
|
||||||
|
function quoteIdent(string $s): string
|
||||||
|
{
|
||||||
|
return '`' . str_replace('`', '``', $s) . '`';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** هروب سلسلة نصية لـ MySQL. */
|
||||||
|
function quoteLiteral(string $s): string
|
||||||
|
{
|
||||||
|
return "'" . addcslashes($s, "\\'\"\0\n\r\032") . "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ينفّذ SQL في حاوية mysql.
|
||||||
|
*
|
||||||
|
* كلمة المرور تمرّ عبر متغيّر بيئة لا وسيطاً في سطر الأوامر: الوسيط
|
||||||
|
* يظهر في `ps` لأي مستخدم على الجهاز.
|
||||||
|
*/
|
||||||
|
function runMysql(string $rootPass, string $sql): array
|
||||||
|
{
|
||||||
|
$cmd = 'docker compose -f ' . escapeshellarg(dirname(__DIR__, 3) . '/docker/docker-compose.yml')
|
||||||
|
. ' exec -T -e MYSQL_PWD mysql mysql -uroot -e ' . escapeshellarg($sql) . ' 2>&1';
|
||||||
|
|
||||||
|
$descriptors = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
|
||||||
|
$proc = proc_open($cmd, $descriptors, $pipes, null, ['MYSQL_PWD' => $rootPass]);
|
||||||
|
if (!is_resource($proc)) return [1, 'proc_open failed'];
|
||||||
|
|
||||||
|
$out = stream_get_contents($pipes[1]) . stream_get_contents($pipes[2]);
|
||||||
|
fclose($pipes[1]);
|
||||||
|
fclose($pipes[2]);
|
||||||
|
|
||||||
|
return [proc_close($proc), $out];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** يتأكد أن الاعتماد الجديد يسجّل دخولاً فعلياً. */
|
||||||
|
function verifyDbLogin(string $user, string $pass, string $db): bool
|
||||||
|
{
|
||||||
|
$cmd = 'docker compose -f ' . escapeshellarg(dirname(__DIR__, 3) . '/docker/docker-compose.yml')
|
||||||
|
. ' exec -T -e MYSQL_PWD mysql mysql -u' . escapeshellarg($user)
|
||||||
|
. ' ' . escapeshellarg($db) . ' -e "SELECT 1" 2>&1';
|
||||||
|
|
||||||
|
$descriptors = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
|
||||||
|
$proc = proc_open($cmd, $descriptors, $pipes, null, ['MYSQL_PWD' => $pass]);
|
||||||
|
if (!is_resource($proc)) return false;
|
||||||
|
|
||||||
|
stream_get_contents($pipes[1]);
|
||||||
|
stream_get_contents($pipes[2]);
|
||||||
|
fclose($pipes[1]);
|
||||||
|
fclose($pipes[2]);
|
||||||
|
|
||||||
|
return proc_close($proc) === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** حالة المفتاح بلا كشف قيمته. */
|
||||||
|
function keyState(array $env, string $name): string
|
||||||
|
{
|
||||||
|
if (!array_key_exists($name, $env)) return 'مفقود';
|
||||||
|
return envIsPlaceholder($env[$name]) ? 'نائب' : 'مضبوط';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** بصمة قصيرة تكفي للتمييز ولا تكفي للاستعمال. */
|
||||||
|
function keyFingerprint(array $env, string $name): string
|
||||||
|
{
|
||||||
|
$v = $env[$name] ?? '';
|
||||||
|
return $v === '' ? '—' : substr(hash('sha256', $v), 0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
$csrf = $_SESSION['csrf'] ?? '';
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="ar" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="robots" content="noindex,nofollow">
|
||||||
|
<title>مفاتيح البيئة — سيرو</title>
|
||||||
|
<style>
|
||||||
|
:root { color-scheme: dark; }
|
||||||
|
body { font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||||
|
background:#12151a; color:#e6e9ef; margin:0; padding:2rem 1rem; }
|
||||||
|
.wrap { max-width: 940px; margin: 0 auto; }
|
||||||
|
h1 { font-size:1.4rem; margin:0 0 .3rem; }
|
||||||
|
.sub { color:#8b93a7; font-size:.85rem; margin-bottom:1.5rem; }
|
||||||
|
.card { background:#1a1f27; border:1px solid #2a3140; border-radius:12px;
|
||||||
|
padding:1.1rem 1.25rem; margin-bottom:1.25rem; }
|
||||||
|
.card h2 { font-size:1rem; margin:0 0 .25rem; }
|
||||||
|
.card .hint { color:#8b93a7; font-size:.8rem; margin:0 0 .9rem; line-height:1.6; }
|
||||||
|
table { width:100%; border-collapse:collapse; }
|
||||||
|
td, th { padding:.5rem .4rem; text-align:right; border-bottom:1px solid #232a35;
|
||||||
|
font-size:.88rem; vertical-align:middle; }
|
||||||
|
th { color:#8b93a7; font-weight:600; font-size:.78rem; }
|
||||||
|
code { font-family: ui-monospace, monospace; font-size:.82rem; }
|
||||||
|
.state { font-size:.75rem; padding:.15rem .5rem; border-radius:999px; }
|
||||||
|
.ok { background:#12341f; color:#5ed68b; }
|
||||||
|
.miss { background:#3a1f1f; color:#ff8b8b; }
|
||||||
|
.ph { background:#3a3320; color:#ffc861; }
|
||||||
|
button { background:#2d6cdf; color:#fff; border:0; border-radius:8px;
|
||||||
|
padding:.4rem .9rem; font-size:.82rem; cursor:pointer; font-family:inherit; }
|
||||||
|
button:hover { background:#3d7cef; }
|
||||||
|
button.warn { background:#8a3a2a; }
|
||||||
|
button.warn:hover { background:#a1462f; }
|
||||||
|
input[type=text], input[type=password] { background:#12151a; border:1px solid #2a3140;
|
||||||
|
color:#e6e9ef; border-radius:8px; padding:.45rem .6rem; font-family:inherit;
|
||||||
|
font-size:.85rem; width:100%; box-sizing:border-box; }
|
||||||
|
.frozen { opacity:.72; }
|
||||||
|
.banner { border-radius:10px; padding:.75rem 1rem; margin-bottom:1.25rem; font-size:.88rem; }
|
||||||
|
.banner.err { background:#3a1f1f; border:1px solid #6b3030; }
|
||||||
|
.banner.ok { background:#12341f; border:1px solid #2f6b45; }
|
||||||
|
.banner.danger { background:#33221a; border:1px solid #7a4426; line-height:1.7; }
|
||||||
|
form.inline { display:inline; }
|
||||||
|
.row-actions { display:flex; gap:.5rem; align-items:center; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
|
||||||
|
<?php if (!$authed): ?>
|
||||||
|
<h1>مفاتيح البيئة</h1>
|
||||||
|
<p class="sub">وصول محلي فقط عبر نفق SSH.</p>
|
||||||
|
<?php if ($error): ?><div class="banner err"><?= htmlspecialchars($error) ?></div><?php endif; ?>
|
||||||
|
<div class="card">
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="action" value="login">
|
||||||
|
<p class="hint">عبارة مرور اللوحة</p>
|
||||||
|
<input type="password" name="passphrase" autofocus autocomplete="off">
|
||||||
|
<p></p>
|
||||||
|
<button type="submit">دخول</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
|
||||||
|
<h1>مفاتيح البيئة</h1>
|
||||||
|
<p class="sub">
|
||||||
|
<code><?= htmlspecialchars($envFile) ?></code> ·
|
||||||
|
<a href="?logout=1" style="color:#8b93a7">خروج</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<?php if ($notice): ?><div class="banner ok"><?= htmlspecialchars($notice) ?></div><?php endif; ?>
|
||||||
|
<?php if ($error): ?><div class="banner err"><?= htmlspecialchars($error) ?></div><?php endif; ?>
|
||||||
|
|
||||||
|
<div class="banner danger">
|
||||||
|
<strong>بعد أي تغيير</strong> — أعد تشغيل الحاويات لتقرأ الجديد:
|
||||||
|
<code>docker compose restart php php_food php_transit</code><br>
|
||||||
|
وتذكّر أن <code>WEBHOOK_SECRET_KEY</code> له نظير في تطبيق
|
||||||
|
<code>sms_relay</code> يحتاج إعادة بناء، و<code>S2S_SHARED_KEY</code>
|
||||||
|
و<code>INTERNAL_SOCKET_KEY</code> لهما نظائر في خوادم السوكيت والدفع.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── أسرار داخلية ── -->
|
||||||
|
<div class="card">
|
||||||
|
<h2>أسرار داخلية</h2>
|
||||||
|
<p class="hint">طرفاها كلاهما لنا. التدوير آمن — يكفي إعادة تشغيل ما يحملها.</p>
|
||||||
|
<table>
|
||||||
|
<tr><th>المفتاح</th><th>الحالة</th><th>بصمة</th><th></th></tr>
|
||||||
|
<?php foreach (ENV_GENERATED as $name => $bytes):
|
||||||
|
$s = keyState($env, $name);
|
||||||
|
$cls = $s === 'مضبوط' ? 'ok' : ($s === 'نائب' ? 'ph' : 'miss'); ?>
|
||||||
|
<tr>
|
||||||
|
<td><code><?= htmlspecialchars($name) ?></code></td>
|
||||||
|
<td><span class="state <?= $cls ?>"><?= $s ?></span></td>
|
||||||
|
<td><code><?= keyFingerprint($env, $name) ?></code></td>
|
||||||
|
<td>
|
||||||
|
<form method="post" class="inline">
|
||||||
|
<input type="hidden" name="csrf" value="<?= htmlspecialchars($csrf) ?>">
|
||||||
|
<input type="hidden" name="action" value="rotate">
|
||||||
|
<input type="hidden" name="key" value="<?= htmlspecialchars($name) ?>">
|
||||||
|
<button type="submit"><?= $s === 'مضبوط' ? 'تدوير' : 'توليد' ?></button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── مرتبطة بالجلسات ── -->
|
||||||
|
<div class="card">
|
||||||
|
<h2>مرتبطة بالجلسات</h2>
|
||||||
|
<p class="hint">
|
||||||
|
تدويرها يُخرج <strong>كل</strong> السائقين والركّاب من حساباتهم ويطالبهم
|
||||||
|
بتسجيل دخول جديد. ليس عطلاً، لكنه ليس شيئاً يُفعل وقت الذروة.
|
||||||
|
</p>
|
||||||
|
<table>
|
||||||
|
<tr><th>المفتاح</th><th>الحالة</th><th>بصمة</th><th></th></tr>
|
||||||
|
<?php foreach (ENV_SESSION_SCOPED as $name => $bytes):
|
||||||
|
$s = keyState($env, $name);
|
||||||
|
$cls = $s === 'مضبوط' ? 'ok' : ($s === 'نائب' ? 'ph' : 'miss'); ?>
|
||||||
|
<tr>
|
||||||
|
<td><code><?= htmlspecialchars($name) ?></code></td>
|
||||||
|
<td><span class="state <?= $cls ?>"><?= $s ?></span></td>
|
||||||
|
<td><code><?= keyFingerprint($env, $name) ?></code></td>
|
||||||
|
<td>
|
||||||
|
<form method="post" class="inline"
|
||||||
|
onsubmit="return confirm('سيُخرج هذا كل المستخدمين من حساباتهم. متابعة؟')">
|
||||||
|
<input type="hidden" name="csrf" value="<?= htmlspecialchars($csrf) ?>">
|
||||||
|
<input type="hidden" name="action" value="rotate">
|
||||||
|
<input type="hidden" name="key" value="<?= htmlspecialchars($name) ?>">
|
||||||
|
<button type="submit" class="warn">تدوير</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── قواعد البيانات ── -->
|
||||||
|
<div class="card">
|
||||||
|
<h2>كلمات مرور قواعد البيانات</h2>
|
||||||
|
<p class="hint">
|
||||||
|
التدوير يجري بالترتيب الصحيح: <code>ALTER USER</code> ثم تحقّق باتصال
|
||||||
|
فعلي ثم كتابة <code>.env</code> — وإن فشلت أي خطوة تُستعاد الكلمة
|
||||||
|
القديمة فوراً. <strong>لا مساس بالبيانات ولا إعادة بناء</strong>؛ هذا
|
||||||
|
تغيير في <code>mysql.user</code> لا غير.
|
||||||
|
</p>
|
||||||
|
<table>
|
||||||
|
<tr><th>المفتاح</th><th>المستخدم</th><th>القاعدة</th><th>الحالة</th><th></th></tr>
|
||||||
|
<?php foreach (ENV_DB_CREDENTIALS as $name => $map):
|
||||||
|
$s = keyState($env, $name);
|
||||||
|
$cls = $s === 'مضبوط' ? 'ok' : ($s === 'نائب' ? 'ph' : 'miss'); ?>
|
||||||
|
<tr>
|
||||||
|
<td><code><?= htmlspecialchars($name) ?></code></td>
|
||||||
|
<td><code><?= htmlspecialchars($env[$map['user']] ?? '—') ?></code></td>
|
||||||
|
<td><code><?= htmlspecialchars($env[$map['db']] ?? '—') ?></code></td>
|
||||||
|
<td><span class="state <?= $cls ?>"><?= $s ?></span></td>
|
||||||
|
<td>
|
||||||
|
<?php if ($s === 'مضبوط'): ?>
|
||||||
|
<form method="post" class="inline"
|
||||||
|
onsubmit="return confirm('تدوير كلمة مرور القاعدة. أعد تشغيل حاويات php فوراً بعدها. متابعة؟')">
|
||||||
|
<input type="hidden" name="csrf" value="<?= htmlspecialchars($csrf) ?>">
|
||||||
|
<input type="hidden" name="action" value="rotate_db">
|
||||||
|
<input type="hidden" name="key" value="<?= htmlspecialchars($name) ?>">
|
||||||
|
<button type="submit" class="warn">تدوير</button>
|
||||||
|
</form>
|
||||||
|
<?php else: ?><span style="color:#8b93a7">—</span><?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── طرف ثالث ── -->
|
||||||
|
<div class="card">
|
||||||
|
<h2>مفاتيح طرف ثالث</h2>
|
||||||
|
<p class="hint">لا تُولَّد — تُنسخ من لوحة المزوّد وتُلصق هنا.</p>
|
||||||
|
<table>
|
||||||
|
<tr><th>المفتاح</th><th>الحالة</th><th style="width:45%">قيمة جديدة</th><th></th></tr>
|
||||||
|
<?php foreach (ENV_EXTERNAL as $name):
|
||||||
|
$s = keyState($env, $name);
|
||||||
|
$cls = $s === 'مضبوط' ? 'ok' : ($s === 'نائب' ? 'ph' : 'miss'); ?>
|
||||||
|
<tr>
|
||||||
|
<form method="post">
|
||||||
|
<td><code><?= htmlspecialchars($name) ?></code></td>
|
||||||
|
<td><span class="state <?= $cls ?>"><?= $s ?></span></td>
|
||||||
|
<td>
|
||||||
|
<input type="hidden" name="csrf" value="<?= htmlspecialchars($csrf) ?>">
|
||||||
|
<input type="hidden" name="action" value="set">
|
||||||
|
<input type="hidden" name="key" value="<?= htmlspecialchars($name) ?>">
|
||||||
|
<input type="text" name="value" autocomplete="off" placeholder="اتركه فارغاً لعدم التغيير">
|
||||||
|
</td>
|
||||||
|
<td><button type="submit">حفظ</button></td>
|
||||||
|
</form>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── المجمّدة ── -->
|
||||||
|
<div class="card frozen">
|
||||||
|
<h2>⛔ مفاتيح مجمّدة — للعلم فقط</h2>
|
||||||
|
<p class="hint">
|
||||||
|
بيانات مخزَّنة في القاعدة مشتقّة من هذه المفاتيح: أسماء الركّاب
|
||||||
|
وهواتفهم ولوحات السيارات مشفّرة بـ <code>ENC_KEY</code>، وأعمدة
|
||||||
|
<code>*_bidx</code> محسوبة بـ <code>BLIND_INDEX_PEPPER</code>.
|
||||||
|
تغيير أيٍّ منها <strong>لا يعيد تشفير شيء</strong> — يجعل كل صف قديم
|
||||||
|
غير قابل للقراءة إلى الأبد، ولا نسخة احتياطية تنفع لأنها هي نفسها
|
||||||
|
مشفّرة بالمفتاح القديم. تدويرها ترحيلُ بيانات لا تغييرُ إعداد،
|
||||||
|
ولذلك لا يوجد لها زر هنا ولا مسار يكتبها في هذا الملف.
|
||||||
|
</p>
|
||||||
|
<table>
|
||||||
|
<tr><th>المفتاح</th><th>الحالة</th><th>بصمة</th></tr>
|
||||||
|
<?php foreach (ENV_FROZEN as $name):
|
||||||
|
$s = keyState($env, $name);
|
||||||
|
$cls = $s === 'مضبوط' ? 'ok' : ($s === 'نائب' ? 'ph' : 'miss'); ?>
|
||||||
|
<tr>
|
||||||
|
<td><code><?= htmlspecialchars($name) ?></code></td>
|
||||||
|
<td><span class="state <?= $cls ?>"><?= $s ?></span></td>
|
||||||
|
<td><code><?= keyFingerprint($env, $name) ?></code></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="sub">سجلّ التغييرات: <code>backend/tools/panel/panel_audit.log</code></p>
|
||||||
|
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user