auth stack page, monitor cache fix, symlink install

This commit is contained in:
Gmer4Lfe
2026-06-05 19:20:28 -04:00
parent d9ed8c7704
commit a5936331c9
7 changed files with 1277 additions and 17 deletions
+2 -2
View File
@@ -24,9 +24,9 @@ unset($_master, $_h1m, $_host1_blank, $_my_hostid, $_conf_missing);
// Determine active tab // Determine active tab
$tab = $_GET['tab'] ?? 'monitor'; $tab = $_GET['tab'] ?? 'monitor';
$validTabs = ['monitor', 'scheduler', 'docker', 'watchdog', 'partnership', 'fallback', 'arrs', 'rsync', 'settings']; $validTabs = ['monitor', 'scheduler', 'docker', 'watchdog', 'partnership', 'fallback', 'arrs', 'rsync', 'auth', 'settings'];
if (!in_array($tab, $validTabs)) $tab = 'monitor'; if (!in_array($tab, $validTabs)) $tab = 'monitor';
$tabLabels = ['monitor' => 'Monitor', 'scheduler' => 'Scheduler', 'docker' => 'Docker', 'watchdog' => 'Watchdog', 'partnership' => 'Partnership', 'fallback' => 'FallBack', 'arrs' => 'Arrs', 'rsync' => 'Rsync', 'settings' => 'Settings']; $tabLabels = ['monitor' => 'Monitor', 'scheduler' => 'Scheduler', 'docker' => 'Docker', 'watchdog' => 'Watchdog', 'partnership' => 'Partnership', 'fallback' => 'FallBack', 'arrs' => 'Arrs', 'rsync' => 'Rsync', 'auth' => 'Auth Stack', 'settings' => 'Settings'];
?> ?>
<link rel="stylesheet" href="/plugins/<?=$plugin?>/css/varaverk.css"> <link rel="stylesheet" href="/plugins/<?=$plugin?>/css/varaverk.css">
+47
View File
@@ -0,0 +1,47 @@
<?php
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/auth.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$action = $_GET['action'] ?? '';
$result = match ($action) {
'npm_proxies' => vv_npm_list_proxies(),
'npm_certs' => ['ok' => true, 'certs' => vv_npm_list_certs()],
'lldap_users' => vv_lldap_list_users(),
'lldap_groups' => vv_lldap_list_groups(),
'authelia_rules' => vv_authelia_read_rules(),
default => ['ok' => false, 'error' => 'Unknown action: ' . $action],
};
echo json_encode($result);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['ok' => false, 'error' => 'GET or POST only']);
exit;
}
$action = trim($_POST['action'] ?? '');
$result = match ($action) {
// NPM
'npm_create' => vv_npm_create_proxy(json_decode($_POST['data'] ?? '{}', true) ?: []),
'npm_update' => vv_npm_update_proxy((int)($_POST['id'] ?? 0), json_decode($_POST['data'] ?? '{}', true) ?: []),
'npm_delete' => vv_npm_delete_proxy((int)($_POST['id'] ?? 0)),
'npm_toggle' => vv_npm_toggle_proxy((int)($_POST['id'] ?? 0), ($_POST['enabled'] ?? '0') === '1'),
// lldap
'lldap_create_user' => vv_lldap_create_user($_POST['uid'] ?? '', $_POST['email'] ?? '', $_POST['display_name'] ?? '', $_POST['password'] ?? ''),
'lldap_update_user' => vv_lldap_update_user($_POST['uid'] ?? '', $_POST['email'] ?? '', $_POST['display_name'] ?? ''),
'lldap_delete_user' => vv_lldap_delete_user($_POST['uid'] ?? ''),
'lldap_set_password' => vv_lldap_set_password($_POST['uid'] ?? '', $_POST['password'] ?? ''),
'lldap_create_group' => vv_lldap_create_group($_POST['name'] ?? ''),
'lldap_delete_group' => vv_lldap_delete_group((int)($_POST['id'] ?? 0)),
'lldap_add_to_group' => vv_lldap_add_to_group($_POST['uid'] ?? '', (int)($_POST['gid'] ?? 0)),
'lldap_remove_from_group' => vv_lldap_remove_from_group($_POST['uid'] ?? '', (int)($_POST['gid'] ?? 0)),
// Authelia
'authelia_save' => vv_authelia_write_rules(json_decode($_POST['rules'] ?? '[]', true) ?: [], $_POST['default_policy'] ?? 'deny'),
default => ['ok' => false, 'error' => 'Unknown action: ' . $action],
};
echo json_encode($result);
+2
View File
@@ -2,9 +2,11 @@
header('Content-Type: application/json'); header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php'; require_once dirname(__DIR__) . '/include/config.php';
if (!isset($_GET['live'])) {
$_vv_cached = vv_cache_read('monitor', 300); $_vv_cached = vv_cache_read('monitor', 300);
if ($_vv_cached !== null) { echo json_encode($_vv_cached); exit; } if ($_vv_cached !== null) { echo json_encode($_vv_cached); exit; }
unset($_vv_cached); unset($_vv_cached);
}
require_once dirname(__DIR__) . '/include/monitor.php'; require_once dirname(__DIR__) . '/include/monitor.php';
require_once dirname(__DIR__) . '/include/vms.php'; require_once dirname(__DIR__) . '/include/vms.php';
+285
View File
@@ -0,0 +1,285 @@
<?php
require_once __DIR__ . '/config.php';
// ── Config ────────────────────────────────────────────────────────────────────
function vv_auth_conf(): array {
$v = vv_conf_vars();
$host = strtoupper(vv_detect_host());
return [
'npm_url' => rtrim($v["{$host}_NPM_URL"] ?? 'http://localhost:81', '/'),
'npm_user' => $v["{$host}_NPM_USER"] ?? '',
'npm_pass' => $v["{$host}_NPM_PASS"] ?? '',
'lldap_url' => rtrim($v["{$host}_LLDAP_URL"] ?? 'http://localhost:17170', '/'),
'lldap_user' => $v["{$host}_LLDAP_USER"] ?? '',
'lldap_pass' => $v["{$host}_LLDAP_PASS"] ?? '',
'authelia_config' => $v["{$host}_AUTHELIA_CONFIG"] ?? '/mnt/user/appdata-Fallback/Critical-Data/Authelia/configuration.yml',
'authelia_container' => $v["{$host}_AUTHELIA_CONTAINER"] ?? 'Authelia',
'is_owner' => vv_is_owner(),
];
}
// ── NPM ───────────────────────────────────────────────────────────────────────
function vv_npm_token(): string {
if (!session_id()) session_start();
$conf = vv_auth_conf();
$cached = $_SESSION['vv_npm_token'] ?? '';
$expiry = $_SESSION['vv_npm_token_exp'] ?? 0;
if ($cached && time() < $expiry) return $cached;
$resp = vv_npm_raw('POST', '/api/tokens', [
'identity' => $conf['npm_user'],
'secret' => $conf['npm_pass'],
'expiry' => '1d',
], '', $conf);
$token = $resp['token'] ?? '';
if ($token) {
$_SESSION['vv_npm_token'] = $token;
$_SESSION['vv_npm_token_exp'] = time() + 82800;
}
return $token;
}
function vv_npm_raw(string $method, string $path, array $data, string $token, array $conf = []): array {
if (!$conf) $conf = vv_auth_conf();
$url = $conf['npm_url'] . $path;
$headers = ['Content-Type: application/json', 'Accept: application/json'];
if ($token) $headers[] = 'Authorization: Bearer ' . $token;
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_CUSTOMREQUEST => $method,
]);
if ($data && in_array($method, ['POST', 'PUT'], true))
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
$body = curl_exec($ch);
curl_close($ch);
return json_decode($body ?: '{}', true) ?: [];
}
function vv_npm_req(string $method, string $path, array $data = []): array {
$token = vv_npm_token();
if (!$token) return ['_err' => 'NPM auth failed — check credentials in host conf'];
return vv_npm_raw($method, $path, $data, $token);
}
function vv_npm_list_proxies(): array {
$list = vv_npm_req('GET', '/api/nginx/proxy-hosts?expand=certificate');
if (!is_array($list) || isset($list['_err']))
return ['ok' => false, 'error' => $list['_err'] ?? 'Invalid response from NPM'];
return ['ok' => true, 'proxies' => $list];
}
function vv_npm_list_certs(): array {
$list = vv_npm_req('GET', '/api/nginx/certificates');
return is_array($list) ? $list : [];
}
function vv_npm_create_proxy(array $data): array {
$r = vv_npm_req('POST', '/api/nginx/proxy-hosts', $data);
return isset($r['id']) ? ['ok' => true, 'proxy' => $r] : ['ok' => false, 'error' => $r['error'] ?? ($r['_err'] ?? 'Create failed')];
}
function vv_npm_update_proxy(int $id, array $data): array {
$r = vv_npm_req('PUT', "/api/nginx/proxy-hosts/$id", $data);
return isset($r['id']) ? ['ok' => true, 'proxy' => $r] : ['ok' => false, 'error' => $r['error'] ?? ($r['_err'] ?? 'Update failed')];
}
function vv_npm_delete_proxy(int $id): array {
vv_npm_req('DELETE', "/api/nginx/proxy-hosts/$id");
return ['ok' => true];
}
function vv_npm_toggle_proxy(int $id, bool $enabled): array {
vv_npm_req('POST', "/api/nginx/proxy-hosts/$id/" . ($enabled ? 'enable' : 'disable'));
return ['ok' => true];
}
// ── lldap ─────────────────────────────────────────────────────────────────────
function vv_lldap_token(): string {
if (!session_id()) session_start();
$conf = vv_auth_conf();
$cached = $_SESSION['vv_lldap_token'] ?? '';
$expiry = $_SESSION['vv_lldap_token_exp'] ?? 0;
if ($cached && time() < $expiry) return $cached;
$ch = curl_init($conf['lldap_url'] . '/auth/simple/login');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['username' => $conf['lldap_user'], 'password' => $conf['lldap_pass']]),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);
$body = curl_exec($ch);
curl_close($ch);
$resp = json_decode($body ?: '{}', true) ?: [];
$token = $resp['token'] ?? '';
if ($token) {
$_SESSION['vv_lldap_token'] = $token;
$_SESSION['vv_lldap_token_exp'] = time() + 3500;
}
return $token;
}
function vv_lldap_gql(string $query, array $variables = []): array {
$conf = vv_auth_conf();
$token = vv_lldap_token();
if (!$token) return ['errors' => [['message' => 'lldap auth failed — check credentials']]];
$ch = curl_init($conf['lldap_url'] . '/api/graphql');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['query' => $query, 'variables' => $variables]),
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $token],
]);
$body = curl_exec($ch);
curl_close($ch);
return json_decode($body ?: '{}', true) ?: [];
}
function vv_lldap_list_users(): array {
$r = vv_lldap_gql('query { listUsers { id displayName email creationDate groups { id displayName } } }');
if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Query failed'];
return ['ok' => true, 'users' => $r['data']['listUsers'] ?? []];
}
function vv_lldap_list_groups(): array {
$r = vv_lldap_gql('query { listGroups { id displayName users { id displayName } } }');
if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Query failed'];
return ['ok' => true, 'groups' => $r['data']['listGroups'] ?? []];
}
function vv_lldap_create_user(string $id, string $email, string $displayName, string $password): array {
$r = vv_lldap_gql(
'mutation CreateUser($user: CreateUserInput!) { createUser(user: $user) { id displayName email } }',
['user' => ['id' => $id, 'email' => $email, 'displayName' => $displayName]]
);
if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Create failed'];
if ($password) vv_lldap_set_password($id, $password);
return ['ok' => true, 'user' => $r['data']['createUser'] ?? []];
}
function vv_lldap_update_user(string $id, string $email, string $displayName): array {
$r = vv_lldap_gql(
'mutation UpdateUser($user: UpdateUserInput!) { updateUser(user: $user) { ok } }',
['user' => ['id' => $id, 'email' => $email, 'displayName' => $displayName]]
);
if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Update failed'];
return ['ok' => true];
}
function vv_lldap_delete_user(string $id): array {
$r = vv_lldap_gql(
'mutation DeleteUser($userId: String!) { deleteUser(userId: $userId) { ok } }',
['userId' => $id]
);
if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Delete failed'];
return ['ok' => true];
}
function vv_lldap_set_password(string $userId, string $password): array {
$r = vv_lldap_gql(
'mutation ChangePassword($userId: String!, $password: String!) { changeUserPassword(userId: $userId, password: $password) }',
['userId' => $userId, 'password' => $password]
);
if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Password change failed'];
return ['ok' => true];
}
function vv_lldap_create_group(string $name): array {
$r = vv_lldap_gql(
'mutation CreateGroup($name: String!) { createGroup(name: $name) { id displayName } }',
['name' => $name]
);
if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Create failed'];
return ['ok' => true, 'group' => $r['data']['createGroup'] ?? []];
}
function vv_lldap_delete_group(int $id): array {
$r = vv_lldap_gql(
'mutation DeleteGroup($groupId: Int!) { deleteGroup(groupId: $groupId) { ok } }',
['groupId' => $id]
);
if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Delete failed'];
return ['ok' => true];
}
function vv_lldap_add_to_group(string $userId, int $groupId): array {
$r = vv_lldap_gql(
'mutation AddUserToGroup($userId: String!, $groupId: Int!) { addUserToGroup(userId: $userId, groupId: $groupId) { ok } }',
['userId' => $userId, 'groupId' => $groupId]
);
if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Failed'];
return ['ok' => true];
}
function vv_lldap_remove_from_group(string $userId, int $groupId): array {
$r = vv_lldap_gql(
'mutation RemoveUserFromGroup($userId: String!, $groupId: Int!) { removeUserFromGroup(userId: $userId, groupId: $groupId) { ok } }',
['userId' => $userId, 'groupId' => $groupId]
);
if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Failed'];
return ['ok' => true];
}
// ── Authelia ──────────────────────────────────────────────────────────────────
function vv_authelia_read_rules(): array {
$conf = vv_auth_conf();
$file = $conf['authelia_config'];
if (!file_exists($file)) return ['ok' => false, 'error' => 'Config not found: ' . $file];
$py = "import yaml,json,sys\n"
. "d=yaml.safe_load(open(sys.argv[1]))\n"
. "ac=d.get('access_control',{})\n"
. "print(json.dumps({'default_policy':ac.get('default_policy','deny'),'rules':ac.get('rules',[])}))\n";
$tmp = '/tmp/vv_auth_rd_' . getmypid() . '.py';
file_put_contents($tmp, $py);
$out = shell_exec('python3 ' . escapeshellarg($tmp) . ' ' . escapeshellarg($file) . ' 2>/dev/null');
@unlink($tmp);
if (!$out) return ['ok' => false, 'error' => 'Parse failed — python3 with PyYAML required'];
$data = json_decode(trim($out), true);
if (!$data) return ['ok' => false, 'error' => 'Invalid YAML response'];
return ['ok' => true, 'default_policy' => $data['default_policy'], 'rules' => $data['rules']];
}
function vv_authelia_write_rules(array $rules, string $defaultPolicy): array {
$conf = vv_auth_conf();
$file = $conf['authelia_config'];
if (!file_exists($file)) return ['ok' => false, 'error' => 'Config not found: ' . $file];
$acJson = json_encode(['default_policy' => $defaultPolicy, 'rules' => $rules]);
$py = <<<'PYEOF'
import yaml, json, sys, re
config_file = sys.argv[1]
new_ac = json.loads(sys.argv[2])
with open(config_file, 'r') as f:
content = f.read()
new_block = yaml.dump({'access_control': new_ac}, default_flow_style=False, allow_unicode=True, sort_keys=False)
pattern = r'(?ms)^access_control:.*?(?=^[a-zA-Z#]|\Z)'
if re.search(pattern, content):
content = re.sub(pattern, new_block + '\n', content)
else:
content = content.rstrip('\n') + '\n\n' + new_block + '\n'
with open(config_file, 'w') as f:
f.write(content)
print('ok')
PYEOF;
$tmp = '/tmp/vv_auth_wr_' . getmypid() . '.py';
file_put_contents($tmp, $py);
$out = shell_exec('python3 ' . escapeshellarg($tmp) . ' ' . escapeshellarg($file) . ' ' . escapeshellarg($acJson) . ' 2>&1');
@unlink($tmp);
if (trim($out) !== 'ok') return ['ok' => false, 'error' => 'Write failed: ' . trim($out)];
shell_exec('docker restart ' . escapeshellarg($conf['authelia_container']) . ' >/dev/null 2>&1 &');
return ['ok' => true];
}
+917
View File
@@ -0,0 +1,917 @@
<style>
/* ── Toolbar ─────────────────────────────────────────────────────────────── */
.vv-au-toolbar { display:flex;align-items:center;gap:8px;margin-bottom:12px;flex-wrap:wrap; }
.vv-au-title { font-size:13px;font-weight:bold;color:#888;text-transform:uppercase;letter-spacing:.06em;flex:1; }
.vv-au-tab { font-size:11px;padding:3px 12px;border-radius:3px;border:1px solid #2a2a2a;background:#1a1a1a;color:#666;cursor:pointer;white-space:nowrap; }
.vv-au-tab:hover{ background:#222;color:#aaa; }
.vv-au-tab.active { background:#0d1f2a;border-color:#1a3a5a;color:#5c9fd4; }
.vv-au-btn { font-size:11px;padding:3px 10px;border-radius:3px;border:1px solid #2a2a2a;background:#1a1a1a;color:#888;cursor:pointer;white-space:nowrap; }
.vv-au-btn:hover{ background:#222;color:#bbb; }
.vv-au-btn.prim { border-color:#1a3a5a;background:#0d1f2a;color:#5c9fd4; }
.vv-au-btn.warn { border-color:#3a2000;background:#1f1200;color:#ff9800; }
.vv-au-btn.danger{border-color:#3a1a1a;background:#200d0d;color:#ef5350; }
.vv-au-btn.green{ border-color:#1a3a1a;background:#0d1f0d;color:#4caf50; }
.vv-au-btn:disabled { opacity:.4;cursor:default; }
/* ── Panels ──────────────────────────────────────────────────────────────── */
.vv-au-panel { display:none; }
.vv-au-panel.active { display:block; }
/* ── Tables ──────────────────────────────────────────────────────────────── */
.vv-au-tbl { width:100%;border-collapse:collapse;font-size:11px; }
.vv-au-tbl th { text-align:left;padding:5px 10px;font-size:10px;color:#444;text-transform:uppercase;letter-spacing:.06em;border-bottom:1px solid #1e1e1e;white-space:nowrap; }
.vv-au-tbl td { padding:7px 10px;border-bottom:1px solid #161616;vertical-align:middle; }
.vv-au-tbl tr:hover td { background:#141414; }
.vv-au-tbl tr:last-child td { border-bottom:none; }
/* ── Badges ──────────────────────────────────────────────────────────────── */
.vv-au-badge { font-size:10px;padding:1px 6px;border-radius:2px;white-space:nowrap;display:inline-block; }
.vv-au-badge.ssl { background:#0d1f0d;border:1px solid #1a3a1a;color:#4caf50; }
.vv-au-badge.nossl { background:#111;border:1px solid #222;color:#444; }
.vv-au-badge.on { background:#0d1f0d;border:1px solid #1a3a1a;color:#4caf50; }
.vv-au-badge.off { background:#1a0a0a;border:1px solid #2a1a1a;color:#555; }
.vv-au-badge.bypass { background:#0a1a2a;border:1px solid #1a3a5a;color:#5c9fd4; }
.vv-au-badge.one_factor { background:#1a1a0a;border:1px solid #3a3a1a;color:#cddc39; }
.vv-au-badge.two_factor { background:#0d1f0d;border:1px solid #1a3a1a;color:#4caf50; }
.vv-au-badge.deny { background:#1a0a0a;border:1px solid #3a1a1a;color:#ef5350; }
.vv-au-badge.grp { background:#1a0d2a;border:1px solid #2a1a4a;color:#9c6ff7;margin:1px 2px; }
.vv-au-badge.host { background:#111;border:1px solid #1e1e1e;color:#444; }
/* ── Cards ───────────────────────────────────────────────────────────────── */
.vv-au-card { background:#161616;border:1px solid #222;border-radius:6px;overflow:hidden; }
.vv-au-card-h { display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid #1e1e1e;background:#111; }
.vv-au-card-title { font-size:11px;font-weight:bold;color:#666;text-transform:uppercase;letter-spacing:.06em;flex:1; }
/* ── Users / Groups split ────────────────────────────────────────────────── */
.vv-au-ug-grid { display:grid;grid-template-columns:3fr 2fr;gap:12px; }
@media (max-width:700px) { .vv-au-ug-grid { grid-template-columns:1fr; } }
.vv-au-user-row { padding:8px 12px;border-bottom:1px solid #1a1a1a;display:flex;align-items:center;gap:8px;flex-wrap:wrap; }
.vv-au-user-row:last-child { border-bottom:none; }
.vv-au-user-row:hover { background:#141414; }
.vv-au-user-name { font-size:12px;color:#bbb;font-weight:bold;min-width:80px; }
.vv-au-user-email{ font-size:10px;color:#444;flex:1; }
.vv-au-user-acts { display:flex;gap:4px;margin-left:auto;flex-shrink:0; }
.vv-au-grp-row { padding:8px 12px;border-bottom:1px solid #1a1a1a;cursor:pointer; }
.vv-au-grp-row:last-child { border-bottom:none; }
.vv-au-grp-row:hover { background:#141414; }
.vv-au-grp-name { font-size:12px;color:#bbb;font-weight:bold; }
.vv-au-grp-cnt { font-size:10px;color:#444;margin-top:2px; }
.vv-au-grp-acts { float:right;display:flex;gap:4px;margin-top:1px; }
.vv-au-grp-members { padding:6px 12px 10px 20px;background:#0f0f0f;border-bottom:1px solid #1a1a1a; display:none; }
.vv-au-grp-members.open { display:block; }
.vv-au-grp-member { font-size:11px;color:#666;padding:2px 0;display:flex;align-items:center;gap:6px; }
.vv-au-grp-member-rm { font-size:11px;color:#3a1a1a;cursor:pointer;padding:0 2px; }
.vv-au-grp-member-rm:hover { color:#ef5350; }
/* ── Access Control ──────────────────────────────────────────────────────── */
.vv-au-ac-defpol { display:flex;align-items:center;gap:10px;padding:8px 12px;background:#111;border:1px solid #222;border-radius:4px;margin-bottom:10px;font-size:11px;color:#555; }
.vv-au-ac-defpol select { background:#0d0d0d;border:1px solid #2a2a2a;border-radius:3px;color:#888;font-size:11px;padding:2px 6px; }
.vv-au-rule-num { color:#333;font-size:10px;width:24px;text-align:right;flex-shrink:0; }
.vv-au-rule-acts { display:flex;gap:3px;white-space:nowrap; }
.vv-au-icon-btn { font-size:12px;color:#444;cursor:pointer;padding:1px 3px;border-radius:2px;background:none;border:none;line-height:1; }
.vv-au-icon-btn:hover { color:#bbb;background:#222; }
.vv-au-icon-btn.del:hover { color:#ef5350;background:#1a0808; }
/* ── Section toolbar (within panel) ─────────────────────────────────────── */
.vv-au-sec-bar { display:flex;align-items:center;gap:8px;margin-bottom:10px; }
.vv-au-sec-title{ font-size:11px;color:#555;flex:1; }
/* ── Toggle switch ───────────────────────────────────────────────────────── */
.vv-au-tog { width:28px;height:16px;border-radius:8px;background:#1e1e1e;border:1px solid #2a2a2a;position:relative;cursor:pointer;display:inline-block;flex-shrink:0; }
.vv-au-tog.on { background:#1a3a1a;border-color:#2d5a2d; }
.vv-au-tog::after { content:'';position:absolute;top:2px;left:2px;width:10px;height:10px;border-radius:50%;background:#444;transition:left .12s,background .12s; }
.vv-au-tog.on::after { left:14px;background:#4caf50; }
/* ── Modal ───────────────────────────────────────────────────────────────── */
.vv-au-overlay { position:fixed;inset:0;background:#0009;z-index:9000;display:none;align-items:center;justify-content:center; }
.vv-au-overlay.open { display:flex; }
.vv-au-modal { background:#181818;border:1px solid #2a2a2a;border-radius:6px;padding:18px 20px;min-width:360px;max-width:520px;width:90vw;max-height:85vh;overflow-y:auto; }
.vv-au-modal h3 { font-size:12px;font-weight:bold;color:#888;text-transform:uppercase;letter-spacing:.06em;margin:0 0 14px; }
.vv-au-field { margin-bottom:10px; }
.vv-au-label { display:block;font-size:10px;color:#555;margin-bottom:3px;text-transform:uppercase;letter-spacing:.04em; }
.vv-au-input { width:100%;box-sizing:border-box;background:#0d0d0d;border:1px solid #252525;border-radius:3px;color:#bbb;font-size:11px;padding:5px 8px;outline:none; }
.vv-au-input:focus { border-color:#1a3a5a; }
.vv-au-select { width:100%;box-sizing:border-box;background:#0d0d0d;border:1px solid #252525;border-radius:3px;color:#bbb;font-size:11px;padding:5px 8px;outline:none; }
.vv-au-hint { font-size:10px;color:#333;margin-top:2px; }
.vv-au-modal-acts { display:flex;justify-content:flex-end;gap:8px;margin-top:16px; }
.vv-au-tog-row { display:flex;align-items:center;gap:8px;margin-bottom:10px; }
.vv-au-tog-lbl { font-size:11px;color:#666; }
.vv-au-adv-toggle { font-size:10px;color:#333;cursor:pointer;margin-bottom:8px; }
.vv-au-adv-toggle:hover { color:#666; }
.vv-au-adv-section { display:none; }
.vv-au-adv-section.open { display:block; }
.vv-au-err { font-size:11px;color:#ef5350;margin-top:8px;display:none; }
.vv-au-err.show { display:block; }
/* ── Misc ────────────────────────────────────────────────────────────────── */
.vv-au-empty { padding:24px;text-align:center;font-size:11px;color:#333; }
.vv-au-domain { font-size:12px;color:#bbb;font-weight:bold; }
.vv-au-fwd { font-size:10px;color:#444; }
.vv-au-loading { color:#333;font-size:11px;padding:16px;text-align:center; }
</style>
<?php
require_once __DIR__ . '/../include/auth.php';
$isOwner = vv_is_owner();
?>
<div class="vv-au-toolbar">
<span class="vv-au-title">Auth Stack</span>
<button class="vv-au-tab active" data-tab="proxies">Proxies</button>
<button class="vv-au-tab" data-tab="users">Users &amp; Groups</button>
<button class="vv-au-tab" data-tab="acl">Access Control</button>
<button class="vv-au-btn" id="vv-au-refresh" title="Refresh current tab">&#8635; Refresh</button>
</div>
<!-- ── Proxies ─────────────────────────────────────────────────────────────── -->
<div class="vv-au-panel active" id="vv-au-panel-proxies">
<div class="vv-au-sec-bar">
<span class="vv-au-sec-title" id="vv-au-proxy-count"></span>
<button class="vv-au-btn prim" id="vv-au-proxy-add">+ Add Proxy</button>
</div>
<div class="vv-au-card">
<div class="vv-au-loading" id="vv-au-proxy-loading">Loading…</div>
<table class="vv-au-tbl" id="vv-au-proxy-tbl" style="display:none">
<thead><tr>
<th>Domain</th><th>Forward</th><th>SSL</th><th>Status</th><th></th>
</tr></thead>
<tbody id="vv-au-proxy-body"></tbody>
</table>
<div class="vv-au-empty" id="vv-au-proxy-empty" style="display:none">No proxy hosts configured.</div>
</div>
</div>
<!-- ── Users & Groups ─────────────────────────────────────────────────────── -->
<div class="vv-au-panel" id="vv-au-panel-users">
<div class="vv-au-ug-grid">
<div class="vv-au-card">
<div class="vv-au-card-h">
<span class="vv-au-card-title">Users</span>
<button class="vv-au-btn prim" id="vv-au-user-add">+ Add User</button>
</div>
<div class="vv-au-loading" id="vv-au-users-loading">Loading…</div>
<div id="vv-au-users-list"></div>
<div class="vv-au-empty" id="vv-au-users-empty" style="display:none">No users found.</div>
</div>
<div class="vv-au-card">
<div class="vv-au-card-h">
<span class="vv-au-card-title">Groups</span>
<button class="vv-au-btn prim" id="vv-au-group-add">+ Add Group</button>
</div>
<div class="vv-au-loading" id="vv-au-groups-loading">Loading…</div>
<div id="vv-au-groups-list"></div>
<div class="vv-au-empty" id="vv-au-groups-empty" style="display:none">No groups found.</div>
</div>
</div>
</div>
<!-- ── Access Control ─────────────────────────────────────────────────────── -->
<div class="vv-au-panel" id="vv-au-panel-acl">
<div class="vv-au-ac-defpol" id="vv-au-ac-defpol-bar">
<span>Default policy:</span>
<select id="vv-au-ac-defpol" <?= $isOwner ? '' : 'disabled' ?>>
<option value="deny">deny</option>
<option value="two_factor">two_factor</option>
<option value="one_factor">one_factor</option>
<option value="bypass">bypass</option>
</select>
<?php if (!$isOwner): ?>
<span style="font-size:10px;color:#3a2a1a;">default policy editable on HOST1</span>
<?php endif; ?>
<button class="vv-au-btn green" id="vv-au-ac-save" style="margin-left:auto">Save &amp; Restart Authelia</button>
</div>
<div class="vv-au-sec-bar">
<span class="vv-au-sec-title" id="vv-au-acl-count"></span>
<button class="vv-au-btn prim" id="vv-au-rule-add">+ Add Rule</button>
</div>
<div class="vv-au-card">
<div class="vv-au-loading" id="vv-au-acl-loading">Loading…</div>
<table class="vv-au-tbl" id="vv-au-acl-tbl" style="display:none">
<thead><tr>
<th style="width:24px">#</th>
<th>Domain</th><th>Policy</th><th>Subject</th><th></th>
</tr></thead>
<tbody id="vv-au-acl-body"></tbody>
</table>
<div class="vv-au-empty" id="vv-au-acl-empty" style="display:none">No rules configured.</div>
</div>
</div>
<!-- ── Modal overlay ──────────────────────────────────────────────────────── -->
<div class="vv-au-overlay" id="vv-au-overlay">
<div class="vv-au-modal" id="vv-au-modal"></div>
</div>
<script>
(function () {
'use strict';
const API = '/plugins/varaverk/api/auth.php';
const IS_OWNER = <?= $isOwner ? 'true' : 'false' ?>;
// ── State ─────────────────────────────────────────────────────────────────────
let _proxies = [], _certs = [];
let _users = [], _groups = [];
let _rules = [], _defaultPolicy = 'deny';
let _activeTab = 'proxies';
// ── Helpers ───────────────────────────────────────────────────────────────────
function _esc(s) {
return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function _get(action, cb) {
fetch(API + '?action=' + action)
.then(r => r.json()).then(cb)
.catch(e => cb({ ok: false, error: String(e) }));
}
function _post(params, cb) {
const fd = new FormData();
for (const [k, v] of Object.entries(params)) fd.append(k, v);
fetch(API, { method: 'POST', body: fd })
.then(r => r.json()).then(cb)
.catch(e => cb({ ok: false, error: String(e) }));
}
function _modal(html) {
document.getElementById('vv-au-modal').innerHTML = html;
document.getElementById('vv-au-overlay').classList.add('open');
}
function _closeModal() {
document.getElementById('vv-au-overlay').classList.remove('open');
}
function _togHtml(id, on, title) {
return `<span class="vv-au-tog${on?' on':''}" data-tog="${_esc(id)}" title="${_esc(title)}"></span>`;
}
function _policyBadge(p) {
return `<span class="vv-au-badge ${_esc(p)}">${_esc(p)}</span>`;
}
function _normSubject(val) {
if (!val) return [];
if (Array.isArray(val)) return val;
return [val];
}
function _normDomain(val) {
if (!val) return [];
if (Array.isArray(val)) return val;
return [val];
}
// ── Tab switching ─────────────────────────────────────────────────────────────
document.querySelectorAll('.vv-au-tab').forEach(btn => {
btn.addEventListener('click', () => {
const tab = btn.dataset.tab;
document.querySelectorAll('.vv-au-tab').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.vv-au-panel').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById('vv-au-panel-' + tab).classList.add('active');
_activeTab = tab;
_loadTab(tab);
});
});
document.getElementById('vv-au-refresh').addEventListener('click', () => _loadTab(_activeTab));
function _loadTab(tab) {
if (tab === 'proxies') _loadProxies();
if (tab === 'users') { _loadUsers(); _loadGroups(); }
if (tab === 'acl') _loadAcl();
}
// ── Proxies ───────────────────────────────────────────────────────────────────
function _loadProxies() {
const loading = document.getElementById('vv-au-proxy-loading');
const tbl = document.getElementById('vv-au-proxy-tbl');
const empty = document.getElementById('vv-au-proxy-empty');
loading.style.display = 'block';
tbl.style.display = 'none';
empty.style.display = 'none';
// Load certs and proxies in parallel
let certsLoaded = false, proxiesLoaded = false;
function _check() {
if (!certsLoaded || !proxiesLoaded) return;
loading.style.display = 'none';
if (!_proxies.length) { empty.style.display = 'block'; return; }
tbl.style.display = 'table';
_renderProxies();
}
_get('npm_certs', r => { _certs = r.certs || []; certsLoaded = true; _check(); });
_get('npm_proxies', r => {
if (!r.ok) { loading.innerHTML = '<span style="color:#ef5350">'+_esc(r.error)+'</span>'; return; }
_proxies = r.proxies || [];
proxiesLoaded = true;
_check();
});
}
function _renderProxies() {
const count = document.getElementById('vv-au-proxy-count');
count.textContent = _proxies.length + ' host' + (_proxies.length !== 1 ? 's' : '');
const body = document.getElementById('vv-au-proxy-body');
body.innerHTML = _proxies.map(p => {
const domains = (p.domain_names || []).join(', ');
const fwd = p.forward_scheme + '://' + p.forward_host + ':' + p.forward_port;
const hasSsl = p.certificate_id && p.certificate_id !== '0';
const sslBadge = hasSsl
? `<span class="vv-au-badge ssl">SSL</span>`
: `<span class="vv-au-badge nossl">none</span>`;
const enabled = p.enabled;
return `<tr>
<td><div class="vv-au-domain">${_esc(domains)}</div></td>
<td><div class="vv-au-fwd">${_esc(fwd)}</div></td>
<td>${sslBadge}</td>
<td>${_togHtml('proxy-' + p.id, enabled, enabled ? 'Enabled — click to disable' : 'Disabled — click to enable')}</td>
<td style="text-align:right">
<div class="vv-au-rule-acts">
<button class="vv-au-icon-btn" data-proxy-edit="${p.id}" title="Edit">✎</button>
<button class="vv-au-icon-btn del" data-proxy-del="${p.id}" title="Delete">✕</button>
</div>
</td>
</tr>`;
}).join('');
}
function _proxyModal(id) {
const p = id ? _proxies.find(x => x.id === id) : null;
const certOptions = _certs.map(c =>
`<option value="${c.id}"${p && p.certificate_id == c.id ? ' selected' : ''}>${_esc(c.nice_name || c.domain_names?.join(', '))}</option>`
).join('');
_modal(`<h3>${p ? 'Edit Proxy' : 'Add Proxy'}</h3>
<div class="vv-au-field">
<label class="vv-au-label">Domain Names</label>
<input class="vv-au-input" id="pm-domains" value="${_esc((p?.domain_names||[]).join(', '))}" placeholder="example.com, *.example.com">
<div class="vv-au-hint">Comma-separated</div>
</div>
<div style="display:grid;grid-template-columns:1fr 2fr 80px;gap:8px">
<div class="vv-au-field">
<label class="vv-au-label">Scheme</label>
<select class="vv-au-select" id="pm-scheme">
<option${(!p||p.forward_scheme==='http')?' selected':''}>http</option>
<option${(p?.forward_scheme==='https')?' selected':''}>https</option>
</select>
</div>
<div class="vv-au-field">
<label class="vv-au-label">Forward Host</label>
<input class="vv-au-input" id="pm-host" value="${_esc(p?.forward_host||'')}" placeholder="192.168.1.100">
</div>
<div class="vv-au-field">
<label class="vv-au-label">Port</label>
<input class="vv-au-input" id="pm-port" type="number" value="${_esc(p?.forward_port||80)}">
</div>
</div>
<div class="vv-au-field">
<label class="vv-au-label">SSL Certificate</label>
<select class="vv-au-select" id="pm-cert">
<option value="0">None</option>
${certOptions}
</select>
</div>
<div class="vv-au-tog-row">
<span class="vv-au-tog${p?.ssl_forced?' on':''}" id="pm-ssl-forced"></span>
<span class="vv-au-tog-lbl">Force SSL</span>
</div>
<div class="vv-au-tog-row">
<span class="vv-au-tog${p?.block_exploits?' on':''}" id="pm-block-exploits"></span>
<span class="vv-au-tog-lbl">Block Common Exploits</span>
</div>
<div class="vv-au-tog-row">
<span class="vv-au-tog${(!p||p.allow_websocket_upgrade!==false)?' on':''}" id="pm-websocket"></span>
<span class="vv-au-tog-lbl">WebSocket Support</span>
</div>
<div class="vv-au-err" id="pm-err"></div>
<div class="vv-au-modal-acts">
<button class="vv-au-btn" id="pm-cancel">Cancel</button>
<button class="vv-au-btn prim" id="pm-save">${p ? 'Save' : 'Create'}</button>
</div>`);
// Wire inline toggles
document.querySelectorAll('#vv-au-modal .vv-au-tog').forEach(t => {
t.addEventListener('click', () => t.classList.toggle('on'));
});
document.getElementById('pm-cancel').onclick = _closeModal;
document.getElementById('pm-save').onclick = () => {
const domains = document.getElementById('pm-domains').value.split(',').map(s=>s.trim()).filter(Boolean);
if (!domains.length) { _showModalErr('pm-err', 'Domain required'); return; }
const host = document.getElementById('pm-host').value.trim();
if (!host) { _showModalErr('pm-err', 'Forward host required'); return; }
const data = {
domain_names: domains,
forward_scheme: document.getElementById('pm-scheme').value,
forward_host: host,
forward_port: parseInt(document.getElementById('pm-port').value) || 80,
certificate_id: parseInt(document.getElementById('pm-cert').value) || 0,
ssl_forced: document.getElementById('pm-ssl-forced').classList.contains('on'),
block_exploits: document.getElementById('pm-block-exploits').classList.contains('on'),
allow_websocket_upgrade: document.getElementById('pm-websocket').classList.contains('on'),
http2_support: false,
hsts_enabled: false,
hsts_subdomains: false,
meta: {},
locations: [],
advanced_config: '',
enabled: true,
};
const btn = document.getElementById('pm-save');
btn.disabled = true; btn.textContent = 'Saving…';
const done = r => {
if (!r.ok) { _showModalErr('pm-err', r.error||'Save failed'); btn.disabled=false; btn.textContent = p?'Save':'Create'; return; }
_closeModal(); _loadProxies();
};
if (p) _post({ action:'npm_update', id: p.id, data: JSON.stringify(data) }, done);
else _post({ action:'npm_create', data: JSON.stringify(data) }, done);
};
}
function _showModalErr(id, msg) {
const el = document.getElementById(id);
if (el) { el.textContent = msg; el.classList.add('show'); }
}
// Proxy event delegation
document.getElementById('vv-au-panel-proxies').addEventListener('click', e => {
// Add button
if (e.target.id === 'vv-au-proxy-add') { _proxyModal(null); return; }
// Toggle
const tog = e.target.closest('[data-tog^="proxy-"]');
if (tog) {
const id = parseInt(tog.dataset.tog.split('-')[1]);
const was = tog.classList.contains('on');
tog.classList.toggle('on');
_post({ action:'npm_toggle', id, enabled: was?'0':'1' }, r => {
if (!r.ok) { tog.classList.toggle('on'); }
});
return;
}
// Edit
const editBtn = e.target.closest('[data-proxy-edit]');
if (editBtn) { _proxyModal(parseInt(editBtn.dataset.proxyEdit)); return; }
// Delete
const delBtn = e.target.closest('[data-proxy-del]');
if (delBtn) {
const id = parseInt(delBtn.dataset.proxyDel);
const p = _proxies.find(x => x.id === id);
if (!confirm('Delete proxy for ' + (p?.domain_names||['this host']).join(', ') + '?')) return;
_post({ action:'npm_delete', id }, r => { if (r.ok) _loadProxies(); });
}
});
// ── Users ─────────────────────────────────────────────────────────────────────
function _loadUsers() {
const loading = document.getElementById('vv-au-users-loading');
const list = document.getElementById('vv-au-users-list');
const empty = document.getElementById('vv-au-users-empty');
loading.style.display = 'block';
list.innerHTML = '';
empty.style.display = 'none';
_get('lldap_users', r => {
loading.style.display = 'none';
if (!r.ok) { loading.innerHTML = '<span style="color:#ef5350;padding:10px;display:block">'+_esc(r.error)+'</span>'; loading.style.display='block'; return; }
_users = r.users || [];
if (!_users.length) { empty.style.display = 'block'; return; }
list.innerHTML = _users.map(u => {
const grpBadges = (u.groups||[]).map(g => `<span class="vv-au-badge grp" title="Click to remove" data-rm-from-group="${_esc(u.id)}" data-gid="${g.id}">${_esc(g.displayName)}</span>`).join('');
return `<div class="vv-au-user-row">
<span class="vv-au-user-name">${_esc(u.displayName||u.id)}</span>
<span class="vv-au-user-email">${_esc(u.email||'')}</span>
<div style="display:flex;gap:3px;align-items:center;flex-wrap:wrap">${grpBadges}</div>
<div class="vv-au-user-acts">
<button class="vv-au-icon-btn" data-user-grp="${_esc(u.id)}" title="Add to group">+grp</button>
<button class="vv-au-icon-btn" data-user-pass="${_esc(u.id)}" title="Change password">🔑</button>
<button class="vv-au-icon-btn" data-user-edit="${_esc(u.id)}" title="Edit">✎</button>
<button class="vv-au-icon-btn del" data-user-del="${_esc(u.id)}" title="Delete">✕</button>
</div>
</div>`;
}).join('');
});
}
function _userModal(uid) {
const u = uid ? _users.find(x => x.id === uid) : null;
_modal(`<h3>${u ? 'Edit User' : 'Add User'}</h3>
<div class="vv-au-field">
<label class="vv-au-label">Username (ID)</label>
<input class="vv-au-input" id="um-uid" value="${_esc(u?.id||'')}" ${u?'readonly':''} placeholder="johndoe">
${u ? '' : '<div class="vv-au-hint">Lowercase letters, digits, hyphens — cannot be changed later</div>'}
</div>
<div class="vv-au-field">
<label class="vv-au-label">Display Name</label>
<input class="vv-au-input" id="um-name" value="${_esc(u?.displayName||'')}" placeholder="John Doe">
</div>
<div class="vv-au-field">
<label class="vv-au-label">Email</label>
<input class="vv-au-input" id="um-email" type="email" value="${_esc(u?.email||'')}" placeholder="john@example.com">
</div>
${!u ? `<div class="vv-au-field">
<label class="vv-au-label">Password</label>
<input class="vv-au-input" id="um-pass" type="password" placeholder="Initial password">
</div>` : ''}
<div class="vv-au-err" id="um-err"></div>
<div class="vv-au-modal-acts">
<button class="vv-au-btn" id="um-cancel">Cancel</button>
<button class="vv-au-btn prim" id="um-save">${u ? 'Save' : 'Create'}</button>
</div>`);
document.getElementById('um-cancel').onclick = _closeModal;
document.getElementById('um-save').onclick = () => {
const id = (document.getElementById('um-uid').value||'').trim();
const name = (document.getElementById('um-name').value||'').trim();
const email= (document.getElementById('um-email').value||'').trim();
const pass = document.getElementById('um-pass')?.value || '';
if (!id) { _showModalErr('um-err','Username required'); return; }
if (!email){ _showModalErr('um-err','Email required'); return; }
const btn = document.getElementById('um-save');
btn.disabled = true; btn.textContent = 'Saving…';
const done = r => {
if (!r.ok) { _showModalErr('um-err', r.error||'Save failed'); btn.disabled=false; btn.textContent=u?'Save':'Create'; return; }
_closeModal(); _loadUsers();
};
if (u) _post({ action:'lldap_update_user', uid:id, email, display_name:name }, done);
else _post({ action:'lldap_create_user', uid:id, email, display_name:name, password:pass }, done);
};
}
function _passModal(uid) {
_modal(`<h3>Change Password</h3>
<div class="vv-au-field">
<label class="vv-au-label">User</label>
<input class="vv-au-input" value="${_esc(uid)}" readonly>
</div>
<div class="vv-au-field">
<label class="vv-au-label">New Password</label>
<input class="vv-au-input" id="pw-pass" type="password" placeholder="New password" autofocus>
</div>
<div class="vv-au-err" id="pw-err"></div>
<div class="vv-au-modal-acts">
<button class="vv-au-btn" id="pw-cancel">Cancel</button>
<button class="vv-au-btn prim" id="pw-save">Set Password</button>
</div>`);
document.getElementById('pw-cancel').onclick = _closeModal;
document.getElementById('pw-save').onclick = () => {
const pass = document.getElementById('pw-pass').value;
if (!pass) { _showModalErr('pw-err','Password required'); return; }
const btn = document.getElementById('pw-save');
btn.disabled = true; btn.textContent = 'Saving…';
_post({ action:'lldap_set_password', uid, password:pass }, r => {
if (!r.ok) { _showModalErr('pw-err', r.error||'Failed'); btn.disabled=false; btn.textContent='Set Password'; return; }
_closeModal();
});
};
}
function _addToGroupModal(uid) {
const user = _users.find(u => u.id === uid);
const userGids= new Set((user?.groups||[]).map(g => g.id));
const available = _groups.filter(g => !userGids.has(g.id));
if (!available.length) { alert('User is already in all groups.'); return; }
const opts = available.map(g => `<option value="${g.id}">${_esc(g.displayName)}</option>`).join('');
_modal(`<h3>Add to Group</h3>
<div class="vv-au-field">
<label class="vv-au-label">User</label>
<input class="vv-au-input" value="${_esc(user?.displayName||uid)}" readonly>
</div>
<div class="vv-au-field">
<label class="vv-au-label">Group</label>
<select class="vv-au-select" id="ag-grp">${opts}</select>
</div>
<div class="vv-au-err" id="ag-err"></div>
<div class="vv-au-modal-acts">
<button class="vv-au-btn" id="ag-cancel">Cancel</button>
<button class="vv-au-btn prim" id="ag-save">Add</button>
</div>`);
document.getElementById('ag-cancel').onclick = _closeModal;
document.getElementById('ag-save').onclick = () => {
const gid = parseInt(document.getElementById('ag-grp').value);
const btn = document.getElementById('ag-save');
btn.disabled = true; btn.textContent = 'Adding…';
_post({ action:'lldap_add_to_group', uid, gid }, r => {
if (!r.ok) { _showModalErr('ag-err', r.error||'Failed'); btn.disabled=false; btn.textContent='Add'; return; }
_closeModal(); _loadUsers(); _loadGroups();
});
};
}
// User event delegation
document.getElementById('vv-au-panel-users').addEventListener('click', e => {
if (e.target.id === 'vv-au-user-add') { _userModal(null); return; }
const editBtn = e.target.closest('[data-user-edit]');
if (editBtn) { _userModal(editBtn.dataset.userEdit); return; }
const passBtn = e.target.closest('[data-user-pass]');
if (passBtn) { _passModal(passBtn.dataset.userPass); return; }
const grpBtn = e.target.closest('[data-user-grp]');
if (grpBtn) { _addToGroupModal(grpBtn.dataset.userGrp); return; }
const delBtn = e.target.closest('[data-user-del]');
if (delBtn) {
const uid = delBtn.dataset.userDel;
const user = _users.find(u => u.id === uid);
if (!confirm('Delete user "' + (user?.displayName||uid) + '"?')) return;
_post({ action:'lldap_delete_user', uid }, r => { if (r.ok) _loadUsers(); });
return;
}
const rmBadge = e.target.closest('[data-rm-from-group]');
if (rmBadge) {
const uid = rmBadge.dataset.rmFromGroup;
const gid = parseInt(rmBadge.dataset.gid);
const grp = _groups.find(g => g.id === gid);
if (!confirm('Remove from group "' + (grp?.displayName||gid) + '"?')) return;
_post({ action:'lldap_remove_from_group', uid, gid }, r => { if (r.ok) { _loadUsers(); _loadGroups(); } });
}
});
// ── Groups ────────────────────────────────────────────────────────────────────
function _loadGroups() {
const loading = document.getElementById('vv-au-groups-loading');
const list = document.getElementById('vv-au-groups-list');
const empty = document.getElementById('vv-au-groups-empty');
loading.style.display = 'block';
list.innerHTML = '';
empty.style.display = 'none';
_get('lldap_groups', r => {
loading.style.display = 'none';
if (!r.ok) { loading.innerHTML = '<span style="color:#ef5350;padding:10px;display:block">'+_esc(r.error)+'</span>'; loading.style.display='block'; return; }
_groups = r.groups || [];
if (!_groups.length) { empty.style.display = 'block'; return; }
list.innerHTML = _groups.map(g => {
const members = (g.users||[]).map(u =>
`<div class="vv-au-grp-member">
<span>${_esc(u.displayName||u.id)}</span>
<span class="vv-au-grp-member-rm" data-rm-user="${_esc(u.id)}" data-gid="${g.id}" title="Remove from group">✕</span>
</div>`
).join('');
return `<div class="vv-au-grp-row" data-grp="${g.id}">
<div class="vv-au-grp-acts">
<button class="vv-au-icon-btn del" data-group-del="${g.id}" title="Delete group">✕</button>
</div>
<div class="vv-au-grp-name">${_esc(g.displayName)}</div>
<div class="vv-au-grp-cnt">${(g.users||[]).length} member${(g.users||[]).length!==1?'s':''}</div>
</div>
<div class="vv-au-grp-members" id="gm-${g.id}">
${members || '<div style="font-size:11px;color:#333;padding:2px 0">No members</div>'}
</div>`;
}).join('');
});
}
// Group event delegation
document.getElementById('vv-au-panel-users').addEventListener('click', e => {
if (e.target.id === 'vv-au-group-add') {
_modal(`<h3>Add Group</h3>
<div class="vv-au-field">
<label class="vv-au-label">Group Name</label>
<input class="vv-au-input" id="gm-name" placeholder="admins" autofocus>
</div>
<div class="vv-au-err" id="gm-err"></div>
<div class="vv-au-modal-acts">
<button class="vv-au-btn" id="gm-cancel">Cancel</button>
<button class="vv-au-btn prim" id="gm-save">Create</button>
</div>`);
document.getElementById('gm-cancel').onclick = _closeModal;
document.getElementById('gm-save').onclick = () => {
const name = (document.getElementById('gm-name').value||'').trim();
if (!name) { _showModalErr('gm-err','Name required'); return; }
const btn = document.getElementById('gm-save');
btn.disabled = true; btn.textContent = 'Creating…';
_post({ action:'lldap_create_group', name }, r => {
if (!r.ok) { _showModalErr('gm-err', r.error||'Failed'); btn.disabled=false; btn.textContent='Create'; return; }
_closeModal(); _loadGroups();
});
};
return;
}
const grpRow = e.target.closest('[data-grp]');
if (grpRow && !e.target.closest('button') && !e.target.closest('.vv-au-icon-btn')) {
const panel = document.getElementById('gm-' + grpRow.dataset.grp);
if (panel) panel.classList.toggle('open');
return;
}
const delGrp = e.target.closest('[data-group-del]');
if (delGrp) {
e.stopPropagation();
const id = parseInt(delGrp.dataset.groupDel);
const grp = _groups.find(g => g.id === id);
if (!confirm('Delete group "' + (grp?.displayName||id) + '"?')) return;
_post({ action:'lldap_delete_group', id }, r => { if (r.ok) _loadGroups(); });
return;
}
const rmUser = e.target.closest('[data-rm-user]');
if (rmUser) {
e.stopPropagation();
const uid = rmUser.dataset.rmUser;
const gid = parseInt(rmUser.dataset.gid);
_post({ action:'lldap_remove_from_group', uid, gid }, r => { if (r.ok) { _loadUsers(); _loadGroups(); } });
}
});
// ── Access Control ────────────────────────────────────────────────────────────
function _loadAcl() {
const loading = document.getElementById('vv-au-acl-loading');
const tbl = document.getElementById('vv-au-acl-tbl');
const empty = document.getElementById('vv-au-acl-empty');
loading.style.display = 'block';
tbl.style.display = 'none';
empty.style.display = 'none';
_get('authelia_rules', r => {
loading.style.display = 'none';
if (!r.ok) { loading.innerHTML = '<span style="color:#ef5350;padding:10px;display:block">'+_esc(r.error)+'</span>'; loading.style.display='block'; return; }
_rules = r.rules || [];
_defaultPolicy = r.default_policy || 'deny';
const sel = document.getElementById('vv-au-ac-defpol');
if (sel) sel.value = _defaultPolicy;
if (!_rules.length) { empty.style.display = 'block'; return; }
tbl.style.display = 'table';
_renderAcl();
});
}
function _renderAcl() {
const count = document.getElementById('vv-au-acl-count');
count.textContent = _rules.length + ' rule' + (_rules.length !== 1 ? 's' : '');
const body = document.getElementById('vv-au-acl-body');
body.innerHTML = _rules.map((rule, i) => {
const domains = _normDomain(rule.domain).join(', ');
const subjects = _normSubject(rule.subject).join(', ');
const upBtn = i === 0 ? '<span style="width:18px;display:inline-block"></span>' :
`<button class="vv-au-icon-btn" data-rule-up="${i}" title="Move up">↑</button>`;
const dnBtn = i === _rules.length-1 ? '<span style="width:18px;display:inline-block"></span>' :
`<button class="vv-au-icon-btn" data-rule-dn="${i}" title="Move down">↓</button>`;
return `<tr>
<td class="vv-au-rule-num">${i+1}</td>
<td><div class="vv-au-domain">${_esc(domains)}</div></td>
<td>${_policyBadge(rule.policy)}</td>
<td style="font-size:10px;color:#555">${_esc(subjects||'—')}</td>
<td>
<div class="vv-au-rule-acts">
${upBtn}${dnBtn}
<button class="vv-au-icon-btn" data-rule-edit="${i}" title="Edit">✎</button>
<button class="vv-au-icon-btn del" data-rule-del="${i}" title="Delete">✕</button>
</div>
</td>
</tr>`;
}).join('');
}
function _ruleModal(idx) {
const rule = idx !== null ? _rules[idx] : null;
const domains = _normDomain(rule?.domain).join(', ');
const subjects = _normSubject(rule?.subject).join(', ');
const nets = (rule?.networks||[]).join(', ');
const resources= (rule?.resources||[]).join(', ');
const policyOpts = ['bypass','one_factor','two_factor','deny'].map(p =>
`<option value="${p}"${(rule?.policy||'two_factor')===p?' selected':''}>${p}</option>`
).join('');
_modal(`<h3>${rule ? 'Edit Rule' : 'Add Rule'}</h3>
<div class="vv-au-field">
<label class="vv-au-label">Domain</label>
<input class="vv-au-input" id="rm-domain" value="${_esc(domains)}" placeholder="*.example.com, example.com">
<div class="vv-au-hint">Comma-separated; wildcards supported</div>
</div>
<div class="vv-au-field">
<label class="vv-au-label">Policy</label>
<select class="vv-au-select" id="rm-policy">${policyOpts}</select>
</div>
<div class="vv-au-field">
<label class="vv-au-label">Subject <span style="color:#2a2a2a">(optional)</span></label>
<input class="vv-au-input" id="rm-subject" value="${_esc(subjects)}" placeholder="group:admins, user:john">
<div class="vv-au-hint">Comma-separated; prefix with group: or user:</div>
</div>
<div class="vv-au-adv-toggle" id="rm-adv-toggle">▶ Advanced (networks, resources)</div>
<div class="vv-au-adv-section" id="rm-adv">
<div class="vv-au-field">
<label class="vv-au-label">Networks <span style="color:#2a2a2a">(optional)</span></label>
<input class="vv-au-input" id="rm-networks" value="${_esc(nets)}" placeholder="192.168.1.0/24, 10.0.0.0/8">
<div class="vv-au-hint">Comma-separated CIDR ranges</div>
</div>
<div class="vv-au-field">
<label class="vv-au-label">Resources <span style="color:#2a2a2a">(optional)</span></label>
<input class="vv-au-input" id="rm-resources" value="${_esc(resources)}" placeholder="^/api, ^/admin">
<div class="vv-au-hint">Comma-separated regex patterns</div>
</div>
</div>
<div class="vv-au-err" id="rm-err"></div>
<div class="vv-au-modal-acts">
<button class="vv-au-btn" id="rm-cancel">Cancel</button>
<button class="vv-au-btn prim" id="rm-save">${rule ? 'Save' : 'Add Rule'}</button>
</div>`);
document.getElementById('rm-adv-toggle').addEventListener('click', () => {
const sec = document.getElementById('rm-adv');
const tog = document.getElementById('rm-adv-toggle');
const open = sec.classList.toggle('open');
tog.textContent = (open ? '▼' : '▶') + ' Advanced (networks, resources)';
});
document.getElementById('rm-cancel').onclick = _closeModal;
document.getElementById('rm-save').onclick = () => {
const domains = document.getElementById('rm-domain').value.split(',').map(s=>s.trim()).filter(Boolean);
if (!domains.length) { _showModalErr('rm-err','Domain required'); return; }
const subjects = document.getElementById('rm-subject').value.split(',').map(s=>s.trim()).filter(Boolean);
const networks = document.getElementById('rm-networks').value.split(',').map(s=>s.trim()).filter(Boolean);
const resources = document.getElementById('rm-resources').value.split(',').map(s=>s.trim()).filter(Boolean);
const newRule = {
domain: domains.length === 1 ? domains[0] : domains,
policy: document.getElementById('rm-policy').value,
};
if (subjects.length) newRule.subject = subjects.length === 1 ? subjects[0] : subjects;
if (networks.length) newRule.networks = networks;
if (resources.length) newRule.resources = resources;
if (idx !== null) _rules[idx] = newRule;
else _rules.push(newRule);
_closeModal();
_renderAcl();
};
}
// ACL event delegation
document.getElementById('vv-au-panel-acl').addEventListener('click', e => {
if (e.target.id === 'vv-au-rule-add') { _ruleModal(null); return; }
if (e.target.id === 'vv-au-ac-save') {
const dp = document.getElementById('vv-au-ac-defpol').value;
const btn = document.getElementById('vv-au-ac-save');
btn.disabled = true; btn.textContent = 'Saving…';
_post({ action:'authelia_save', rules: JSON.stringify(_rules), default_policy: dp }, r => {
btn.disabled = false; btn.textContent = 'Save & Restart Authelia';
if (!r.ok) { alert('Save failed: ' + (r.error||'unknown error')); return; }
// brief visual confirmation
btn.textContent = 'Saved ✓';
setTimeout(() => { btn.textContent = 'Save & Restart Authelia'; }, 2000);
});
return;
}
const editBtn = e.target.closest('[data-rule-edit]');
if (editBtn) { _ruleModal(parseInt(editBtn.dataset.ruleEdit)); return; }
const delBtn = e.target.closest('[data-rule-del]');
if (delBtn) {
const i = parseInt(delBtn.dataset.ruleDel);
if (!confirm('Delete this rule?')) return;
_rules.splice(i, 1);
_renderAcl();
return;
}
const upBtn = e.target.closest('[data-rule-up]');
if (upBtn) {
const i = parseInt(upBtn.dataset.ruleUp);
if (i > 0) { [_rules[i-1], _rules[i]] = [_rules[i], _rules[i-1]]; _renderAcl(); }
return;
}
const dnBtn = e.target.closest('[data-rule-dn]');
if (dnBtn) {
const i = parseInt(dnBtn.dataset.ruleDn);
if (i < _rules.length-1) { [_rules[i], _rules[i+1]] = [_rules[i+1], _rules[i]]; _renderAcl(); }
}
});
// ── Close modal on overlay click ──────────────────────────────────────────────
document.getElementById('vv-au-overlay').addEventListener('click', e => {
if (e.target === document.getElementById('vv-au-overlay')) _closeModal();
});
// ── Boot ──────────────────────────────────────────────────────────────────────
_loadProxies();
})();
</script>
+5 -1
View File
@@ -590,8 +590,12 @@ function vvDiskCol(disks) {
// ── Poll ────────────────────────────────────────────────────────────────────── // ── Poll ──────────────────────────────────────────────────────────────────────
let _vvFirstPoll = true;
function vvPollMonitor() { function vvPollMonitor() {
fetch('/plugins/varaverk/api/monitor.php') const _url = _vvFirstPoll ? '/plugins/varaverk/api/monitor.php' : '/plugins/varaverk/api/monitor.php?live=1';
_vvFirstPoll = false;
fetch(_url)
.then(r => r.json()) .then(r => r.json())
.then(d => { .then(d => {
+16 -11
View File
@@ -41,19 +41,24 @@
</CHANGES> </CHANGES>
<!-- <!--
── 1. Web files package (runs on every boot) ────────────────────────────────── ── 1. Web files symlink (runs on every boot) ──────────────────────────────────
unRAID runs `plugin install` on every .plg at boot, BEFORE the array mounts. Instead of extracting a .txz, we symlink the installed plugin web dir directly
The .txz lives on flash, so this installs the PHP/JS UI to RAM in time — and the to the workspace on flash. Changes to Plugin/unraid/ are live instantly — no
disks_mounted event hooks inside it are then present when the array starts, so package build, no sync step. /boot is always mounted before this runs.
cron is rebuilt automatically. No Method attr = treated as "install" = every boot.
Download is skipped when the .txz is already on flash with a matching SHA256, so
this also works offline / for a local install (copy the .txz into &cfgdir;).
──────────────────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────────────────────────
--> -->
<FILE Name="&cfgdir;/&pkg;" Run="/sbin/upgradepkg --install-new --reinstall"> <FILE Run="/bin/bash">
<URL>https://github.com/FailedProxy/Varaverk/releases/download/&version;/&pkg;</URL> <INLINE>
<SHA256>&sha256;</SHA256> <![CDATA[
#!/bin/bash
WEB_DIR="/usr/local/emhttp/plugins/varaverk"
SRC="/boot/config/plugins/varaverk/Plugin/unraid"
[[ -L "$WEB_DIR" ]] && rm -f "$WEB_DIR"
[[ -d "$WEB_DIR" ]] && rm -rf "$WEB_DIR"
ln -sf "$SRC" "$WEB_DIR"
echo "[Varaverk] web dir symlinked → $SRC"
]]>
</INLINE>
</FILE> </FILE>
<!-- <!--