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) */
|
/* ===== ADMINER – gleicher Look wie style.css ===== */
|
||||||
|
|
||||||
|
/* ── RESET & BASE ─────────────────────────────── */
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg:#0b1020;
|
--bg: #0f0f13;
|
||||||
--text:#e5e7eb;
|
--surface: #1a1a24;
|
||||||
--muted:#94a3b8;
|
--surface-hover: #22222f;
|
||||||
--card:rgba(255,255,255,.06);
|
--border: rgba(255,255,255,0.07);
|
||||||
--cardBorder:rgba(255,255,255,.10);
|
--text: #e8e8f0;
|
||||||
|
--text-muted: #8888a8;
|
||||||
|
--radius: 16px;
|
||||||
|
--transition: 0.25s ease;
|
||||||
--accent: #6366f1;
|
--accent: #6366f1;
|
||||||
--accent2:#22d3ee;
|
|
||||||
--danger:#ef4444;
|
|
||||||
--success:#22c55e;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
*{box-sizing:border-box}
|
html { scroll-behavior: smooth; overflow-x: hidden; }
|
||||||
html,body{height:100%}
|
|
||||||
body {
|
body {
|
||||||
margin:0;
|
background-color: var(--bg);
|
||||||
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);
|
color: var(--text);
|
||||||
}
|
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
.wrap{max-width:1100px;margin:34px auto;padding:0 16px}
|
|
||||||
|
|
||||||
.top{
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content:space-between;
|
padding: 2rem 1rem 4rem;
|
||||||
gap:12px;
|
position: relative;
|
||||||
margin-bottom:16px;
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1{font-size:28px;letter-spacing:.2px;margin:0}
|
/* ── BACKGROUND ───────────────────────────────── */
|
||||||
|
@keyframes bgShift {
|
||||||
a{color:#93c5fd;text-decoration:none}
|
0% { opacity:1; transform:scale(1) translate(0,0); }
|
||||||
a:hover{text-decoration:underline}
|
33% { opacity:.85; transform:scale(1.15) translate(2%,3%); }
|
||||||
|
66% { opacity:.9; transform:scale(1.08) translate(-3%,-2%); }
|
||||||
.pill{
|
100% { opacity:1; transform:scale(1) translate(0,0); }
|
||||||
font-size:12px;
|
|
||||||
background:rgba(255,255,255,.10);
|
|
||||||
border:1px solid rgba(255,255,255,.14);
|
|
||||||
padding:5px 10px;
|
|
||||||
border-radius:999px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card{
|
.background-blur {
|
||||||
background:var(--card);
|
position:fixed; inset:-25%; z-index:-1; pointer-events:none;
|
||||||
border:1px solid var(--cardBorder);
|
background:
|
||||||
border-radius:16px;
|
radial-gradient(ellipse 60% 40% at 20% 10%, rgba(99,102,241,.18), transparent),
|
||||||
padding:16px;
|
radial-gradient(ellipse 50% 40% at 80% 80%, rgba(14,165,233,.13), transparent),
|
||||||
backdrop-filter: blur(14px);
|
radial-gradient(ellipse 40% 30% at 60% 40%, rgba(168,85,247,.08), transparent);
|
||||||
box-shadow:0 18px 40px rgba(0,0,0,.35);
|
animation: bgShift 12s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid{display:grid;grid-template-columns:320px 1fr;gap:16px}
|
#particle-canvas {
|
||||||
@media (max-width: 900px){.grid{grid-template-columns:1fr}}
|
position:fixed; inset:0; z-index:-1; pointer-events:none; opacity:.55;
|
||||||
|
}
|
||||||
|
|
||||||
.muted{color:var(--muted)}
|
/* ── 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);
|
||||||
|
}
|
||||||
|
.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); }
|
||||||
|
}
|
||||||
|
|
||||||
label{display:block;color:#cbd5e1;font-weight:600}
|
/* 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%);
|
||||||
|
}
|
||||||
|
|
||||||
input,textarea,select{
|
/* ── LAYOUT ───────────────────────────────────── */
|
||||||
|
.wrap {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding:10px 12px;
|
max-width: 900px;
|
||||||
margin-top:6px;
|
}
|
||||||
|
|
||||||
|
/* ── 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;
|
border-radius:12px;
|
||||||
background:rgba(0,0,0,.35);
|
background:rgba(255,255,255,.04);
|
||||||
border:1px solid rgba(255,255,255,.14);
|
border:1px solid var(--border);
|
||||||
color:var(--text);
|
color:var(--text);
|
||||||
|
font-family:inherit; font-size:.95rem;
|
||||||
outline:none;
|
outline:none;
|
||||||
|
transition:border-color var(--transition), box-shadow var(--transition);
|
||||||
}
|
}
|
||||||
input:focus,textarea:focus,select:focus{border-color:rgba(99,102,241,.75);box-shadow:0 0 0 3px rgba(99,102,241,.20)}
|
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; }
|
||||||
|
|
||||||
hr{border:0;border-top:1px solid rgba(255,255,255,.10);margin:16px 0}
|
.field { margin-bottom:1rem; }
|
||||||
|
|
||||||
|
/* ── BUTTONS ──────────────────────────────────── */
|
||||||
.btn {
|
.btn {
|
||||||
display:inline-flex;
|
display:inline-flex; align-items:center; justify-content:center; gap:6px;
|
||||||
align-items:center;
|
padding:10px 18px; border-radius:var(--radius);
|
||||||
justify-content:center;
|
border:1px solid rgba(255,255,255,.12);
|
||||||
gap:8px;
|
cursor:pointer; font-size:.9rem; font-weight:600; letter-spacing:.2px;
|
||||||
padding:10px 12px;
|
background:linear-gradient(135deg,rgba(99,102,241,.9),rgba(14,165,233,.65));
|
||||||
border-radius:12px;
|
color:#fff;
|
||||||
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;
|
text-decoration:none;
|
||||||
transition:transform .12s ease, filter .12s ease;
|
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.06)}
|
.btn:hover { filter:brightness(1.1); transform:translateY(-1px); text-decoration:none; }
|
||||||
.btn:active{transform:translateY(1px)}
|
.btn:active { transform:translateY(1px); }
|
||||||
.btn.secondary{
|
|
||||||
background:rgba(255,255,255,.08);
|
.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{padding:10px 12px;border-radius:12px;border:1px solid rgba(255,255,255,.14)}
|
/* ── NOTICE / ALERTS ──────────────────────────── */
|
||||||
.notice.ok{background:rgba(34,197,94,.12);border-color:rgba(34,197,94,.25);color:#bbf7d0}
|
.notice {
|
||||||
.notice.err{background:rgba(239,68,68,.10);border-color:rgba(239,68,68,.25);color:#fecaca}
|
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; }
|
||||||
|
|
||||||
.table{width:100%;border-collapse:collapse;overflow:hidden;border-radius:14px}
|
/* ── NAV LIST (Tabellenliste) ─────────────────── */
|
||||||
.table th,.table td{padding:9px 10px;border-bottom:1px solid rgba(255,255,255,.10);vertical-align:top}
|
.nav-list { list-style:none; }
|
||||||
.table th{color:#cbd5e1;text-align:left;font-weight:700;background:rgba(255,255,255,.04)}
|
.nav-list li { margin:3px 0; }
|
||||||
.code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace}
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
.list{list-style:none;margin:0;padding:0}
|
/* ── TABLE ────────────────────────────────────── */
|
||||||
.list li{margin:6px 0}
|
.db-table {
|
||||||
.list a{display:block;padding:8px 10px;border-radius:12px}
|
width:100%; border-collapse:collapse; font-size:.85rem;
|
||||||
.list a:hover{background:rgba(255,255,255,.06)}
|
overflow:hidden;
|
||||||
.list a.active{background:rgba(99,102,241,.16);border:1px solid rgba(99,102,241,.25)}
|
}
|
||||||
|
.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,24 +10,25 @@ adminer_app_session_start();
|
|||||||
try {
|
try {
|
||||||
adminer_app_bootstrap();
|
adminer_app_bootstrap();
|
||||||
} catch (Throwable $e) {
|
} 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;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$appPage = (string)($_GET['page'] ?? 'login'); // login|register
|
// App-Logout
|
||||||
$appAction = (string)($_GET['auth'] ?? '');
|
if ((string)($_GET['auth'] ?? '') === 'logout') {
|
||||||
if ($appAction === 'logout') {
|
|
||||||
adminer_app_logout();
|
adminer_app_logout();
|
||||||
header('Location: /adminer', true, 302);
|
header('Location: /adminer', true, 302);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle app login
|
$appPage = (string)($_GET['page'] ?? 'login');
|
||||||
$appError = null;
|
$appError = null;
|
||||||
|
$appRegError = null;
|
||||||
|
$appRegOk = null;
|
||||||
|
|
||||||
|
// Login POST
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'app_login') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'app_login') {
|
||||||
$u = (string)($_POST['username'] ?? '');
|
$res = adminer_app_try_login((string)($_POST['username'] ?? ''), (string)($_POST['password'] ?? ''));
|
||||||
$p = (string)($_POST['password'] ?? '');
|
|
||||||
$res = adminer_app_try_login($u, $p);
|
|
||||||
if (!empty($res['ok'])) {
|
if (!empty($res['ok'])) {
|
||||||
header('Location: /adminer', true, 302);
|
header('Location: /adminer', true, 302);
|
||||||
exit;
|
exit;
|
||||||
@ -36,23 +37,20 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') ==
|
|||||||
$appPage = 'login';
|
$appPage = 'login';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle registration
|
// Register POST
|
||||||
$appRegError = null;
|
|
||||||
$appRegOk = null;
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'app_register') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'app_register') {
|
||||||
$u = (string)($_POST['username'] ?? '');
|
$res = adminer_app_try_register(
|
||||||
$p1 = (string)($_POST['password'] ?? '');
|
(string)($_POST['username'] ?? ''),
|
||||||
$p2 = (string)($_POST['password2'] ?? '');
|
(string)($_POST['password'] ?? ''),
|
||||||
|
(string)($_POST['password2'] ?? '')
|
||||||
$res = adminer_app_try_register($u, $p1, $p2);
|
);
|
||||||
if (!empty($res['ok'])) {
|
if (!empty($res['ok'])) {
|
||||||
// Auto-login after register
|
$lr = adminer_app_try_login((string)($_POST['username'] ?? ''), (string)($_POST['password'] ?? ''));
|
||||||
$loginRes = adminer_app_try_login($u, $p1);
|
if (!empty($lr['ok'])) {
|
||||||
if (!empty($loginRes['ok'])) {
|
|
||||||
header('Location: /adminer', true, 302);
|
header('Location: /adminer', true, 302);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
$appRegOk = 'Registrierung erfolgreich. Bitte einloggen.';
|
$appRegOk = 'Konto erstellt! Bitte einloggen.';
|
||||||
$appPage = 'login';
|
$appPage = 'login';
|
||||||
} else {
|
} else {
|
||||||
$appRegError = (string)($res['error'] ?? 'Registrierung fehlgeschlagen.');
|
$appRegError = (string)($res['error'] ?? 'Registrierung fehlgeschlagen.');
|
||||||
@ -60,81 +58,69 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') ==
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── LOGIN / REGISTER SEITE ────────────────────────────────────────────────
|
||||||
if (!adminer_app_is_logged_in()) {
|
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="login-wrap">';
|
||||||
$body .= '<div class="card">';
|
|
||||||
|
|
||||||
// Tabs
|
// Tabs
|
||||||
$body .= '<div style="display:flex;gap:10px;margin-bottom:14px">'
|
$body .= '<div class="tabs">';
|
||||||
. '<a class="btn secondary" href="/adminer?page=login" style="text-decoration:none">Login</a>'
|
$body .= '<a class="tab ' . (!$isReg ? 'active' : '') . '" href="/adminer?page=login">Login</a>';
|
||||||
. ($canRegister ? '<a class="btn secondary" href="/adminer?page=register" style="text-decoration:none">Registrieren</a>' : '')
|
if ($canReg) $body .= '<a class="tab ' . ($isReg ? 'active' : '') . '" href="/adminer?page=register">Registrieren</a>';
|
||||||
. '</div>';
|
$body .= '</div>';
|
||||||
|
|
||||||
if ($appRegOk) {
|
$body .= '<div class="card">';
|
||||||
$body .= '<div class="notice ok">' . h($appRegOk) . '</div><br>';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($appPage === 'register') {
|
if ($appRegOk) $body .= '<div class="notice notice-ok">' . h($appRegOk) . '</div>';
|
||||||
if (!$canRegister) {
|
if ($appError) $body .= '<div class="notice notice-err">' . h($appError) . '</div>';
|
||||||
$body .= '<div class="notice err">Registrierung ist deaktiviert.</div>';
|
if ($appRegError)$body .= '<div class="notice notice-err">' . h($appRegError) . '</div>';
|
||||||
} else {
|
|
||||||
if ($appRegError) $body .= '<div class="notice err">' . h($appRegError) . '</div><br>';
|
|
||||||
|
|
||||||
$body .= '<h3>Registrieren</h3>';
|
if ($isReg && $canReg) {
|
||||||
|
// ── Registrierungsformular ──────────────────────────────────────
|
||||||
$body .= '<form method="post">'
|
$body .= '<form method="post">'
|
||||||
. '<input type="hidden" name="action" value="app_register">'
|
. '<input type="hidden" name="action" value="app_register">'
|
||||||
. '<label>Benutzername<br><input name="username" autocomplete="username"></label><br><br>'
|
. '<div class="field"><label>Benutzername</label><input name="username" autocomplete="username" placeholder="z.B. fabian"></div>'
|
||||||
. '<label>Passwort<br><input name="password" type="password" autocomplete="new-password"></label><br><br>'
|
. '<div class="field"><label>Passwort</label><input name="password" type="password" autocomplete="new-password" placeholder="mind. 8 Zeichen"></div>'
|
||||||
. '<label>Passwort wiederholen<br><input name="password2" type="password" autocomplete="new-password"></label><br><br>'
|
. '<div class="field"><label>Passwort wiederholen</label><input name="password2" type="password" autocomplete="new-password" placeholder="Wiederholung"></div>'
|
||||||
. '<button class="btn" type="submit">Account erstellen</button>'
|
. '<button class="btn" type="submit" style="width:100%">Konto erstellen</button>'
|
||||||
. '</form>';
|
. '</form>';
|
||||||
|
$body .= '<p class="muted" style="margin-top:.9rem;text-align:center">Bereits ein Konto? <a href="/adminer?page=login">Login</a></p>';
|
||||||
$body .= '<div class="muted" style="margin-top:12px">Dein Account wird in <code class="code">FabianWebsite.adminer_users</code> gespeichert.</div>';
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if ($appError) $body .= '<div class="notice err">' . h($appError) . '</div><br>';
|
// ── Login-Formular ──────────────────────────────────────────────
|
||||||
|
|
||||||
$body .= '<h3>Login</h3>';
|
|
||||||
$body .= '<form method="post">'
|
$body .= '<form method="post">'
|
||||||
. '<input type="hidden" name="action" value="app_login">'
|
. '<input type="hidden" name="action" value="app_login">'
|
||||||
. '<label>Benutzername<br><input name="username" autocomplete="username"></label><br><br>'
|
. '<div class="field"><label>Benutzername</label><input name="username" autocomplete="username" placeholder="Dein Benutzername"></div>'
|
||||||
. '<label>Passwort<br><input name="password" type="password" autocomplete="current-password"></label><br><br>'
|
. '<div class="field"><label>Passwort</label><input name="password" type="password" autocomplete="current-password" placeholder="Dein Passwort"></div>'
|
||||||
. '<button class="btn" type="submit">Anmelden</button>'
|
. '<button class="btn" type="submit" style="width:100%">Anmelden</button>'
|
||||||
. '</form>';
|
. '</form>';
|
||||||
|
if ($canReg) $body .= '<p class="muted" style="margin-top:.9rem;text-align:center">Noch kein Konto? <a href="/adminer?page=register">Registrieren</a></p>';
|
||||||
if ($canRegister) {
|
|
||||||
$body .= '<div class="muted" style="margin-top:12px">Noch kein Account? <a href="/adminer?page=register">Registrieren</a></div>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$body .= '</div>';
|
||||||
$body .= '</div>';
|
$body .= '</div>';
|
||||||
|
|
||||||
admin_layout('DB-Verwaltung', $body);
|
admin_layout('DB-Verwaltung', $body, $isReg ? 'Neues Konto erstellen' : 'Bitte einloggen');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── DB-VERBINDUNGS-LOGIN ──────────────────────────────────────────────────
|
||||||
require_once __DIR__ . '/auth.php';
|
require_once __DIR__ . '/auth.php';
|
||||||
|
|
||||||
admin_session_start();
|
admin_session_start();
|
||||||
|
|
||||||
$action = (string)($_GET['a'] ?? '');
|
if ((string)($_GET['a'] ?? '') === 'logout') {
|
||||||
if ($action === 'logout') {
|
|
||||||
admin_logout();
|
admin_logout();
|
||||||
header('Location: /adminer', true, 302);
|
header('Location: /adminer', true, 302);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── DB-Auswahl (Step 1) ───────────────────────────────────────────────────
|
if (!isset($_SESSION['db_admin_select'])) $_SESSION['db_admin_select'] = [];
|
||||||
// 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'] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$selectError = null;
|
$selectError = null;
|
||||||
$selectMsg = null;
|
$selectMsg = null;
|
||||||
|
|
||||||
|
// Probe: Datenbanken laden
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'probe') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'probe') {
|
||||||
$host = trim((string)($_POST['host'] ?? ''));
|
$host = trim((string)($_POST['host'] ?? ''));
|
||||||
$port = (int)($_POST['port'] ?? 3306);
|
$port = (int)($_POST['port'] ?? 3306);
|
||||||
@ -145,129 +131,99 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') ==
|
|||||||
$selectError = 'Bitte Host, Port und Benutzer angeben.';
|
$selectError = 'Bitte Host, Port und Benutzer angeben.';
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
$dsn = sprintf('mysql:host=%s;port=%d;charset=utf8mb4', $host, $port);
|
$pdo = new PDO(sprintf('mysql:host=%s;port=%d;charset=utf8mb4', $host, $port), $user, $pass, [
|
||||||
$pdo = new PDO($dsn, $user, $pass, [
|
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
]);
|
]);
|
||||||
$dbs = $pdo->query('SHOW DATABASES')->fetchAll(PDO::FETCH_COLUMN, 0);
|
$dbs = $pdo->query('SHOW DATABASES')->fetchAll(PDO::FETCH_COLUMN, 0);
|
||||||
|
$_SESSION['db_admin_select'] = compact('host', 'port', 'user', 'pass', 'dbs');
|
||||||
$_SESSION['db_admin_select'] = [
|
$selectMsg = 'Datenbanken geladen – bitte unten eine auswählen.';
|
||||||
'host' => $host,
|
|
||||||
'port' => $port,
|
|
||||||
'user' => $user,
|
|
||||||
'pass' => $pass,
|
|
||||||
'dbs' => $dbs,
|
|
||||||
];
|
|
||||||
$selectMsg = 'Datenbanken geladen.';
|
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$selectError = 'Konnte Datenbanken nicht laden: ' . $e->getMessage();
|
$selectError = 'Fehler: ' . $e->getMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login (Step 2)
|
// DB-Login
|
||||||
$error = null;
|
$dbError = null;
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'login') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'login') {
|
||||||
$host = trim((string)($_POST['host'] ?? ''));
|
$res = admin_try_login(
|
||||||
$port = (int)($_POST['port'] ?? 3306);
|
trim((string)($_POST['host'] ?? '')),
|
||||||
$user = trim((string)($_POST['user'] ?? ''));
|
(int)($_POST['port'] ?? 3306),
|
||||||
$pass = (string)($_POST['pass'] ?? '');
|
trim((string)($_POST['user'] ?? '')),
|
||||||
$db = trim((string)($_POST['db'] ?? ''));
|
(string)($_POST['pass'] ?? ''),
|
||||||
|
trim((string)($_POST['db'] ?? ''))
|
||||||
$res = admin_try_login($host, $port, $user, $pass, $db);
|
);
|
||||||
if ($res['ok']) {
|
if ($res['ok']) {
|
||||||
header('Location: /adminer', true, 302);
|
header('Location: /adminer', true, 302);
|
||||||
exit;
|
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'] : [];
|
$selectState = is_array($_SESSION['db_admin_select']) ? $_SESSION['db_admin_select'] : [];
|
||||||
|
|
||||||
// UI when not logged in
|
|
||||||
if (!admin_is_logged_in()) {
|
|
||||||
$prefHost = isset($selectState['host']) ? (string)$selectState['host'] : (string)$defaults['host'];
|
$prefHost = isset($selectState['host']) ? (string)$selectState['host'] : (string)$defaults['host'];
|
||||||
$prefPort = isset($selectState['port']) ? (int)$selectState['port'] : (int)$defaults['port'];
|
$prefPort = isset($selectState['port']) ? (int)$selectState['port'] : (int)$defaults['port'];
|
||||||
$prefUser = isset($selectState['user']) ? (string)$selectState['user'] : (string)$defaults['user'];
|
$prefUser = isset($selectState['user']) ? (string)$selectState['user'] : (string)$defaults['user'];
|
||||||
$prefPass = isset($selectState['pass']) ? (string)$selectState['pass'] : '';
|
$prefPass = isset($selectState['pass']) ? (string)$selectState['pass'] : '';
|
||||||
|
|
||||||
$dbList = isset($selectState['dbs']) && is_array($selectState['dbs']) ? $selectState['dbs'] : [];
|
$dbList = isset($selectState['dbs']) && is_array($selectState['dbs']) ? $selectState['dbs'] : [];
|
||||||
|
|
||||||
$body = "<div class=\"top\"><h1>DB-Verwaltung</h1><span class=\"pill\">Mini-Admin</span></div>";
|
if (!admin_is_logged_in()) {
|
||||||
$body .= "<div class=\"card\">";
|
$body = '<div class="card" style="max-width:520px;margin:0 auto">';
|
||||||
$body .= "<p class=\"muted\">Zuerst Verbindungsdaten prüfen (ohne DB), dann bekommst du eine Datenbank-Auswahl. Defaults kommen aus <code>.env</code>.</p>";
|
|
||||||
|
|
||||||
if ($selectMsg) $body .= '<div class="ok">' . h($selectMsg) . '</div><br>';
|
if ($selectMsg) $body .= '<div class="notice notice-ok">' . h($selectMsg) . '</div>';
|
||||||
if ($selectError) $body .= '<div class="err">' . h($selectError) . '</div><br>';
|
if ($selectError)$body .= '<div class="notice notice-err">' . h($selectError) . '</div>';
|
||||||
if ($error) $body .= '<div class="err">' . h($error) . '</div><br>';
|
if ($dbError) $body .= '<div class="notice notice-err">' . h($dbError) . '</div>';
|
||||||
|
|
||||||
// Step 1: Probe
|
// Step 1
|
||||||
$body .= "<h3>1) Verbindung testen & Datenbanken laden</h3>";
|
$body .= '<h2>1 · Server verbinden</h2>';
|
||||||
$body .= "<form method=\"post\">";
|
$body .= '<form method="post">'
|
||||||
$body .= "<input type=\"hidden\" name=\"action\" value=\"probe\">";
|
. '<input type="hidden" name="action" value="probe">'
|
||||||
$body .= "<div class=\"grid\">";
|
. '<div class="field"><label>Host</label><input name="host" value="' . h($prefHost) . '" placeholder="localhost"></div>'
|
||||||
$body .= "<div>";
|
. '<div class="field"><label>Port</label><input name="port" type="number" value="' . h((string)$prefPort) . '"></div>'
|
||||||
$body .= "<label>Host<br><input name=\"host\" value=\"" . h($prefHost) . "\"></label><br><br>";
|
. '<div class="field"><label>Benutzer</label><input name="user" value="' . h($prefUser) . '"></div>'
|
||||||
$body .= "<label>Port<br><input name=\"port\" type=\"number\" value=\"" . h((string)$prefPort) . "\"></label><br><br>";
|
. '<div class="field"><label>Passwort</label><input name="pass" type="password" value="' . h($prefPass) . '"></div>'
|
||||||
$body .= "<label>Benutzer<br><input name=\"user\" value=\"" . h($prefUser) . "\"></label><br><br>";
|
. '<button class="btn btn-ghost btn-sm" type="submit">Datenbanken laden</button>'
|
||||||
$body .= "<label>Passwort<br><input name=\"pass\" type=\"password\" value=\"" . h($prefPass) . "\"></label><br><br>";
|
. '</form>';
|
||||||
$body .= "<button class=\"btn secondary\" type=\"submit\">Datenbanken laden</button>";
|
|
||||||
$body .= "</div>";
|
|
||||||
|
|
||||||
$body .= "<div>";
|
$body .= '<hr>';
|
||||||
$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
|
// Step 2
|
||||||
$body .= "<hr style=\"border:0;border-top:1px solid rgba(255,255,255,.10);margin:16px 0\">";
|
$body .= '<h2>2 · Datenbank auswählen & einloggen</h2>';
|
||||||
$body .= "<h3>2) Login in Datenbank</h3>";
|
$body .= '<form method="post">'
|
||||||
|
. '<input type="hidden" name="action" value="login">'
|
||||||
$body .= "<form method=\"post\">";
|
. '<input type="hidden" name="host" value="' . h($prefHost) . '">'
|
||||||
$body .= "<input type=\"hidden\" name=\"action\" value=\"login\">";
|
. '<input type="hidden" name="port" value="' . h((string)$prefPort) . '">'
|
||||||
$body .= "<div class=\"grid\">";
|
. '<input type="hidden" name="user" value="' . h($prefUser) . '">'
|
||||||
$body .= "<div>";
|
. '<input type="hidden" name="pass" value="' . h($prefPass) . '">'
|
||||||
$body .= "<label>Host<br><input name=\"host\" value=\"" . h($prefHost) . "\"></label><br><br>";
|
. '<div class="field"><label>Datenbank</label>';
|
||||||
$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>";
|
|
||||||
|
|
||||||
if (!empty($dbList)) {
|
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\">";
|
$body .= '<select name="db">';
|
||||||
$selectedDb = (string)$defaults['db'];
|
$sel = (string)$defaults['db'];
|
||||||
foreach ($dbList as $dbName) {
|
foreach ($dbList as $dbName) {
|
||||||
$dbName = (string)$dbName;
|
$dbName = (string)$dbName;
|
||||||
$sel = ($dbName === $selectedDb) ? ' selected' : '';
|
$body .= '<option value="' . h($dbName) . '"' . ($dbName === $sel ? ' selected' : '') . '>' . h($dbName) . '</option>';
|
||||||
$body .= '<option value="' . h($dbName) . '"' . $sel . '>' . h($dbName) . '</option>';
|
|
||||||
}
|
}
|
||||||
$body .= "</select></label><br><br>";
|
$body .= '</select>';
|
||||||
} else {
|
} else {
|
||||||
$body .= "<label>Datenbank<br><input name=\"db\" value=\"" . h((string)$defaults['db']) . "\" placeholder=\"z.B. mydb\"></label><br><br>";
|
$body .= '<input name="db" value="' . h((string)$defaults['db']) . '" placeholder="Datenbank-Name">';
|
||||||
$body .= "<div class=\"muted\" style=\"margin-top:-8px;margin-bottom:12px\">Tipp: Erst oben \"Datenbanken laden\" klicken für Vorschläge.</div>";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$body .= "<button class=\"btn\" type=\"submit\">Login</button>";
|
$body .= '</div>'
|
||||||
$body .= "</div>";
|
. '<button class="btn" type="submit" style="width:100%">Einloggen</button>'
|
||||||
$body .= "</div>";
|
. '</form>';
|
||||||
$body .= "</form>";
|
|
||||||
|
|
||||||
$body .= "</div>";
|
$body .= '</div>';
|
||||||
|
|
||||||
admin_layout('DB-Verwaltung', $body);
|
admin_layout('DB-Verwaltung', $body, 'Datenbankverbindung');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logged-in area
|
// ── DB-VERWALTUNG (eingeloggt) ────────────────────────────────────────────
|
||||||
try {
|
try {
|
||||||
$pdo = admin_pdo();
|
$pdo = admin_pdo();
|
||||||
|
|
||||||
$table = (string)($_GET['t'] ?? '');
|
$table = (string)($_GET['t'] ?? '');
|
||||||
$page = max(1, (int)($_GET['p'] ?? 1));
|
$page = max(1, (int)($_GET['p'] ?? 1));
|
||||||
$limit = 50;
|
$limit = 50;
|
||||||
@ -276,138 +232,139 @@ try {
|
|||||||
$msg = null;
|
$msg = null;
|
||||||
$queryResultHtml = '';
|
$queryResultHtml = '';
|
||||||
|
|
||||||
|
// SQL Query ausführen
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'query') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'query') {
|
||||||
$sql = trim((string)($_POST['sql'] ?? ''));
|
$sql = trim((string)($_POST['sql'] ?? ''));
|
||||||
if ($sql !== '') {
|
if ($sql !== '') {
|
||||||
// Allow multiple statements? No. Keep minimal & safer.
|
|
||||||
if (preg_match('/;\s*\S/', $sql)) {
|
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 {
|
} else {
|
||||||
try {
|
try {
|
||||||
$stmt = $pdo->query($sql);
|
$stmt = $pdo->query($sql);
|
||||||
if ($stmt instanceof PDOStatement) {
|
if ($stmt instanceof PDOStatement) {
|
||||||
$rows = $stmt->fetchAll();
|
$rows = $stmt->fetchAll();
|
||||||
$queryResultHtml .= '<h3>Ergebnis</h3>';
|
$queryResultHtml = '<h3 style="margin-top:1.2rem">Ergebnis</h3>' . admin_render_table($rows);
|
||||||
$queryResultHtml .= admin_render_table($rows);
|
$msg = ['ok' => true, 'text' => 'Query ausgeführt (' . count($rows) . ' Zeilen).'];
|
||||||
$msg = ['type' => 'ok', 'text' => 'Query ausgeführt.'];
|
|
||||||
} else {
|
} else {
|
||||||
$msg = ['type' => 'ok', 'text' => 'Statement ausgeführt.'];
|
$msg = ['ok' => true, 'text' => 'Statement ausgeführt.'];
|
||||||
}
|
}
|
||||||
} catch (Throwable $e) {
|
} 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);
|
$tables = $pdo->query('SHOW TABLES')->fetchAll(PDO::FETCH_NUM);
|
||||||
|
|
||||||
$body = '<div class="top">'
|
// ── TOP BAR ──────────────────────────────────────────────────────────
|
||||||
. '<div><h1>DB-Verwaltung</h1><div class="muted">eingeloggt</div></div>'
|
$dbName = (string)($_SESSION['db_admin']['db'] ?? '');
|
||||||
. '<div style="display:flex;gap:10px">'
|
$uname = (string)($_SESSION['adminer_app']['username'] ?? '');
|
||||||
. '<a class="btn secondary" href="/adminer?auth=logout">App-Logout</a>'
|
|
||||||
. '<a class="btn secondary" href="/adminer?a=logout">DB-Logout</a>'
|
$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>'
|
||||||
. '</div>';
|
. '</div>';
|
||||||
|
|
||||||
$body .= '<div class="grid">';
|
// ── GRID: TABELLENLISTE + CONTENT ─────────────────────────────────────
|
||||||
|
$body .= '<div class="admin-grid">';
|
||||||
$body .= '<div class="card">'
|
|
||||||
. '<h3>Tabellen</h3>'
|
|
||||||
. '<div class="muted" style="margin-bottom:8px">Klick zum Anzeigen</div>';
|
|
||||||
|
|
||||||
|
// Linke Spalte: Tabellenliste
|
||||||
|
$body .= '<div class="card">';
|
||||||
|
$body .= '<h2>Tabellen</h2>';
|
||||||
if (empty($tables)) {
|
if (empty($tables)) {
|
||||||
$body .= '<div class="muted">Keine Tabellen gefunden.</div>';
|
$body .= '<p class="muted">Keine Tabellen gefunden.</p>';
|
||||||
} else {
|
} else {
|
||||||
$body .= '<ul class="list">';
|
$body .= '<ul class="nav-list">';
|
||||||
foreach ($tables as $row) {
|
foreach ($tables as $row) {
|
||||||
$tname = (string)$row[0];
|
$tn = (string)$row[0];
|
||||||
$active = ($tname === $table) ? 'active' : '';
|
$cls = ($tn === $table) ? 'active' : '';
|
||||||
$body .= '<li><a class="' . $active . '" href="/adminer?t=' . rawurlencode($tname) . '">' . h($tname) . '</a></li>';
|
$body .= '<li><a class="' . $cls . '" href="/adminer?t=' . rawurlencode($tn) . '">' . h($tn) . '</a></li>';
|
||||||
}
|
}
|
||||||
$body .= '</ul>';
|
$body .= '</ul>';
|
||||||
}
|
}
|
||||||
$body .= '</div>';
|
$body .= '</div>';
|
||||||
|
|
||||||
$body .= '<div class="card">';
|
// Rechte Spalte: Browse + Query
|
||||||
|
$body .= '<div>';
|
||||||
|
|
||||||
|
// Notices
|
||||||
if ($msg) {
|
if ($msg) {
|
||||||
$cls = $msg['type'] === 'ok' ? 'notice ok' : 'notice err';
|
$cls = $msg['ok'] ? 'notice-ok' : 'notice-err';
|
||||||
$body .= '<div class="' . $cls . '">' . h($msg['text']) . '</div><br>';
|
$body .= '<div class="notice ' . $cls . '">' . h($msg['text']) . '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Browse table
|
// Browse
|
||||||
if ($table !== '') {
|
if ($table !== '') {
|
||||||
// naive identifier quoting for MySQL
|
|
||||||
if (!preg_match('/^[A-Za-z0-9_]+$/', $table)) {
|
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 {
|
} else {
|
||||||
$body .= '<h3>Tabelle: <code>' . h($table) . '</code></h3>';
|
|
||||||
$stmt = $pdo->query('SELECT * FROM `' . $table . '` LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset);
|
$stmt = $pdo->query('SELECT * FROM `' . $table . '` LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset);
|
||||||
$rows = $stmt->fetchAll();
|
$rows = $stmt->fetchAll();
|
||||||
$body .= admin_render_table($rows);
|
|
||||||
|
|
||||||
$body .= '<div style="margin-top:10px;display:flex;gap:10px">';
|
$body .= '<div class="card" style="margin-bottom:16px">';
|
||||||
if ($page > 1) {
|
$body .= '<h2>' . h($table) . '</h2>';
|
||||||
$body .= '<a class="btn secondary" href="/adminer?t=' . rawurlencode($table) . '&p=' . ($page - 1) . '">← Zurück</a>';
|
$body .= admin_render_table($rows);
|
||||||
}
|
$body .= '<div class="pagination">';
|
||||||
$body .= '<a class="btn secondary" href="/adminer?t=' . rawurlencode($table) . '&p=' . ($page + 1) . '">Weiter →</a>';
|
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 .= '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
$body .= '<hr style="border:0;border-top:1px solid rgba(255,255,255,.10);margin:16px 0">';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query box
|
// SQL Query Box
|
||||||
$body .= '<h3>SQL Query</h3>';
|
$body .= '<div class="card">';
|
||||||
|
$body .= '<h2>SQL Query</h2>';
|
||||||
$body .= '<form method="post">'
|
$body .= '<form method="post">'
|
||||||
. '<input type="hidden" name="action" value="query">'
|
. '<input type="hidden" name="action" value="query">'
|
||||||
. '<textarea name="sql" rows="6" spellcheck="false" placeholder="SELECT * FROM ..."></textarea>'
|
. '<div class="field"><textarea name="sql" rows="6" placeholder="SELECT * FROM tabelle LIMIT 10"></textarea></div>'
|
||||||
. '<div style="margin-top:10px"><button class="btn" type="submit">Ausführen</button></div>'
|
. '<button class="btn btn-sm" type="submit">Ausführen</button>'
|
||||||
. '</form>';
|
. '</form>';
|
||||||
|
|
||||||
$body .= $queryResultHtml;
|
$body .= $queryResultHtml;
|
||||||
|
$body .= '</div>';
|
||||||
|
|
||||||
$body .= '</div>'; // card
|
$body .= '</div>'; // right col
|
||||||
$body .= '</div>'; // grid
|
$body .= '</div>'; // admin-grid
|
||||||
|
|
||||||
|
admin_layout('DB-Verwaltung', $body, h($dbName));
|
||||||
|
|
||||||
admin_layout('DB-Verwaltung', $body);
|
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
// Session invalid, force re-login
|
|
||||||
admin_logout();
|
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
|
function admin_render_table(array $rows): string
|
||||||
{
|
{
|
||||||
if (empty($rows)) {
|
if (empty($rows)) return '<p class="muted">(keine Zeilen)</p>';
|
||||||
return '<div class="muted">(keine Zeilen)</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
$cols = array_keys((array)$rows[0]);
|
$cols = array_keys((array)$rows[0]);
|
||||||
$html = '<table class="table"><thead><tr>';
|
$html = '<div style="overflow-x:auto"><table class="db-table"><thead><tr>';
|
||||||
foreach ($cols as $c) {
|
foreach ($cols as $c) $html .= '<th>' . h((string)$c) . '</th>';
|
||||||
$html .= '<th>' . h((string)$c) . '</th>';
|
|
||||||
}
|
|
||||||
$html .= '</tr></thead><tbody>';
|
$html .= '</tr></thead><tbody>';
|
||||||
|
|
||||||
foreach ($rows as $r) {
|
foreach ($rows as $r) {
|
||||||
$html .= '<tr>';
|
$html .= '<tr>';
|
||||||
foreach ($cols as $c) {
|
foreach ($cols as $c) {
|
||||||
$v = $r[$c] ?? null;
|
$v = $r[$c] ?? null;
|
||||||
if ($v === null) {
|
$cell = $v === null
|
||||||
$cell = '<span class="muted">NULL</span>';
|
? '<span class="null-val">NULL</span>'
|
||||||
} else {
|
: (strlen((string)$v) > 300 ? h(substr((string)$v, 0, 300)) . '…' : h((string)$v));
|
||||||
$s = (string)$v;
|
|
||||||
$cell = strlen($s) > 500 ? h(substr($s, 0, 500)) . '…' : h($s);
|
|
||||||
}
|
|
||||||
$html .= '<td>' . $cell . '</td>';
|
$html .= '<td>' . $cell . '</td>';
|
||||||
}
|
}
|
||||||
$html .= '</tr>';
|
$html .= '</tr>';
|
||||||
}
|
}
|
||||||
|
$html .= '</tbody></table></div>';
|
||||||
$html .= '</tbody></table>';
|
|
||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,16 +6,72 @@ function h($s)
|
|||||||
return htmlspecialchars((string)$s, ENT_QUOTES);
|
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";
|
<!DOCTYPE html>
|
||||||
echo "<meta charset=\"utf-8\">\n";
|
<html lang="de">
|
||||||
echo "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n";
|
<head>
|
||||||
echo '<title>' . h($title) . "</title>\n";
|
<meta charset="UTF-8">
|
||||||
echo "<link rel=\"stylesheet\" href=\"/adminer/adminer.css\">\n";
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
echo "</head>\n<body>\n";
|
<title><?= h($title) ?> – Fabian Schieder</title>
|
||||||
echo "<div class=\"wrap\">\n";
|
<link rel="stylesheet" href="/adminer/adminer.css">
|
||||||
echo $bodyHtml;
|
</head>
|
||||||
echo "</div>\n</body>\n</html>";
|
<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