Skip to content

Commit 89e8c7a

Browse files
f3l1xclaude
andcommitted
Full: add server list and autologin plugins via env vars (#73)
Two standalone Adminer plugins activated by env vars at container startup: - ADMINER_AUTOLOGIN_SERVER=driver://user:pass@host:port/db Skips login form, connects directly. Credentials handled server-side. - ADMINER_SERVERS_Name=driver://user:pass@host:port/db Server dropdown with Auto Sign-In button. Multiple servers supported. Both Login and Auto Sign-In use stored credentials server-side. Plugins use Adminer's auto-discovery (adminer-plugins/ directory). Entrypoint copies the appropriate plugin file based on env vars. Includes docker-compose.yml for easy testing with MariaDB + PostgreSQL. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1cd8122 commit 89e8c7a

5 files changed

Lines changed: 231 additions & 0 deletions

File tree

adminer-full/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ RUN echo '@community http://nl.alpinelinux.org/alpine/v3.22/community' >> /etc/a
3939
rm -rf /var/cache/apk/*
4040

4141
ADD ./entrypoint.sh /entrypoint.sh
42+
ADD ./plugins-available/ /srv/plugins-available/
4243
RUN chmod +x /entrypoint.sh
4344

4445
WORKDIR /srv

adminer-full/entrypoint.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ else
2727
echo "[adminer] No driver plugins directory found at /srv/plugins/drivers, skipping..."
2828
fi
2929

30+
# Activate plugins based on env vars (autologin takes precedence over server list)
31+
if [ -n "${ADMINER_AUTOLOGIN_SERVER}" ]; then
32+
cp /srv/plugins-available/adminer-autologin.php /srv/adminer-plugins/
33+
echo "[adminer] Autologin plugin activated."
34+
elif env | grep -q '^ADMINER_SERVERS_'; then
35+
cp /srv/plugins-available/adminer-server-list.php /srv/adminer-plugins/
36+
echo "[adminer] Server list plugin activated."
37+
fi
38+
3039
# Copy theme CSS files based on ADMINER_THEME environment variable
3140
if [ -n "${ADMINER_THEME}" ]; then
3241
THEME_DIR="/srv/designs/${ADMINER_THEME}"
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
/**
4+
* Auto-login to a single database server. Skips the login form entirely.
5+
*
6+
* Activated by: ADMINER_AUTOLOGIN_SERVER env var
7+
* DSN format: driver://username:password@host:port/database
8+
* Example: server://root:secret@mysql:3306/mydb
9+
*/
10+
class AdminerAutologin extends Adminer\Plugin {
11+
private $config;
12+
13+
function __construct() {
14+
$dsn = getenv('ADMINER_AUTOLOGIN_SERVER');
15+
if (!$dsn || !preg_match('#^(\w+)://(?:([^:@]*)(?::([^@]*))?@)?([^/:]+)(?::(\d+))?(?:/(.*))?$#', $dsn, $m)) {
16+
return;
17+
}
18+
$host = $m[4] . (!empty($m[5]) ? ':' . $m[5] : '');
19+
$this->config = [
20+
'driver' => $m[1],
21+
'username' => urldecode($m[2] ?? ''),
22+
'password' => urldecode($m[3] ?? ''),
23+
'server' => $host,
24+
'db' => urldecode($m[6] ?? ''),
25+
];
26+
27+
// Inject login data on first visit (before Adminer processes the request)
28+
if (!isset($_GET["username"]) && empty($_POST["auth"]) && !isset($_GET["server"])) {
29+
$_POST["auth"] = [
30+
"driver" => $this->config["driver"],
31+
"server" => $this->config["server"],
32+
"username" => $this->config["username"],
33+
"password" => $this->config["password"],
34+
"db" => $this->config["db"],
35+
"permanent" => "",
36+
];
37+
}
38+
}
39+
40+
function credentials() {
41+
if ($this->config) {
42+
return [$this->config["server"], $this->config["username"], $this->config["password"]];
43+
}
44+
}
45+
46+
function login($login, $password) {
47+
if ($this->config) {
48+
return true;
49+
}
50+
}
51+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php
2+
3+
/**
4+
* Server list with optional stored credentials and Auto Sign-In button.
5+
* Credentials are handled server-side — never exposed to the browser.
6+
*
7+
* Activated by: ADMINER_SERVERS_* env vars
8+
* DSN format: driver://username:password@host:port/database
9+
* Examples:
10+
* ADMINER_SERVERS_MariaDB=server://root:secret@mysql:3306/mydb
11+
* ADMINER_SERVERS_PostgreSQL=pgsql://postgres:pwd@pg:5432/app
12+
* ADMINER_SERVERS_SQLite=sqlite:///data/db.sqlite (no credentials)
13+
*
14+
* The suffix after ADMINER_SERVERS_ becomes the display name in the dropdown.
15+
*/
16+
class AdminerServerList extends Adminer\Plugin {
17+
private $servers = [];
18+
private $credentials = [];
19+
20+
function __construct() {
21+
foreach (getenv() as $key => $value) {
22+
if (!str_starts_with($key, 'ADMINER_SERVERS_')) {
23+
continue;
24+
}
25+
$name = substr($key, strlen('ADMINER_SERVERS_'));
26+
if (!preg_match('#^(\w+)://(?:([^:@]*)(?::([^@]*))?@)?([^/:]+)(?::(\d+))?(?:/(.*))?$#', $value, $m)) {
27+
continue;
28+
}
29+
$host = $m[4] . (!empty($m[5]) ? ':' . $m[5] : '');
30+
$this->servers[$name] = ['server' => $host, 'driver' => $m[1]];
31+
if (!empty($m[2])) {
32+
$this->credentials[$name] = [
33+
'username' => urldecode($m[2]),
34+
'password' => urldecode($m[3] ?? ''),
35+
'db' => urldecode($m[6] ?? ''),
36+
];
37+
}
38+
}
39+
40+
// Set driver from selected server on POST (same as AdminerLoginServers)
41+
if ($_POST["auth"] && isset($this->servers[$_POST["auth"]["server"]])) {
42+
$_POST["auth"]["driver"] = $this->servers[$_POST["auth"]["server"]]["driver"];
43+
}
44+
45+
// Handle Auto Sign-In: inject stored credentials into POST
46+
if (isset($_POST["_autologin"]) && isset($this->credentials[$_POST["auth"]["server"]])) {
47+
$cred = $this->credentials[$_POST["auth"]["server"]];
48+
$_POST["auth"]["username"] = $cred["username"];
49+
$_POST["auth"]["password"] = $cred["password"];
50+
$_POST["auth"]["db"] = $cred["db"];
51+
}
52+
}
53+
54+
function credentials() {
55+
$server = Adminer\SERVER;
56+
if (isset($this->servers[$server])) {
57+
$host = $this->servers[$server]["server"];
58+
if (isset($this->credentials[$server])) {
59+
$cred = $this->credentials[$server];
60+
$username = $_GET["username"] ?: $cred["username"];
61+
$password = Adminer\get_password() ?: $cred["password"];
62+
return [$host, $username, $password];
63+
}
64+
return [$host, $_GET["username"], Adminer\get_password()];
65+
}
66+
}
67+
68+
function login($login, $password) {
69+
if (!isset($this->servers[Adminer\SERVER])) {
70+
return false;
71+
}
72+
if (isset($this->credentials[Adminer\SERVER])) {
73+
return true;
74+
}
75+
}
76+
77+
function loginFormField($name, $heading, $value) {
78+
if ($name == 'driver') {
79+
return '';
80+
}
81+
if ($name == 'server') {
82+
return $heading . Adminer\html_select("auth[server]", array_keys($this->servers), Adminer\SERVER) . "\n";
83+
}
84+
}
85+
86+
function head() {
87+
if (isset($_GET["username"]) || empty($this->credentials)) {
88+
return;
89+
}
90+
$servers = json_encode(array_keys($this->credentials), JSON_HEX_TAG | JSON_HEX_AMP);
91+
$nonce = \Adminer\get_nonce();
92+
echo <<<SCRIPT
93+
<script nonce="$nonce">
94+
(function() {
95+
var serversWithCreds = $servers;
96+
document.addEventListener("DOMContentLoaded", function() {
97+
var form = document.querySelector("form");
98+
if (!form) return;
99+
var serverSelect = form.querySelector('[name="auth[server]"]');
100+
if (!serverSelect) return;
101+
102+
var hidden = document.createElement("input");
103+
hidden.type = "hidden";
104+
hidden.name = "_autologin";
105+
hidden.disabled = true;
106+
form.appendChild(hidden);
107+
108+
var btn = document.createElement("input");
109+
btn.type = "submit";
110+
btn.value = "Auto Sign-In";
111+
btn.style.display = "none";
112+
btn.style.marginLeft = "5px";
113+
btn.addEventListener("click", function() {
114+
hidden.disabled = false;
115+
hidden.value = "1";
116+
});
117+
var loginBtn = form.querySelector('[type="submit"][value="Login"]');
118+
if (loginBtn) loginBtn.parentNode.insertBefore(btn, loginBtn.nextSibling);
119+
120+
function updateButton(serverName) {
121+
btn.style.display = serversWithCreds.indexOf(serverName) >= 0 ? "inline" : "none";
122+
}
123+
serverSelect.addEventListener("change", function() { updateButton(this.value); });
124+
updateButton(serverSelect.value);
125+
});
126+
})();
127+
</script>
128+
SCRIPT;
129+
}
130+
}

docker-compose.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
services:
2+
adminer:
3+
build:
4+
context: adminer-full
5+
dockerfile: Dockerfile
6+
ports:
7+
- "8080:80"
8+
environment:
9+
# Autologin: skips login form, connects directly (takes precedence over ADMINER_SERVERS_*)
10+
#ADMINER_AUTOLOGIN_SERVER: "server://root:root@mariadb:3306/testdb"
11+
ADMINER_SERVERS_MariaDB: "server://root:root@mariadb:3306/testdb"
12+
ADMINER_SERVERS_PostgreSQL: "pgsql://root:root@postgres:5432/testdb"
13+
depends_on:
14+
mariadb:
15+
condition: service_healthy
16+
postgres:
17+
condition: service_healthy
18+
19+
mariadb:
20+
image: mariadb:11
21+
environment:
22+
MARIADB_ROOT_PASSWORD: root
23+
MARIADB_DATABASE: testdb
24+
healthcheck:
25+
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
26+
interval: 5s
27+
timeout: 3s
28+
retries: 10
29+
30+
postgres:
31+
image: postgres:17-alpine
32+
environment:
33+
POSTGRES_USER: root
34+
POSTGRES_PASSWORD: root
35+
POSTGRES_DB: testdb
36+
healthcheck:
37+
test: ["CMD-SHELL", "pg_isready -U root -d testdb"]
38+
interval: 5s
39+
timeout: 3s
40+
retries: 10

0 commit comments

Comments
 (0)