527 lines
24 KiB
PHP
527 lines
24 KiB
PHP
<?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>
|