Revamp Adminer UI with new CSS styles and enhanced HTML structure for improved user experience
This commit is contained in:
parent
b74752b2fd
commit
156a0ddeb3
@ -1,113 +1,353 @@
|
||||
/* Adminer Mini-Admin UI – angelehnt an style.css (Glassmorphism + Accent) */
|
||||
:root{
|
||||
--bg:#0b1020;
|
||||
--text:#e5e7eb;
|
||||
--muted:#94a3b8;
|
||||
--card:rgba(255,255,255,.06);
|
||||
--cardBorder:rgba(255,255,255,.10);
|
||||
--accent:#6366f1;
|
||||
--accent2:#22d3ee;
|
||||
--danger:#ef4444;
|
||||
--success:#22c55e;
|
||||
/* ===== ADMINER – gleicher Look wie style.css ===== */
|
||||
|
||||
/* ── RESET & BASE ─────────────────────────────── */
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #0f0f13;
|
||||
--surface: #1a1a24;
|
||||
--surface-hover: #22222f;
|
||||
--border: rgba(255,255,255,0.07);
|
||||
--text: #e8e8f0;
|
||||
--text-muted: #8888a8;
|
||||
--radius: 16px;
|
||||
--transition: 0.25s ease;
|
||||
--accent: #6366f1;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
html,body{height:100%}
|
||||
body{
|
||||
margin:0;
|
||||
font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;
|
||||
background:radial-gradient(1200px 600px at 20% 10%, rgba(99,102,241,.25), transparent 60%),
|
||||
radial-gradient(900px 600px at 80% 30%, rgba(34,211,238,.18), transparent 60%),
|
||||
radial-gradient(900px 700px at 40% 90%, rgba(168,85,247,.14), transparent 60%),
|
||||
var(--bg);
|
||||
color:var(--text);
|
||||
html { scroll-behavior: smooth; overflow-x: hidden; }
|
||||
|
||||
body {
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 2rem 1rem 4rem;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.wrap{max-width:1100px;margin:34px auto;padding:0 16px}
|
||||
|
||||
.top{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:12px;
|
||||
margin-bottom:16px;
|
||||
/* ── BACKGROUND ───────────────────────────────── */
|
||||
@keyframes bgShift {
|
||||
0% { opacity:1; transform:scale(1) translate(0,0); }
|
||||
33% { opacity:.85; transform:scale(1.15) translate(2%,3%); }
|
||||
66% { opacity:.9; transform:scale(1.08) translate(-3%,-2%); }
|
||||
100% { opacity:1; transform:scale(1) translate(0,0); }
|
||||
}
|
||||
|
||||
h1{font-size:28px;letter-spacing:.2px;margin:0}
|
||||
|
||||
a{color:#93c5fd;text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
|
||||
.pill{
|
||||
font-size:12px;
|
||||
background:rgba(255,255,255,.10);
|
||||
border:1px solid rgba(255,255,255,.14);
|
||||
padding:5px 10px;
|
||||
border-radius:999px;
|
||||
.background-blur {
|
||||
position:fixed; inset:-25%; z-index:-1; pointer-events:none;
|
||||
background:
|
||||
radial-gradient(ellipse 60% 40% at 20% 10%, rgba(99,102,241,.18), transparent),
|
||||
radial-gradient(ellipse 50% 40% at 80% 80%, rgba(14,165,233,.13), transparent),
|
||||
radial-gradient(ellipse 40% 30% at 60% 40%, rgba(168,85,247,.08), transparent);
|
||||
animation: bgShift 12s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.card{
|
||||
background:var(--card);
|
||||
border:1px solid var(--cardBorder);
|
||||
border-radius:16px;
|
||||
padding:16px;
|
||||
backdrop-filter: blur(14px);
|
||||
box-shadow:0 18px 40px rgba(0,0,0,.35);
|
||||
#particle-canvas {
|
||||
position:fixed; inset:0; z-index:-1; pointer-events:none; opacity:.55;
|
||||
}
|
||||
|
||||
.grid{display:grid;grid-template-columns:320px 1fr;gap:16px}
|
||||
@media (max-width: 900px){.grid{grid-template-columns:1fr}}
|
||||
|
||||
.muted{color:var(--muted)}
|
||||
|
||||
label{display:block;color:#cbd5e1;font-weight:600}
|
||||
|
||||
input,textarea,select{
|
||||
width:100%;
|
||||
padding:10px 12px;
|
||||
margin-top:6px;
|
||||
border-radius:12px;
|
||||
background:rgba(0,0,0,.35);
|
||||
border:1px solid rgba(255,255,255,.14);
|
||||
color:var(--text);
|
||||
outline:none;
|
||||
/* ── ORBS ─────────────────────────────────────── */
|
||||
.orb {
|
||||
position:fixed; border-radius:50%; pointer-events:none; z-index:-1;
|
||||
filter:blur(70px); opacity:0;
|
||||
animation:orbFloat var(--dur,18s) ease-in-out infinite var(--delay,0s);
|
||||
}
|
||||
input:focus,textarea:focus,select:focus{border-color:rgba(99,102,241,.75);box-shadow:0 0 0 3px rgba(99,102,241,.20)}
|
||||
|
||||
hr{border:0;border-top:1px solid rgba(255,255,255,.10);margin:16px 0}
|
||||
|
||||
.btn{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
gap:8px;
|
||||
padding:10px 12px;
|
||||
border-radius:12px;
|
||||
border:1px solid rgba(255,255,255,.16);
|
||||
cursor:pointer;
|
||||
background:linear-gradient(135deg, rgba(99,102,241,.95), rgba(34,211,238,.55));
|
||||
color:white;
|
||||
text-decoration:none;
|
||||
transition:transform .12s ease, filter .12s ease;
|
||||
}
|
||||
.btn:hover{filter:brightness(1.06)}
|
||||
.btn:active{transform:translateY(1px)}
|
||||
.btn.secondary{
|
||||
background:rgba(255,255,255,.08);
|
||||
.orb-1 { width:320px;height:320px; background:radial-gradient(circle,rgba(99,102,241,.25),transparent 70%); top:-80px;left:-80px; --dur:20s;--delay:0s; }
|
||||
.orb-2 { width:260px;height:260px; background:radial-gradient(circle,rgba(14,165,233,.2), transparent 70%); bottom:5%;right:-60px; --dur:17s;--delay:-6s; }
|
||||
.orb-3 { width:200px;height:200px; background:radial-gradient(circle,rgba(168,85,247,.18),transparent 70%); top:45%;left:-40px; --dur:23s;--delay:-11s;}
|
||||
.orb-4 { width:180px;height:180px; background:radial-gradient(circle,rgba(236,72,153,.15), transparent 70%); top:20%;right:-30px; --dur:19s;--delay:-4s; }
|
||||
@keyframes orbFloat {
|
||||
0% {opacity:0; transform:translate(0,0) scale(1); }
|
||||
10% {opacity:1;}
|
||||
50% {opacity:1; transform:translate(20px,25px) scale(1.08); }
|
||||
90% {opacity:1;}
|
||||
100%{opacity:0; transform:translate(0,0) scale(1); }
|
||||
}
|
||||
|
||||
.notice{padding:10px 12px;border-radius:12px;border:1px solid rgba(255,255,255,.14)}
|
||||
.notice.ok{background:rgba(34,197,94,.12);border-color:rgba(34,197,94,.25);color:#bbf7d0}
|
||||
.notice.err{background:rgba(239,68,68,.10);border-color:rgba(239,68,68,.25);color:#fecaca}
|
||||
/* Grid lines */
|
||||
body::before {
|
||||
content:''; position:fixed; inset:0; z-index:-1; pointer-events:none;
|
||||
background-image:
|
||||
linear-gradient(rgba(255,255,255,.025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,.025) 1px, transparent 1px);
|
||||
background-size:60px 60px;
|
||||
mask-image:radial-gradient(ellipse 80% 80% at 50% 50%, black 30%, transparent 100%);
|
||||
-webkit-mask-image:radial-gradient(ellipse 80% 80% at 50% 50%, black 30%, transparent 100%);
|
||||
}
|
||||
|
||||
.table{width:100%;border-collapse:collapse;overflow:hidden;border-radius:14px}
|
||||
.table th,.table td{padding:9px 10px;border-bottom:1px solid rgba(255,255,255,.10);vertical-align:top}
|
||||
.table th{color:#cbd5e1;text-align:left;font-weight:700;background:rgba(255,255,255,.04)}
|
||||
.code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace}
|
||||
/* ── LAYOUT ───────────────────────────────────── */
|
||||
.wrap {
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.list{list-style:none;margin:0;padding:0}
|
||||
.list li{margin:6px 0}
|
||||
.list a{display:block;padding:8px 10px;border-radius:12px}
|
||||
.list a:hover{background:rgba(255,255,255,.06)}
|
||||
.list a.active{background:rgba(99,102,241,.16);border:1px solid rgba(99,102,241,.25)}
|
||||
/* ── ANIMATIONS ───────────────────────────────── */
|
||||
@keyframes fadeDown {
|
||||
from { opacity:0; transform:translateY(-20px); }
|
||||
to { opacity:1; transform:translateY(0); }
|
||||
}
|
||||
@keyframes fadeUp {
|
||||
from { opacity:0; transform:translateY(24px); }
|
||||
to { opacity:1; transform:translateY(0); }
|
||||
}
|
||||
@keyframes shimmerText {
|
||||
from { background-position:0% center; }
|
||||
to { background-position:200% center; }
|
||||
}
|
||||
|
||||
/* ── HEADER ───────────────────────────────────── */
|
||||
.admin-header {
|
||||
text-align:center;
|
||||
margin-bottom: 2.5rem;
|
||||
margin-top: 1.5rem;
|
||||
animation: fadeDown .7s ease both;
|
||||
}
|
||||
|
||||
@keyframes avatarPulse {
|
||||
0%,100% { box-shadow:0 0 0 4px rgba(99,102,241,.2),0 0 20px rgba(99,102,241,.1); }
|
||||
50% { box-shadow:0 0 0 8px rgba(99,102,241,.12),0 0 40px rgba(99,102,241,.2); }
|
||||
}
|
||||
|
||||
.admin-avatar {
|
||||
width:72px; height:72px; border-radius:50%;
|
||||
background:linear-gradient(135deg,#6366f1,#0ea5e9);
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
font-size:1.6rem; font-weight:700;
|
||||
margin:0 auto 1rem;
|
||||
animation:avatarPulse 3s ease-in-out infinite;
|
||||
transition:transform .3s ease;
|
||||
cursor:default; user-select:none;
|
||||
}
|
||||
.admin-avatar:hover { transform:scale(1.08) rotate(6deg); }
|
||||
|
||||
.admin-title {
|
||||
font-size: clamp(1.5rem,3.5vw,2.2rem);
|
||||
font-weight:700;
|
||||
background:linear-gradient(90deg,#e8e8f0 0%,#6366f1 40%,#0ea5e9 60%,#e8e8f0 100%);
|
||||
background-size:200% auto;
|
||||
-webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text;
|
||||
letter-spacing:-.5px;
|
||||
animation:shimmerText 5s linear infinite;
|
||||
}
|
||||
|
||||
.admin-subtitle {
|
||||
color:var(--text-muted);
|
||||
margin-top:.4rem;
|
||||
font-size:.95rem;
|
||||
animation: fadeDown .9s ease .3s both;
|
||||
}
|
||||
|
||||
/* ── PILL / TAG ───────────────────────────────── */
|
||||
.pill {
|
||||
display:inline-block;
|
||||
font-size:.72rem; font-weight:600;
|
||||
text-transform:uppercase; letter-spacing:1px;
|
||||
color:var(--text-muted);
|
||||
background:rgba(255,255,255,.06);
|
||||
border:1px solid var(--border);
|
||||
padding:3px 10px; border-radius:999px;
|
||||
vertical-align:middle; margin-left:8px;
|
||||
}
|
||||
|
||||
/* ── CARD ─────────────────────────────────────── */
|
||||
.card {
|
||||
background:var(--surface);
|
||||
border:1px solid var(--border);
|
||||
border-radius:var(--radius);
|
||||
padding:1.4rem 1.6rem;
|
||||
position:relative;
|
||||
overflow:hidden;
|
||||
animation: fadeUp .5s ease both;
|
||||
}
|
||||
.card::before {
|
||||
content:''; position:absolute; inset:0; border-radius:var(--radius);
|
||||
opacity:0; transition:opacity var(--transition);
|
||||
background:linear-gradient(135deg,rgba(99,102,241,.07),transparent 60%);
|
||||
pointer-events:none;
|
||||
}
|
||||
.card:hover::before { opacity:1; }
|
||||
|
||||
/* ── GRID LAYOUT ──────────────────────────────── */
|
||||
.admin-grid {
|
||||
display:grid;
|
||||
grid-template-columns:240px 1fr;
|
||||
gap:16px;
|
||||
align-items:start;
|
||||
}
|
||||
@media(max-width:700px){ .admin-grid{ grid-template-columns:1fr; } }
|
||||
|
||||
/* ── TYPOGRAPHY ───────────────────────────────── */
|
||||
h2 {
|
||||
font-size:1rem; font-weight:700;
|
||||
color:var(--text-muted);
|
||||
text-transform:uppercase; letter-spacing:1.2px;
|
||||
margin-bottom:.75rem;
|
||||
padding-bottom:.5rem;
|
||||
border-bottom:1px solid var(--border);
|
||||
}
|
||||
h3 {
|
||||
font-size:.95rem; font-weight:600; color:var(--text-muted);
|
||||
text-transform:uppercase; letter-spacing:1px;
|
||||
margin-bottom:.6rem;
|
||||
}
|
||||
a { color:#93c5fd; text-decoration:none; }
|
||||
a:hover { text-decoration:underline; }
|
||||
.muted { color:var(--text-muted); font-size:.88rem; }
|
||||
code { font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:.85em;
|
||||
background:rgba(255,255,255,.06); border:1px solid var(--border); padding:1px 5px; border-radius:5px; }
|
||||
|
||||
hr { border:0; border-top:1px solid var(--border); margin:1.2rem 0; }
|
||||
|
||||
/* ── FORM ─────────────────────────────────────── */
|
||||
label {
|
||||
display:block;
|
||||
font-size:.82rem; font-weight:600; color:var(--text-muted);
|
||||
text-transform:uppercase; letter-spacing:.8px;
|
||||
margin-bottom:.35rem;
|
||||
}
|
||||
input, textarea, select {
|
||||
width:100%; padding:10px 14px;
|
||||
border-radius:12px;
|
||||
background:rgba(255,255,255,.04);
|
||||
border:1px solid var(--border);
|
||||
color:var(--text);
|
||||
font-family:inherit; font-size:.95rem;
|
||||
outline:none;
|
||||
transition:border-color var(--transition), box-shadow var(--transition);
|
||||
}
|
||||
input:focus, textarea:focus, select:focus {
|
||||
border-color:rgba(99,102,241,.7);
|
||||
box-shadow:0 0 0 3px rgba(99,102,241,.15);
|
||||
}
|
||||
textarea { font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; resize:vertical; }
|
||||
|
||||
.field { margin-bottom:1rem; }
|
||||
|
||||
/* ── BUTTONS ──────────────────────────────────── */
|
||||
.btn {
|
||||
display:inline-flex; align-items:center; justify-content:center; gap:6px;
|
||||
padding:10px 18px; border-radius:var(--radius);
|
||||
border:1px solid rgba(255,255,255,.12);
|
||||
cursor:pointer; font-size:.9rem; font-weight:600; letter-spacing:.2px;
|
||||
background:linear-gradient(135deg,rgba(99,102,241,.9),rgba(14,165,233,.65));
|
||||
color:#fff;
|
||||
text-decoration:none;
|
||||
transition:transform var(--transition), filter var(--transition), box-shadow var(--transition);
|
||||
box-shadow:0 4px 18px rgba(99,102,241,.25);
|
||||
white-space:nowrap;
|
||||
}
|
||||
.btn:hover { filter:brightness(1.1); transform:translateY(-1px); text-decoration:none; }
|
||||
.btn:active { transform:translateY(1px); }
|
||||
|
||||
.btn-sm { padding:7px 12px; font-size:.82rem; border-radius:10px; }
|
||||
|
||||
.btn-ghost {
|
||||
background:rgba(255,255,255,.06);
|
||||
border:1px solid var(--border);
|
||||
box-shadow:none;
|
||||
}
|
||||
.btn-ghost:hover { background:rgba(255,255,255,.1); filter:none; }
|
||||
|
||||
/* ── TABS ─────────────────────────────────────── */
|
||||
.tabs { display:flex; gap:6px; margin-bottom:1.4rem; }
|
||||
.tab {
|
||||
padding:8px 18px; border-radius:10px; font-size:.88rem; font-weight:600;
|
||||
border:1px solid var(--border);
|
||||
background:rgba(255,255,255,.04); color:var(--text-muted);
|
||||
text-decoration:none; transition:background var(--transition), color var(--transition), border-color var(--transition);
|
||||
cursor:pointer;
|
||||
}
|
||||
.tab:hover { background:rgba(255,255,255,.08); color:var(--text); text-decoration:none; }
|
||||
.tab.active {
|
||||
background:rgba(99,102,241,.18); border-color:rgba(99,102,241,.4);
|
||||
color:#a5b4fc;
|
||||
}
|
||||
|
||||
/* ── NOTICE / ALERTS ──────────────────────────── */
|
||||
.notice {
|
||||
padding:10px 14px; border-radius:12px; font-size:.88rem;
|
||||
border:1px solid var(--border); margin-bottom:1rem;
|
||||
}
|
||||
.notice-ok { background:rgba(34,197,94,.1); border-color:rgba(34,197,94,.25); color:#86efac; }
|
||||
.notice-err { background:rgba(239,68,68,.09); border-color:rgba(239,68,68,.25); color:#fca5a5; }
|
||||
|
||||
/* ── NAV LIST (Tabellenliste) ─────────────────── */
|
||||
.nav-list { list-style:none; }
|
||||
.nav-list li { margin:3px 0; }
|
||||
.nav-list a {
|
||||
display:block; padding:8px 12px; border-radius:12px;
|
||||
color:var(--text-muted); font-size:.88rem;
|
||||
border:1px solid transparent;
|
||||
transition:background var(--transition), color var(--transition), border-color var(--transition);
|
||||
}
|
||||
.nav-list a:hover { background:var(--surface-hover); color:var(--text); text-decoration:none; }
|
||||
.nav-list a.active {
|
||||
background:rgba(99,102,241,.12);
|
||||
border-color:rgba(99,102,241,.25);
|
||||
color:#a5b4fc;
|
||||
}
|
||||
|
||||
/* ── TABLE ────────────────────────────────────── */
|
||||
.db-table {
|
||||
width:100%; border-collapse:collapse; font-size:.85rem;
|
||||
overflow:hidden;
|
||||
}
|
||||
.db-table thead th {
|
||||
padding:9px 10px; text-align:left;
|
||||
font-size:.75rem; font-weight:700; text-transform:uppercase; letter-spacing:.8px;
|
||||
color:var(--text-muted);
|
||||
background:rgba(255,255,255,.03);
|
||||
border-bottom:1px solid var(--border);
|
||||
}
|
||||
.db-table tbody td {
|
||||
padding:9px 10px;
|
||||
border-bottom:1px solid var(--border);
|
||||
vertical-align:top;
|
||||
font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
||||
font-size:.82rem;
|
||||
color:var(--text);
|
||||
word-break:break-word;
|
||||
max-width:280px;
|
||||
overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
|
||||
}
|
||||
.db-table tbody tr:hover td { background:rgba(255,255,255,.025); }
|
||||
.db-table tbody tr:last-child td { border-bottom:none; }
|
||||
.null-val { color:var(--text-muted); font-style:italic; font-size:.8rem; }
|
||||
|
||||
/* ── PAGINATION ───────────────────────────────── */
|
||||
.pagination { display:flex; gap:8px; margin-top:12px; align-items:center; }
|
||||
|
||||
/* ── TOP BAR ──────────────────────────────────── */
|
||||
.top-bar {
|
||||
display:flex; align-items:center; justify-content:space-between; gap:12px;
|
||||
margin-bottom:1.4rem;
|
||||
flex-wrap:wrap;
|
||||
}
|
||||
.top-bar-left { display:flex; align-items:center; gap:10px; }
|
||||
.top-bar-actions { display:flex; gap:8px; flex-wrap:wrap; }
|
||||
|
||||
/* ── LOGIN CARD CENTERED ──────────────────────── */
|
||||
.login-wrap {
|
||||
width:100%; max-width:420px;
|
||||
margin:0 auto;
|
||||
animation: fadeUp .5s ease both;
|
||||
}
|
||||
|
||||
/* ── BACK LINK ────────────────────────────────── */
|
||||
.back-link {
|
||||
display:inline-flex; align-items:center; gap:5px;
|
||||
color:var(--text-muted); font-size:.85rem;
|
||||
margin-bottom:1.2rem;
|
||||
}
|
||||
.back-link:hover { color:var(--text); text-decoration:none; }
|
||||
|
||||
@media(max-width:480px) {
|
||||
body { padding:1.2rem .75rem 3rem; }
|
||||
.admin-grid { grid-template-columns:1fr; }
|
||||
}
|
||||
|
||||
@ -10,131 +10,117 @@ adminer_app_session_start();
|
||||
try {
|
||||
adminer_app_bootstrap();
|
||||
} catch (Throwable $e) {
|
||||
admin_layout('DB-Verwaltung', '<div class="top"><h1>DB-Verwaltung</h1></div><div class="card"><div class="err">' . h($e->getMessage()) . '</div></div>');
|
||||
admin_layout('DB-Verwaltung', '<div class="notice notice-err">' . h($e->getMessage()) . '</div>', 'Fehler beim Start');
|
||||
exit;
|
||||
}
|
||||
|
||||
$appPage = (string)($_GET['page'] ?? 'login'); // login|register
|
||||
$appAction = (string)($_GET['auth'] ?? '');
|
||||
if ($appAction === 'logout') {
|
||||
// App-Logout
|
||||
if ((string)($_GET['auth'] ?? '') === 'logout') {
|
||||
adminer_app_logout();
|
||||
header('Location: /adminer', true, 302);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Handle app login
|
||||
$appPage = (string)($_GET['page'] ?? 'login');
|
||||
$appError = null;
|
||||
$appRegError = null;
|
||||
$appRegOk = null;
|
||||
|
||||
// Login POST
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'app_login') {
|
||||
$u = (string)($_POST['username'] ?? '');
|
||||
$p = (string)($_POST['password'] ?? '');
|
||||
$res = adminer_app_try_login($u, $p);
|
||||
$res = adminer_app_try_login((string)($_POST['username'] ?? ''), (string)($_POST['password'] ?? ''));
|
||||
if (!empty($res['ok'])) {
|
||||
header('Location: /adminer', true, 302);
|
||||
exit;
|
||||
}
|
||||
$appError = (string)($res['error'] ?? 'Login fehlgeschlagen.');
|
||||
$appPage = 'login';
|
||||
$appPage = 'login';
|
||||
}
|
||||
|
||||
// Handle registration
|
||||
$appRegError = null;
|
||||
$appRegOk = null;
|
||||
// Register POST
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'app_register') {
|
||||
$u = (string)($_POST['username'] ?? '');
|
||||
$p1 = (string)($_POST['password'] ?? '');
|
||||
$p2 = (string)($_POST['password2'] ?? '');
|
||||
|
||||
$res = adminer_app_try_register($u, $p1, $p2);
|
||||
$res = adminer_app_try_register(
|
||||
(string)($_POST['username'] ?? ''),
|
||||
(string)($_POST['password'] ?? ''),
|
||||
(string)($_POST['password2'] ?? '')
|
||||
);
|
||||
if (!empty($res['ok'])) {
|
||||
// Auto-login after register
|
||||
$loginRes = adminer_app_try_login($u, $p1);
|
||||
if (!empty($loginRes['ok'])) {
|
||||
$lr = adminer_app_try_login((string)($_POST['username'] ?? ''), (string)($_POST['password'] ?? ''));
|
||||
if (!empty($lr['ok'])) {
|
||||
header('Location: /adminer', true, 302);
|
||||
exit;
|
||||
}
|
||||
$appRegOk = 'Registrierung erfolgreich. Bitte einloggen.';
|
||||
$appPage = 'login';
|
||||
$appRegOk = 'Konto erstellt! Bitte einloggen.';
|
||||
$appPage = 'login';
|
||||
} else {
|
||||
$appRegError = (string)($res['error'] ?? 'Registrierung fehlgeschlagen.');
|
||||
$appPage = 'register';
|
||||
$appPage = 'register';
|
||||
}
|
||||
}
|
||||
|
||||
// ── LOGIN / REGISTER SEITE ────────────────────────────────────────────────
|
||||
if (!adminer_app_is_logged_in()) {
|
||||
$canRegister = adminer_app_allow_register();
|
||||
$canReg = adminer_app_allow_register();
|
||||
$isReg = ($appPage === 'register');
|
||||
|
||||
$body = '<div class="top"><h1>DB-Verwaltung</h1><span class="pill">Login</span></div>';
|
||||
$body .= '<div class="card">';
|
||||
$body = '<div class="login-wrap">';
|
||||
|
||||
// Tabs
|
||||
$body .= '<div style="display:flex;gap:10px;margin-bottom:14px">'
|
||||
. '<a class="btn secondary" href="/adminer?page=login" style="text-decoration:none">Login</a>'
|
||||
. ($canRegister ? '<a class="btn secondary" href="/adminer?page=register" style="text-decoration:none">Registrieren</a>' : '')
|
||||
. '</div>';
|
||||
$body .= '<div class="tabs">';
|
||||
$body .= '<a class="tab ' . (!$isReg ? 'active' : '') . '" href="/adminer?page=login">Login</a>';
|
||||
if ($canReg) $body .= '<a class="tab ' . ($isReg ? 'active' : '') . '" href="/adminer?page=register">Registrieren</a>';
|
||||
$body .= '</div>';
|
||||
|
||||
if ($appRegOk) {
|
||||
$body .= '<div class="notice ok">' . h($appRegOk) . '</div><br>';
|
||||
}
|
||||
$body .= '<div class="card">';
|
||||
|
||||
if ($appPage === 'register') {
|
||||
if (!$canRegister) {
|
||||
$body .= '<div class="notice err">Registrierung ist deaktiviert.</div>';
|
||||
} else {
|
||||
if ($appRegError) $body .= '<div class="notice err">' . h($appRegError) . '</div><br>';
|
||||
if ($appRegOk) $body .= '<div class="notice notice-ok">' . h($appRegOk) . '</div>';
|
||||
if ($appError) $body .= '<div class="notice notice-err">' . h($appError) . '</div>';
|
||||
if ($appRegError)$body .= '<div class="notice notice-err">' . h($appRegError) . '</div>';
|
||||
|
||||
$body .= '<h3>Registrieren</h3>';
|
||||
$body .= '<form method="post">'
|
||||
. '<input type="hidden" name="action" value="app_register">'
|
||||
. '<label>Benutzername<br><input name="username" autocomplete="username"></label><br><br>'
|
||||
. '<label>Passwort<br><input name="password" type="password" autocomplete="new-password"></label><br><br>'
|
||||
. '<label>Passwort wiederholen<br><input name="password2" type="password" autocomplete="new-password"></label><br><br>'
|
||||
. '<button class="btn" type="submit">Account erstellen</button>'
|
||||
. '</form>';
|
||||
|
||||
$body .= '<div class="muted" style="margin-top:12px">Dein Account wird in <code class="code">FabianWebsite.adminer_users</code> gespeichert.</div>';
|
||||
}
|
||||
if ($isReg && $canReg) {
|
||||
// ── Registrierungsformular ──────────────────────────────────────
|
||||
$body .= '<form method="post">'
|
||||
. '<input type="hidden" name="action" value="app_register">'
|
||||
. '<div class="field"><label>Benutzername</label><input name="username" autocomplete="username" placeholder="z.B. fabian"></div>'
|
||||
. '<div class="field"><label>Passwort</label><input name="password" type="password" autocomplete="new-password" placeholder="mind. 8 Zeichen"></div>'
|
||||
. '<div class="field"><label>Passwort wiederholen</label><input name="password2" type="password" autocomplete="new-password" placeholder="Wiederholung"></div>'
|
||||
. '<button class="btn" type="submit" style="width:100%">Konto erstellen</button>'
|
||||
. '</form>';
|
||||
$body .= '<p class="muted" style="margin-top:.9rem;text-align:center">Bereits ein Konto? <a href="/adminer?page=login">Login</a></p>';
|
||||
} else {
|
||||
if ($appError) $body .= '<div class="notice err">' . h($appError) . '</div><br>';
|
||||
|
||||
$body .= '<h3>Login</h3>';
|
||||
// ── Login-Formular ──────────────────────────────────────────────
|
||||
$body .= '<form method="post">'
|
||||
. '<input type="hidden" name="action" value="app_login">'
|
||||
. '<label>Benutzername<br><input name="username" autocomplete="username"></label><br><br>'
|
||||
. '<label>Passwort<br><input name="password" type="password" autocomplete="current-password"></label><br><br>'
|
||||
. '<button class="btn" type="submit">Anmelden</button>'
|
||||
. '<div class="field"><label>Benutzername</label><input name="username" autocomplete="username" placeholder="Dein Benutzername"></div>'
|
||||
. '<div class="field"><label>Passwort</label><input name="password" type="password" autocomplete="current-password" placeholder="Dein Passwort"></div>'
|
||||
. '<button class="btn" type="submit" style="width:100%">Anmelden</button>'
|
||||
. '</form>';
|
||||
|
||||
if ($canRegister) {
|
||||
$body .= '<div class="muted" style="margin-top:12px">Noch kein Account? <a href="/adminer?page=register">Registrieren</a></div>';
|
||||
}
|
||||
if ($canReg) $body .= '<p class="muted" style="margin-top:.9rem;text-align:center">Noch kein Konto? <a href="/adminer?page=register">Registrieren</a></p>';
|
||||
}
|
||||
|
||||
$body .= '</div>';
|
||||
$body .= '</div>';
|
||||
|
||||
admin_layout('DB-Verwaltung', $body);
|
||||
admin_layout('DB-Verwaltung', $body, $isReg ? 'Neues Konto erstellen' : 'Bitte einloggen');
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── DB-VERBINDUNGS-LOGIN ──────────────────────────────────────────────────
|
||||
require_once __DIR__ . '/auth.php';
|
||||
|
||||
admin_session_start();
|
||||
|
||||
$action = (string)($_GET['a'] ?? '');
|
||||
if ($action === 'logout') {
|
||||
if ((string)($_GET['a'] ?? '') === 'logout') {
|
||||
admin_logout();
|
||||
header('Location: /adminer', true, 302);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── DB-Auswahl (Step 1) ───────────────────────────────────────────────────
|
||||
// Wir speichern Host/Port/User/Pass kurz in der Session, um die DB-Liste zu holen.
|
||||
if (!isset($_SESSION['db_admin_select'])) {
|
||||
$_SESSION['db_admin_select'] = [];
|
||||
}
|
||||
if (!isset($_SESSION['db_admin_select'])) $_SESSION['db_admin_select'] = [];
|
||||
|
||||
$selectError = null;
|
||||
$selectMsg = null;
|
||||
$selectMsg = null;
|
||||
|
||||
// Probe: Datenbanken laden
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'probe') {
|
||||
$host = trim((string)($_POST['host'] ?? ''));
|
||||
$port = (int)($_POST['port'] ?? 3306);
|
||||
@ -145,269 +131,240 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') ==
|
||||
$selectError = 'Bitte Host, Port und Benutzer angeben.';
|
||||
} else {
|
||||
try {
|
||||
$dsn = sprintf('mysql:host=%s;port=%d;charset=utf8mb4', $host, $port);
|
||||
$pdo = new PDO($dsn, $user, $pass, [
|
||||
$pdo = new PDO(sprintf('mysql:host=%s;port=%d;charset=utf8mb4', $host, $port), $user, $pass, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
$dbs = $pdo->query('SHOW DATABASES')->fetchAll(PDO::FETCH_COLUMN, 0);
|
||||
|
||||
$_SESSION['db_admin_select'] = [
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
'user' => $user,
|
||||
'pass' => $pass,
|
||||
'dbs' => $dbs,
|
||||
];
|
||||
$selectMsg = 'Datenbanken geladen.';
|
||||
$_SESSION['db_admin_select'] = compact('host', 'port', 'user', 'pass', 'dbs');
|
||||
$selectMsg = 'Datenbanken geladen – bitte unten eine auswählen.';
|
||||
} catch (Throwable $e) {
|
||||
$selectError = 'Konnte Datenbanken nicht laden: ' . $e->getMessage();
|
||||
$selectError = 'Fehler: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Login (Step 2)
|
||||
$error = null;
|
||||
// DB-Login
|
||||
$dbError = null;
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'login') {
|
||||
$host = trim((string)($_POST['host'] ?? ''));
|
||||
$port = (int)($_POST['port'] ?? 3306);
|
||||
$user = trim((string)($_POST['user'] ?? ''));
|
||||
$pass = (string)($_POST['pass'] ?? '');
|
||||
$db = trim((string)($_POST['db'] ?? ''));
|
||||
|
||||
$res = admin_try_login($host, $port, $user, $pass, $db);
|
||||
$res = admin_try_login(
|
||||
trim((string)($_POST['host'] ?? '')),
|
||||
(int)($_POST['port'] ?? 3306),
|
||||
trim((string)($_POST['user'] ?? '')),
|
||||
(string)($_POST['pass'] ?? ''),
|
||||
trim((string)($_POST['db'] ?? ''))
|
||||
);
|
||||
if ($res['ok']) {
|
||||
header('Location: /adminer', true, 302);
|
||||
exit;
|
||||
}
|
||||
$error = (string)($res['error'] ?? 'Login fehlgeschlagen.');
|
||||
$dbError = (string)($res['error'] ?? 'Login fehlgeschlagen.');
|
||||
}
|
||||
|
||||
$defaults = admin_default_creds();
|
||||
$defaults = admin_default_creds();
|
||||
$selectState = is_array($_SESSION['db_admin_select']) ? $_SESSION['db_admin_select'] : [];
|
||||
$prefHost = isset($selectState['host']) ? (string)$selectState['host'] : (string)$defaults['host'];
|
||||
$prefPort = isset($selectState['port']) ? (int)$selectState['port'] : (int)$defaults['port'];
|
||||
$prefUser = isset($selectState['user']) ? (string)$selectState['user'] : (string)$defaults['user'];
|
||||
$prefPass = isset($selectState['pass']) ? (string)$selectState['pass'] : '';
|
||||
$dbList = isset($selectState['dbs']) && is_array($selectState['dbs']) ? $selectState['dbs'] : [];
|
||||
|
||||
// UI when not logged in
|
||||
if (!admin_is_logged_in()) {
|
||||
$prefHost = isset($selectState['host']) ? (string)$selectState['host'] : (string)$defaults['host'];
|
||||
$prefPort = isset($selectState['port']) ? (int)$selectState['port'] : (int)$defaults['port'];
|
||||
$prefUser = isset($selectState['user']) ? (string)$selectState['user'] : (string)$defaults['user'];
|
||||
$prefPass = isset($selectState['pass']) ? (string)$selectState['pass'] : '';
|
||||
$body = '<div class="card" style="max-width:520px;margin:0 auto">';
|
||||
|
||||
$dbList = isset($selectState['dbs']) && is_array($selectState['dbs']) ? $selectState['dbs'] : [];
|
||||
if ($selectMsg) $body .= '<div class="notice notice-ok">' . h($selectMsg) . '</div>';
|
||||
if ($selectError)$body .= '<div class="notice notice-err">' . h($selectError) . '</div>';
|
||||
if ($dbError) $body .= '<div class="notice notice-err">' . h($dbError) . '</div>';
|
||||
|
||||
$body = "<div class=\"top\"><h1>DB-Verwaltung</h1><span class=\"pill\">Mini-Admin</span></div>";
|
||||
$body .= "<div class=\"card\">";
|
||||
$body .= "<p class=\"muted\">Zuerst Verbindungsdaten prüfen (ohne DB), dann bekommst du eine Datenbank-Auswahl. Defaults kommen aus <code>.env</code>.</p>";
|
||||
// Step 1
|
||||
$body .= '<h2>1 · Server verbinden</h2>';
|
||||
$body .= '<form method="post">'
|
||||
. '<input type="hidden" name="action" value="probe">'
|
||||
. '<div class="field"><label>Host</label><input name="host" value="' . h($prefHost) . '" placeholder="localhost"></div>'
|
||||
. '<div class="field"><label>Port</label><input name="port" type="number" value="' . h((string)$prefPort) . '"></div>'
|
||||
. '<div class="field"><label>Benutzer</label><input name="user" value="' . h($prefUser) . '"></div>'
|
||||
. '<div class="field"><label>Passwort</label><input name="pass" type="password" value="' . h($prefPass) . '"></div>'
|
||||
. '<button class="btn btn-ghost btn-sm" type="submit">Datenbanken laden</button>'
|
||||
. '</form>';
|
||||
|
||||
if ($selectMsg) $body .= '<div class="ok">' . h($selectMsg) . '</div><br>';
|
||||
if ($selectError) $body .= '<div class="err">' . h($selectError) . '</div><br>';
|
||||
if ($error) $body .= '<div class="err">' . h($error) . '</div><br>';
|
||||
$body .= '<hr>';
|
||||
|
||||
// Step 1: Probe
|
||||
$body .= "<h3>1) Verbindung testen & Datenbanken laden</h3>";
|
||||
$body .= "<form method=\"post\">";
|
||||
$body .= "<input type=\"hidden\" name=\"action\" value=\"probe\">";
|
||||
$body .= "<div class=\"grid\">";
|
||||
$body .= "<div>";
|
||||
$body .= "<label>Host<br><input name=\"host\" value=\"" . h($prefHost) . "\"></label><br><br>";
|
||||
$body .= "<label>Port<br><input name=\"port\" type=\"number\" value=\"" . h((string)$prefPort) . "\"></label><br><br>";
|
||||
$body .= "<label>Benutzer<br><input name=\"user\" value=\"" . h($prefUser) . "\"></label><br><br>";
|
||||
$body .= "<label>Passwort<br><input name=\"pass\" type=\"password\" value=\"" . h($prefPass) . "\"></label><br><br>";
|
||||
$body .= "<button class=\"btn secondary\" type=\"submit\">Datenbanken laden</button>";
|
||||
$body .= "</div>";
|
||||
|
||||
$body .= "<div>";
|
||||
$body .= "<h3>Hinweise</h3>";
|
||||
$body .= "<ul class=\"muted\">";
|
||||
$body .= "<li>Der Zugriff ist durch den App-Login geschützt.</li>";
|
||||
$body .= "<li>Für produktive Nutzung zusätzlich mit IP-Allowlist kombinieren.</li>";
|
||||
$body .= "</ul>";
|
||||
$body .= "</div>";
|
||||
$body .= "</div>";
|
||||
$body .= "</form>";
|
||||
|
||||
// Step 2: Login
|
||||
$body .= "<hr style=\"border:0;border-top:1px solid rgba(255,255,255,.10);margin:16px 0\">";
|
||||
$body .= "<h3>2) Login in Datenbank</h3>";
|
||||
|
||||
$body .= "<form method=\"post\">";
|
||||
$body .= "<input type=\"hidden\" name=\"action\" value=\"login\">";
|
||||
$body .= "<div class=\"grid\">";
|
||||
$body .= "<div>";
|
||||
$body .= "<label>Host<br><input name=\"host\" value=\"" . h($prefHost) . "\"></label><br><br>";
|
||||
$body .= "<label>Port<br><input name=\"port\" type=\"number\" value=\"" . h((string)$prefPort) . "\"></label><br><br>";
|
||||
$body .= "<label>Benutzer<br><input name=\"user\" value=\"" . h($prefUser) . "\"></label><br><br>";
|
||||
$body .= "<label>Passwort<br><input name=\"pass\" type=\"password\" value=\"" . h($prefPass) . "\"></label><br><br>";
|
||||
// Step 2
|
||||
$body .= '<h2>2 · Datenbank auswählen & einloggen</h2>';
|
||||
$body .= '<form method="post">'
|
||||
. '<input type="hidden" name="action" value="login">'
|
||||
. '<input type="hidden" name="host" value="' . h($prefHost) . '">'
|
||||
. '<input type="hidden" name="port" value="' . h((string)$prefPort) . '">'
|
||||
. '<input type="hidden" name="user" value="' . h($prefUser) . '">'
|
||||
. '<input type="hidden" name="pass" value="' . h($prefPass) . '">'
|
||||
. '<div class="field"><label>Datenbank</label>';
|
||||
|
||||
if (!empty($dbList)) {
|
||||
$body .= "<label>Datenbank<br><select name=\"db\" style=\"width:100%;box-sizing:border-box;background:rgba(0,0,0,.35);color:#e5e7eb;border:1px solid rgba(255,255,255,.14);border-radius:10px;padding:10px\">";
|
||||
$selectedDb = (string)$defaults['db'];
|
||||
$body .= '<select name="db">';
|
||||
$sel = (string)$defaults['db'];
|
||||
foreach ($dbList as $dbName) {
|
||||
$dbName = (string)$dbName;
|
||||
$sel = ($dbName === $selectedDb) ? ' selected' : '';
|
||||
$body .= '<option value="' . h($dbName) . '"' . $sel . '>' . h($dbName) . '</option>';
|
||||
$body .= '<option value="' . h($dbName) . '"' . ($dbName === $sel ? ' selected' : '') . '>' . h($dbName) . '</option>';
|
||||
}
|
||||
$body .= "</select></label><br><br>";
|
||||
$body .= '</select>';
|
||||
} else {
|
||||
$body .= "<label>Datenbank<br><input name=\"db\" value=\"" . h((string)$defaults['db']) . "\" placeholder=\"z.B. mydb\"></label><br><br>";
|
||||
$body .= "<div class=\"muted\" style=\"margin-top:-8px;margin-bottom:12px\">Tipp: Erst oben \"Datenbanken laden\" klicken für Vorschläge.</div>";
|
||||
$body .= '<input name="db" value="' . h((string)$defaults['db']) . '" placeholder="Datenbank-Name">';
|
||||
}
|
||||
|
||||
$body .= "<button class=\"btn\" type=\"submit\">Login</button>";
|
||||
$body .= "</div>";
|
||||
$body .= "</div>";
|
||||
$body .= "</form>";
|
||||
$body .= '</div>'
|
||||
. '<button class="btn" type="submit" style="width:100%">Einloggen</button>'
|
||||
. '</form>';
|
||||
|
||||
$body .= "</div>";
|
||||
$body .= '</div>';
|
||||
|
||||
admin_layout('DB-Verwaltung', $body);
|
||||
admin_layout('DB-Verwaltung', $body, 'Datenbankverbindung');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Logged-in area
|
||||
// ── DB-VERWALTUNG (eingeloggt) ────────────────────────────────────────────
|
||||
try {
|
||||
$pdo = admin_pdo();
|
||||
|
||||
$table = (string)($_GET['t'] ?? '');
|
||||
$page = max(1, (int)($_GET['p'] ?? 1));
|
||||
$limit = 50;
|
||||
$pdo = admin_pdo();
|
||||
$table = (string)($_GET['t'] ?? '');
|
||||
$page = max(1, (int)($_GET['p'] ?? 1));
|
||||
$limit = 50;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$msg = null;
|
||||
$msg = null;
|
||||
$queryResultHtml = '';
|
||||
|
||||
// SQL Query ausführen
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'query') {
|
||||
$sql = trim((string)($_POST['sql'] ?? ''));
|
||||
if ($sql !== '') {
|
||||
// Allow multiple statements? No. Keep minimal & safer.
|
||||
if (preg_match('/;\s*\S/', $sql)) {
|
||||
$msg = ['type' => 'err', 'text' => 'Bitte nur ein Statement ohne zusätzliche Semikolons ausführen.'];
|
||||
$msg = ['ok' => false, 'text' => 'Nur ein Statement ausführen (kein zweites Semikolon).'];
|
||||
} else {
|
||||
try {
|
||||
$stmt = $pdo->query($sql);
|
||||
if ($stmt instanceof PDOStatement) {
|
||||
$rows = $stmt->fetchAll();
|
||||
$queryResultHtml .= '<h3>Ergebnis</h3>';
|
||||
$queryResultHtml .= admin_render_table($rows);
|
||||
$msg = ['type' => 'ok', 'text' => 'Query ausgeführt.'];
|
||||
$queryResultHtml = '<h3 style="margin-top:1.2rem">Ergebnis</h3>' . admin_render_table($rows);
|
||||
$msg = ['ok' => true, 'text' => 'Query ausgeführt (' . count($rows) . ' Zeilen).'];
|
||||
} else {
|
||||
$msg = ['type' => 'ok', 'text' => 'Statement ausgeführt.'];
|
||||
$msg = ['ok' => true, 'text' => 'Statement ausgeführt.'];
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$msg = ['type' => 'err', 'text' => 'Fehler: ' . $e->getMessage()];
|
||||
$msg = ['ok' => false, 'text' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build left nav tables
|
||||
$tables = $pdo->query('SHOW TABLES')->fetchAll(PDO::FETCH_NUM);
|
||||
|
||||
$body = '<div class="top">'
|
||||
. '<div><h1>DB-Verwaltung</h1><div class="muted">eingeloggt</div></div>'
|
||||
. '<div style="display:flex;gap:10px">'
|
||||
. '<a class="btn secondary" href="/adminer?auth=logout">App-Logout</a>'
|
||||
. '<a class="btn secondary" href="/adminer?a=logout">DB-Logout</a>'
|
||||
// ── TOP BAR ──────────────────────────────────────────────────────────
|
||||
$dbName = (string)($_SESSION['db_admin']['db'] ?? '');
|
||||
$uname = (string)($_SESSION['adminer_app']['username'] ?? '');
|
||||
|
||||
$body = '<div class="top-bar">'
|
||||
. '<div class="top-bar-left">'
|
||||
. '<span style="font-weight:700;font-size:1rem">' . h($dbName) . '</span>'
|
||||
. ($uname ? '<span class="pill">' . h($uname) . '</span>' : '')
|
||||
. '</div>'
|
||||
. '<div class="top-bar-actions">'
|
||||
. '<a class="btn btn-ghost btn-sm" href="/adminer?a=logout">DB-Logout</a>'
|
||||
. '<a class="btn btn-ghost btn-sm" href="/adminer?auth=logout">Account-Logout</a>'
|
||||
. '</div>'
|
||||
. '</div>';
|
||||
|
||||
$body .= '<div class="grid">';
|
||||
|
||||
$body .= '<div class="card">'
|
||||
. '<h3>Tabellen</h3>'
|
||||
. '<div class="muted" style="margin-bottom:8px">Klick zum Anzeigen</div>';
|
||||
// ── GRID: TABELLENLISTE + CONTENT ─────────────────────────────────────
|
||||
$body .= '<div class="admin-grid">';
|
||||
|
||||
// Linke Spalte: Tabellenliste
|
||||
$body .= '<div class="card">';
|
||||
$body .= '<h2>Tabellen</h2>';
|
||||
if (empty($tables)) {
|
||||
$body .= '<div class="muted">Keine Tabellen gefunden.</div>';
|
||||
$body .= '<p class="muted">Keine Tabellen gefunden.</p>';
|
||||
} else {
|
||||
$body .= '<ul class="list">';
|
||||
$body .= '<ul class="nav-list">';
|
||||
foreach ($tables as $row) {
|
||||
$tname = (string)$row[0];
|
||||
$active = ($tname === $table) ? 'active' : '';
|
||||
$body .= '<li><a class="' . $active . '" href="/adminer?t=' . rawurlencode($tname) . '">' . h($tname) . '</a></li>';
|
||||
$tn = (string)$row[0];
|
||||
$cls = ($tn === $table) ? 'active' : '';
|
||||
$body .= '<li><a class="' . $cls . '" href="/adminer?t=' . rawurlencode($tn) . '">' . h($tn) . '</a></li>';
|
||||
}
|
||||
$body .= '</ul>';
|
||||
}
|
||||
$body .= '</div>';
|
||||
|
||||
$body .= '<div class="card">';
|
||||
// Rechte Spalte: Browse + Query
|
||||
$body .= '<div>';
|
||||
|
||||
// Notices
|
||||
if ($msg) {
|
||||
$cls = $msg['type'] === 'ok' ? 'notice ok' : 'notice err';
|
||||
$body .= '<div class="' . $cls . '">' . h($msg['text']) . '</div><br>';
|
||||
$cls = $msg['ok'] ? 'notice-ok' : 'notice-err';
|
||||
$body .= '<div class="notice ' . $cls . '">' . h($msg['text']) . '</div>';
|
||||
}
|
||||
|
||||
// Browse table
|
||||
// Browse
|
||||
if ($table !== '') {
|
||||
// naive identifier quoting for MySQL
|
||||
if (!preg_match('/^[A-Za-z0-9_]+$/', $table)) {
|
||||
$body .= '<div class="err">Ungültiger Tabellenname.</div>';
|
||||
$body .= '<div class="notice notice-err">Ungültiger Tabellenname.</div>';
|
||||
} else {
|
||||
$body .= '<h3>Tabelle: <code>' . h($table) . '</code></h3>';
|
||||
$stmt = $pdo->query('SELECT * FROM `' . $table . '` LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset);
|
||||
$rows = $stmt->fetchAll();
|
||||
$body .= admin_render_table($rows);
|
||||
|
||||
$body .= '<div style="margin-top:10px;display:flex;gap:10px">';
|
||||
if ($page > 1) {
|
||||
$body .= '<a class="btn secondary" href="/adminer?t=' . rawurlencode($table) . '&p=' . ($page - 1) . '">← Zurück</a>';
|
||||
}
|
||||
$body .= '<a class="btn secondary" href="/adminer?t=' . rawurlencode($table) . '&p=' . ($page + 1) . '">Weiter →</a>';
|
||||
$body .= '<div class="card" style="margin-bottom:16px">';
|
||||
$body .= '<h2>' . h($table) . '</h2>';
|
||||
$body .= admin_render_table($rows);
|
||||
$body .= '<div class="pagination">';
|
||||
if ($page > 1) $body .= '<a class="btn btn-ghost btn-sm" href="/adminer?t=' . rawurlencode($table) . '&p=' . ($page - 1) . '">← Zurück</a>';
|
||||
if (count($rows) === $limit) $body .= '<a class="btn btn-ghost btn-sm" href="/adminer?t=' . rawurlencode($table) . '&p=' . ($page + 1) . '">Weiter →</a>';
|
||||
if ($page > 1 || count($rows) === $limit) $body .= '<span class="muted">Seite ' . $page . '</span>';
|
||||
$body .= '</div>';
|
||||
$body .= '</div>';
|
||||
}
|
||||
|
||||
$body .= '<hr style="border:0;border-top:1px solid rgba(255,255,255,.10);margin:16px 0">';
|
||||
}
|
||||
|
||||
// Query box
|
||||
$body .= '<h3>SQL Query</h3>';
|
||||
// SQL Query Box
|
||||
$body .= '<div class="card">';
|
||||
$body .= '<h2>SQL Query</h2>';
|
||||
$body .= '<form method="post">'
|
||||
. '<input type="hidden" name="action" value="query">'
|
||||
. '<textarea name="sql" rows="6" spellcheck="false" placeholder="SELECT * FROM ..."></textarea>'
|
||||
. '<div style="margin-top:10px"><button class="btn" type="submit">Ausführen</button></div>'
|
||||
. '<div class="field"><textarea name="sql" rows="6" placeholder="SELECT * FROM tabelle LIMIT 10"></textarea></div>'
|
||||
. '<button class="btn btn-sm" type="submit">Ausführen</button>'
|
||||
. '</form>';
|
||||
|
||||
$body .= $queryResultHtml;
|
||||
$body .= '</div>';
|
||||
|
||||
$body .= '</div>'; // card
|
||||
$body .= '</div>'; // grid
|
||||
$body .= '</div>'; // right col
|
||||
$body .= '</div>'; // admin-grid
|
||||
|
||||
admin_layout('DB-Verwaltung', $body, h($dbName));
|
||||
|
||||
admin_layout('DB-Verwaltung', $body);
|
||||
} catch (Throwable $e) {
|
||||
// Session invalid, force re-login
|
||||
admin_logout();
|
||||
admin_layout('DB-Verwaltung', '<div class="top"><h1>DB-Verwaltung</h1></div><div class="card"><div class="err">' . h($e->getMessage()) . '</div><br><a class="btn" href="/adminer">Zum Login</a></div>');
|
||||
admin_layout('DB-Verwaltung',
|
||||
'<div class="notice notice-err">' . h($e->getMessage()) . '</div>'
|
||||
. '<p style="margin-top:1rem;text-align:center"><a class="btn btn-ghost btn-sm" href="/adminer">Zurück zum Login</a></p>',
|
||||
'Fehler'
|
||||
);
|
||||
}
|
||||
|
||||
function admin_render_table(array $rows): string
|
||||
{
|
||||
if (empty($rows)) {
|
||||
return '<div class="muted">(keine Zeilen)</div>';
|
||||
}
|
||||
if (empty($rows)) return '<p class="muted">(keine Zeilen)</p>';
|
||||
|
||||
$cols = array_keys((array)$rows[0]);
|
||||
$html = '<table class="table"><thead><tr>';
|
||||
foreach ($cols as $c) {
|
||||
$html .= '<th>' . h((string)$c) . '</th>';
|
||||
}
|
||||
$html = '<div style="overflow-x:auto"><table class="db-table"><thead><tr>';
|
||||
foreach ($cols as $c) $html .= '<th>' . h((string)$c) . '</th>';
|
||||
$html .= '</tr></thead><tbody>';
|
||||
|
||||
foreach ($rows as $r) {
|
||||
$html .= '<tr>';
|
||||
foreach ($cols as $c) {
|
||||
$v = $r[$c] ?? null;
|
||||
if ($v === null) {
|
||||
$cell = '<span class="muted">NULL</span>';
|
||||
} else {
|
||||
$s = (string)$v;
|
||||
$cell = strlen($s) > 500 ? h(substr($s, 0, 500)) . '…' : h($s);
|
||||
}
|
||||
$cell = $v === null
|
||||
? '<span class="null-val">NULL</span>'
|
||||
: (strlen((string)$v) > 300 ? h(substr((string)$v, 0, 300)) . '…' : h((string)$v));
|
||||
$html .= '<td>' . $cell . '</td>';
|
||||
}
|
||||
$html .= '</tr>';
|
||||
}
|
||||
|
||||
$html .= '</tbody></table>';
|
||||
$html .= '</tbody></table></div>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
@ -6,16 +6,72 @@ function h($s)
|
||||
return htmlspecialchars((string)$s, ENT_QUOTES);
|
||||
}
|
||||
|
||||
function admin_layout($title, $bodyHtml)
|
||||
function admin_layout($title, $bodyHtml, $subtitle = 'Datenbankverwaltung')
|
||||
{
|
||||
echo "<!doctype html>\n";
|
||||
echo "<html lang=\"de\">\n<head>\n";
|
||||
echo "<meta charset=\"utf-8\">\n";
|
||||
echo "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n";
|
||||
echo '<title>' . h($title) . "</title>\n";
|
||||
echo "<link rel=\"stylesheet\" href=\"/adminer/adminer.css\">\n";
|
||||
echo "</head>\n<body>\n";
|
||||
echo "<div class=\"wrap\">\n";
|
||||
echo $bodyHtml;
|
||||
echo "</div>\n</body>\n</html>";
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= h($title) ?> – Fabian Schieder</title>
|
||||
<link rel="stylesheet" href="/adminer/adminer.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<canvas id="particle-canvas"></canvas>
|
||||
<div class="orb orb-1"></div>
|
||||
<div class="orb orb-2"></div>
|
||||
<div class="orb orb-3"></div>
|
||||
<div class="orb orb-4"></div>
|
||||
<div class="background-blur"></div>
|
||||
|
||||
<div class="wrap">
|
||||
|
||||
<div class="admin-header">
|
||||
<div class="admin-avatar">FS</div>
|
||||
<div class="admin-title"><?= h($title) ?></div>
|
||||
<div class="admin-subtitle"><?= h($subtitle) ?></div>
|
||||
</div>
|
||||
|
||||
<?= $bodyHtml ?>
|
||||
|
||||
<footer style="margin-top:3rem;padding-top:1.5rem;color:var(--text-muted);font-size:.8rem;text-align:center;border-top:1px solid var(--border)">
|
||||
© <?= date('Y') ?> Fabian Schieder —
|
||||
<a href="/" style="color:var(--text-muted)">← Zurück zur Startseite</a>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ── Particles ──────────────────────────────────────────────────────────
|
||||
(function(){
|
||||
const canvas=document.getElementById('particle-canvas');
|
||||
if(!canvas)return;
|
||||
const ctx=canvas.getContext('2d');
|
||||
const COLORS=['#6366f1','#0ea5e9','#a855f7','#ec4899','#22d3ee'];
|
||||
let W,H,particles=[];
|
||||
function resize(){W=canvas.width=window.innerWidth;H=canvas.height=window.innerHeight;}
|
||||
function rand(a,b){return Math.random()*(b-a)+a;}
|
||||
function mkP(){return{x:rand(0,W),y:rand(0,H),r:rand(.8,2.2),dx:rand(-.25,.25),dy:rand(-.35,-.08),alpha:rand(.2,.8),fade:rand(.002,.006),color:COLORS[Math.floor(Math.random()*COLORS.length)]};}
|
||||
function init(){resize();particles=Array.from({length:80},mkP);}
|
||||
function draw(){
|
||||
ctx.clearRect(0,0,W,H);
|
||||
for(let p of particles){
|
||||
ctx.beginPath();ctx.arc(p.x,p.y,p.r,0,Math.PI*2);
|
||||
ctx.fillStyle=p.color;ctx.globalAlpha=p.alpha;ctx.fill();
|
||||
p.x+=p.dx;p.y+=p.dy;p.alpha-=p.fade;
|
||||
if(p.alpha<=0||p.y<-5)Object.assign(p,mkP(),{y:H+5,alpha:rand(.2,.7)});
|
||||
}
|
||||
ctx.globalAlpha=1;
|
||||
requestAnimationFrame(draw);
|
||||
}
|
||||
window.addEventListener('resize',resize);
|
||||
init();draw();
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user