mirror of
https://github.com/andrey271192/PCAtelegram_web.git
synced 2026-09-20 11:55:32 +00:00
feat: add branded login and editable admin credentials
This commit is contained in:
@@ -8,7 +8,6 @@ Installers generate /root/pcatelegram_web-admin.password for credentials.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import csv
|
||||
import fcntl
|
||||
import hashlib
|
||||
@@ -52,7 +51,10 @@ HOST = os.getenv("PCATELEGRAM_WEB_ADMIN_HOST", "0.0.0.0")
|
||||
PORT = int(os.getenv("PCATELEGRAM_WEB_ADMIN_PORT", "1984"))
|
||||
ADMIN_USER = os.getenv("PCATELEGRAM_WEB_ADMIN_USER", "admin")
|
||||
ADMIN_PASSWORD = os.getenv("PCATELEGRAM_WEB_ADMIN_PASSWORD", "")
|
||||
ADMIN_REALM = os.getenv("PCATELEGRAM_WEB_ADMIN_REALM", "PCAtelegram_web")
|
||||
ADMIN_AUTH_FILE = Path(os.getenv("PCATELEGRAM_WEB_ADMIN_AUTH_FILE", "/root/pcatelegram_web-admin.password"))
|
||||
SESSION_COOKIE = "pcatelegram_web_session"
|
||||
SESSION_TTL_SECONDS = 12 * 60 * 60
|
||||
SESSIONS: dict[str, float] = {}
|
||||
VERSION = "2.5.0"
|
||||
USER_RE = re.compile(r"^[A-Za-z0-9_.-]{1,48}$")
|
||||
LANG_RE = re.compile(r"^(en|ru)$")
|
||||
@@ -69,15 +71,238 @@ TRAFFIC_WINDOWS = {
|
||||
}
|
||||
|
||||
|
||||
def load_admin_password_file() -> str:
|
||||
path = Path(os.getenv("PCATELEGRAM_WEB_ADMIN_AUTH_FILE", "/root/pcatelegram_web-admin.password"))
|
||||
def load_admin_credentials() -> tuple[str, str]:
|
||||
if ADMIN_PASSWORD:
|
||||
return ADMIN_USER or "admin", ADMIN_PASSWORD
|
||||
user = "admin"
|
||||
password = "admin"
|
||||
try:
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
for line in ADMIN_AUTH_FILE.read_text(encoding="utf-8").splitlines():
|
||||
if line.startswith("user="):
|
||||
user = line.split("=", 1)[1].strip() or "admin"
|
||||
if line.startswith("password="):
|
||||
return line.split("=", 1)[1].strip()
|
||||
password = line.split("=", 1)[1].strip() or "admin"
|
||||
except OSError:
|
||||
return ""
|
||||
return ""
|
||||
pass
|
||||
return user, password
|
||||
|
||||
|
||||
def write_admin_credentials(username: str, password: str) -> None:
|
||||
ADMIN_AUTH_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = ADMIN_AUTH_FILE.with_suffix(".tmp")
|
||||
body = "\n".join([
|
||||
f"user={username}",
|
||||
f"password={password}",
|
||||
f"url=http://{public_host_for_notes()}:{PORT}/",
|
||||
"",
|
||||
])
|
||||
tmp.write_text(body, encoding="utf-8")
|
||||
os.chmod(tmp, 0o600)
|
||||
tmp.replace(ADMIN_AUTH_FILE)
|
||||
|
||||
|
||||
def public_host_for_notes() -> str:
|
||||
try:
|
||||
host = socket.gethostbyname(socket.gethostname())
|
||||
if host and not host.startswith("127."):
|
||||
return host
|
||||
except OSError:
|
||||
pass
|
||||
code, out, _ = run(["hostname", "-I"], timeout=3)
|
||||
if code == 0:
|
||||
first = (out.strip().split() or [""])[0]
|
||||
if first:
|
||||
return first
|
||||
return HOST if HOST != "0.0.0.0" else "127.0.0.1"
|
||||
|
||||
|
||||
def make_session() -> str:
|
||||
token = secrets.token_urlsafe(32)
|
||||
SESSIONS[token] = time.time() + SESSION_TTL_SECONDS
|
||||
return token
|
||||
|
||||
|
||||
def session_is_valid(token: str) -> bool:
|
||||
exp = SESSIONS.get(token)
|
||||
if not exp:
|
||||
return False
|
||||
if exp < time.time():
|
||||
SESSIONS.pop(token, None)
|
||||
return False
|
||||
SESSIONS[token] = time.time() + SESSION_TTL_SECONDS
|
||||
return True
|
||||
|
||||
|
||||
def clear_session(token: str) -> None:
|
||||
if token:
|
||||
SESSIONS.pop(token, None)
|
||||
|
||||
|
||||
def login_page() -> bytes:
|
||||
return """<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>PCAtelegram_web Login</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #efe6df;
|
||||
--card: #fffafd;
|
||||
--text: #251f1d;
|
||||
--muted: #665b55;
|
||||
--line: #d8c8bd;
|
||||
--brand: #9a5f00;
|
||||
--brand-dark: #805000;
|
||||
--soft: #ffddb8;
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background:
|
||||
radial-gradient(circle at 18% 16%, rgba(154, 95, 0, .10), transparent 30%),
|
||||
linear-gradient(135deg, #efe6df, #f7efe9 48%, #eaded6);
|
||||
color: var(--text);
|
||||
padding: 24px;
|
||||
}
|
||||
.login-card {
|
||||
width: min(560px, 100%);
|
||||
border: 1px solid rgba(64, 49, 43, .22);
|
||||
border-radius: 28px;
|
||||
background: color-mix(in srgb, var(--card) 96%, white);
|
||||
box-shadow: 0 30px 80px rgba(33, 28, 25, .22);
|
||||
padding: clamp(26px, 5vw, 44px);
|
||||
}
|
||||
.brand {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.mark {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 20px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #b87306, #805000);
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,.24), 0 14px 30px rgba(128,80,0,.24);
|
||||
font-weight: 900;
|
||||
letter-spacing: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(28px, 5vw, 40px);
|
||||
line-height: 1.08;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.subtitle { margin-top: 6px; color: var(--muted); font-size: 15px; }
|
||||
form { display: grid; gap: 18px; }
|
||||
label {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
color: var(--muted);
|
||||
font-weight: 800;
|
||||
font-size: 14px;
|
||||
}
|
||||
input {
|
||||
min-height: 58px;
|
||||
border: 2px solid var(--line);
|
||||
border-radius: 16px;
|
||||
background: #fffbff;
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
font-size: 20px;
|
||||
padding: 0 18px;
|
||||
outline: none;
|
||||
}
|
||||
input:focus {
|
||||
border-color: var(--brand);
|
||||
box-shadow: 0 0 0 4px rgba(154, 95, 0, .13);
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 14px;
|
||||
margin-top: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
button {
|
||||
min-height: 56px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
padding: 0 32px;
|
||||
font: inherit;
|
||||
font-size: 18px;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
}
|
||||
.soft { background: var(--soft); color: var(--text); }
|
||||
.primary { background: var(--brand); color: white; min-width: 150px; }
|
||||
.primary:hover { background: var(--brand-dark); }
|
||||
.hint { margin-top: 18px; color: var(--muted); font-size: 13px; }
|
||||
.error { min-height: 22px; color: #b42318; font-weight: 800; }
|
||||
@media (max-width: 520px) {
|
||||
body { padding: 14px; }
|
||||
.login-card { border-radius: 20px; }
|
||||
.actions button { flex: 1; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="login-card">
|
||||
<div class="brand">
|
||||
<div class="mark">PCA</div>
|
||||
<div>
|
||||
<h1>PCAtelegram_web</h1>
|
||||
<div class="subtitle">Web admin panel</div>
|
||||
</div>
|
||||
</div>
|
||||
<form id="loginForm">
|
||||
<label>Имя пользователя
|
||||
<input name="username" autocomplete="username" value="admin" required autofocus>
|
||||
</label>
|
||||
<label>Пароль
|
||||
<input name="password" type="password" autocomplete="current-password" value="admin" required>
|
||||
</label>
|
||||
<div id="error" class="error"></div>
|
||||
<div class="actions">
|
||||
<button type="reset" class="soft">Очистить</button>
|
||||
<button type="submit" class="primary">Войти</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="hint">По умолчанию: admin / admin. Смените пароль в Settings после входа.</p>
|
||||
</main>
|
||||
<script>
|
||||
document.getElementById("loginForm").addEventListener("submit", async function (ev) {
|
||||
ev.preventDefault();
|
||||
const error = document.getElementById("error");
|
||||
error.textContent = "";
|
||||
const form = new FormData(ev.currentTarget);
|
||||
const res = await fetch("/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
username: String(form.get("username") || ""),
|
||||
password: String(form.get("password") || "")
|
||||
})
|
||||
});
|
||||
if (res.ok) {
|
||||
window.location.assign("/");
|
||||
} else {
|
||||
error.textContent = "Неверный логин или пароль";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
""".encode("utf-8")
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
@@ -1297,35 +1522,76 @@ class AdminHandler(BaseHTTPRequestHandler):
|
||||
def log_message(self, fmt: str, *args: Any) -> None:
|
||||
print("%s - %s" % (self.address_string(), fmt % args))
|
||||
|
||||
def auth_enabled(self) -> bool:
|
||||
return bool(ADMIN_PASSWORD or load_admin_password_file())
|
||||
def cookie_session(self) -> str:
|
||||
raw = self.headers.get("Cookie", "")
|
||||
for item in raw.split(";"):
|
||||
item = item.strip()
|
||||
if item.startswith(SESSION_COOKIE + "="):
|
||||
return item.split("=", 1)[1]
|
||||
return ""
|
||||
|
||||
def is_authorized(self) -> bool:
|
||||
if not self.auth_enabled():
|
||||
return True
|
||||
header = self.headers.get("Authorization", "")
|
||||
if not header.startswith("Basic "):
|
||||
return False
|
||||
try:
|
||||
decoded = base64.b64decode(header[6:].strip(), validate=True).decode("utf-8")
|
||||
username, password = decoded.split(":", 1)
|
||||
except Exception:
|
||||
return False
|
||||
expected_password = ADMIN_PASSWORD or load_admin_password_file()
|
||||
return hmac.compare_digest(username, ADMIN_USER) and hmac.compare_digest(password, expected_password)
|
||||
return session_is_valid(self.cookie_session())
|
||||
|
||||
def require_auth(self) -> bool:
|
||||
if self.is_authorized():
|
||||
return True
|
||||
body = b"Authentication required\n"
|
||||
self.send_response(401)
|
||||
self.send_header("WWW-Authenticate", f'Basic realm="{ADMIN_REALM}"')
|
||||
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
||||
self.send_error_json(401, "unauthorized")
|
||||
return False
|
||||
|
||||
def send_login_page(self) -> None:
|
||||
body = login_page()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def set_session_cookie(self, token: str) -> None:
|
||||
self.send_header(
|
||||
"Set-Cookie",
|
||||
f"{SESSION_COOKIE}={token}; Path=/; HttpOnly; SameSite=Lax; Max-Age={SESSION_TTL_SECONDS}",
|
||||
)
|
||||
|
||||
def clear_session_cookie(self) -> None:
|
||||
self.send_header("Set-Cookie", f"{SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0")
|
||||
|
||||
def credentials_match(self, username: str, password: str) -> bool:
|
||||
expected_user, expected_password = load_admin_credentials()
|
||||
return hmac.compare_digest(username, expected_user) and hmac.compare_digest(password, expected_password)
|
||||
|
||||
def handle_login(self) -> None:
|
||||
try:
|
||||
body = self.read_json_body()
|
||||
except Exception:
|
||||
self.send_error_json(400, "bad request")
|
||||
return
|
||||
username = str(body.get("username", "")).strip()
|
||||
password = str(body.get("password", ""))
|
||||
if not self.credentials_match(username, password):
|
||||
self.send_error_json(401, "invalid credentials")
|
||||
return
|
||||
token = make_session()
|
||||
payload = json.dumps({"ok": True, "data": {"user": username}}, ensure_ascii=False).encode("utf-8")
|
||||
self.send_response(200)
|
||||
self.set_session_cookie(token)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.send_header("Content-Length", str(len(payload)))
|
||||
self.end_headers()
|
||||
self.wfile.write(payload)
|
||||
|
||||
def handle_logout(self) -> None:
|
||||
clear_session(self.cookie_session())
|
||||
body = b'{"ok": true}\n'
|
||||
self.send_response(200)
|
||||
self.clear_session_cookie()
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
return False
|
||||
|
||||
def send_json(self, payload: Any, status: int = 200) -> None:
|
||||
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
@@ -1611,6 +1877,28 @@ class AdminHandler(BaseHTTPRequestHandler):
|
||||
self.send_error_json(400, str(exc))
|
||||
return
|
||||
self.send_json({"ok": True, "data": lang_payload})
|
||||
elif path == "/api/settings/auth":
|
||||
current_password = str(body.get("current_password", ""))
|
||||
username = str(body.get("username", "")).strip()
|
||||
new_password = str(body.get("new_password", ""))
|
||||
current_user, current_expected = load_admin_credentials()
|
||||
if not hmac.compare_digest(current_password, current_expected):
|
||||
self.send_error_json(403, "current password is wrong")
|
||||
return
|
||||
if not USER_RE.match(username):
|
||||
self.send_error_json(400, "invalid username")
|
||||
return
|
||||
if len(new_password) < 4:
|
||||
self.send_error_json(400, "password must be at least 4 chars")
|
||||
return
|
||||
try:
|
||||
write_admin_credentials(username, new_password)
|
||||
except Exception as exc:
|
||||
self.send_error_json(500, f"failed to save credentials: {exc}")
|
||||
return
|
||||
self.send_json({"ok": True, "data": {"user": username, "changed": username != current_user or new_password != current_expected}})
|
||||
elif path == "/api/auth/logout":
|
||||
self.handle_logout()
|
||||
elif path.startswith("/api/services/") and path.endswith("/restart"):
|
||||
service = path[len("/api/services/"):-len("/restart")]
|
||||
allowed = {"telemt", "nginx", "pcatelegram_web-bot", "pcatelegram_web-stats"}
|
||||
@@ -1678,18 +1966,25 @@ class AdminHandler(BaseHTTPRequestHandler):
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_GET(self) -> None:
|
||||
if not self.require_auth():
|
||||
return
|
||||
parsed = urllib.parse.urlparse(self.path)
|
||||
if not self.is_authorized():
|
||||
if parsed.path.startswith("/api/"):
|
||||
self.send_error_json(401, "unauthorized")
|
||||
else:
|
||||
self.send_login_page()
|
||||
return
|
||||
if parsed.path.startswith("/api/"):
|
||||
self.route_get_api(parsed)
|
||||
else:
|
||||
self.send_static(parsed)
|
||||
|
||||
def do_POST(self) -> None:
|
||||
parsed = urllib.parse.urlparse(self.path)
|
||||
if parsed.path == "/api/auth/login":
|
||||
self.handle_login()
|
||||
return
|
||||
if not self.require_auth():
|
||||
return
|
||||
parsed = urllib.parse.urlparse(self.path)
|
||||
if parsed.path.startswith("/api/"):
|
||||
self.route_post_api(parsed)
|
||||
else:
|
||||
|
||||
@@ -100,6 +100,12 @@ const i18n = {
|
||||
panelLanguage: "Panel language",
|
||||
theme: "Theme",
|
||||
bindAddress: "Bind address",
|
||||
authSettingsTitle: "Login credentials",
|
||||
authCurrentPassword: "Current password",
|
||||
authUsername: "Username",
|
||||
authNewPassword: "New password",
|
||||
authSave: "Save login",
|
||||
authSaved: "Login updated",
|
||||
dashboard: "Dashboard",
|
||||
noKeys: "No keys yet",
|
||||
noBackups: "No backups yet",
|
||||
@@ -327,6 +333,12 @@ const i18n = {
|
||||
panelLanguage: "Язык панели",
|
||||
theme: "Тема",
|
||||
bindAddress: "Адрес привязки",
|
||||
authSettingsTitle: "Данные входа",
|
||||
authCurrentPassword: "Текущий пароль",
|
||||
authUsername: "Логин",
|
||||
authNewPassword: "Новый пароль",
|
||||
authSave: "Сохранить вход",
|
||||
authSaved: "Данные входа обновлены",
|
||||
dashboard: "Обзор",
|
||||
noKeys: "Ключей пока нет",
|
||||
noBackups: "Бекапов пока нет",
|
||||
@@ -636,6 +648,31 @@ async function setLanguage(lang) {
|
||||
}
|
||||
}
|
||||
|
||||
async function updateAuthSettings(eventObj) {
|
||||
eventObj.preventDefault();
|
||||
const form = eventObj.currentTarget;
|
||||
const btn = form.querySelector("button[type='submit']");
|
||||
const body = {
|
||||
current_password: form.current_password.value,
|
||||
username: form.username.value.trim(),
|
||||
new_password: form.new_password.value,
|
||||
};
|
||||
btn.disabled = true;
|
||||
try {
|
||||
await api("/api/settings/auth", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
form.current_password.value = "";
|
||||
form.new_password.value = "";
|
||||
toast(t("authSaved"));
|
||||
} catch (err) {
|
||||
toast(err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function setPage(page, push = true) {
|
||||
const next = $(`[data-page="${page}"]`) ? page : "dashboard";
|
||||
state.page = next;
|
||||
@@ -1744,6 +1781,7 @@ $("#createBackupBtn").addEventListener("click", createBackup);
|
||||
$("#loadLogsBtn").addEventListener("click", loadLogs);
|
||||
$("#repairStatsBtn").addEventListener("click", repairStats);
|
||||
$("#collectStatsBtn").addEventListener("click", collectStats);
|
||||
$("#authSettingsForm").addEventListener("submit", updateAuthSettings);
|
||||
window.addEventListener("hashchange", () => setPage((location.hash || "#dashboard").slice(1), false));
|
||||
|
||||
setPage((location.hash || "#dashboard").slice(1), false);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
document.documentElement.dataset.theme = theme;
|
||||
}());
|
||||
</script>
|
||||
<link rel="stylesheet" href="/styles.css?v=2.5.0-admin18">
|
||||
<link rel="stylesheet" href="/styles.css?v=2.5.0-admin19">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell">
|
||||
@@ -342,6 +342,22 @@
|
||||
<strong id="settingsBind">127.0.0.1:1984</strong>
|
||||
</div>
|
||||
</div>
|
||||
<form id="authSettingsForm" class="settings-form">
|
||||
<h3 data-i18n="authSettingsTitle">Login credentials</h3>
|
||||
<label>
|
||||
<span data-i18n="authCurrentPassword">Current password</span>
|
||||
<input name="current_password" type="password" autocomplete="current-password" required>
|
||||
</label>
|
||||
<label>
|
||||
<span data-i18n="authUsername">Username</span>
|
||||
<input name="username" autocomplete="username" value="admin" required maxlength="48">
|
||||
</label>
|
||||
<label>
|
||||
<span data-i18n="authNewPassword">New password</span>
|
||||
<input name="new_password" type="password" autocomplete="new-password" required minlength="4">
|
||||
</label>
|
||||
<button type="submit" data-i18n="authSave">Save login</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
@@ -393,6 +409,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/app.js?v=2.5.0-admin18" type="module"></script>
|
||||
<script src="/app.js?v=2.5.0-admin19" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1253,6 +1253,33 @@ td small {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--panel-soft);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.settings-form h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.settings-form label {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
color: var(--muted);
|
||||
font-weight: 800;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.settings-form button {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.backup-item span,
|
||||
.event small {
|
||||
display: block;
|
||||
|
||||
@@ -25,13 +25,7 @@ ensure_admin_web_password() {
|
||||
elif [ -f "$ADMIN_WEB_AUTH_FILE" ] && [ -s "$ADMIN_WEB_AUTH_FILE" ]; then
|
||||
pw=$(sed -n 's/^password=//p' "$ADMIN_WEB_AUTH_FILE" | head -1)
|
||||
fi
|
||||
if [ -z "$pw" ]; then
|
||||
if command -v openssl >/dev/null 2>&1; then
|
||||
pw=$(openssl rand -base64 24 | tr -d '\n')
|
||||
else
|
||||
pw=$(head -c 24 /dev/urandom | base64 | tr -d '\n')
|
||||
fi
|
||||
fi
|
||||
[ -n "$pw" ] || pw="admin"
|
||||
umask 077
|
||||
cat > "$ADMIN_WEB_AUTH_FILE" <<EOF
|
||||
user=${PCATELEGRAM_WEB_ADMIN_USER:-admin}
|
||||
|
||||
@@ -748,13 +748,7 @@ ensure_admin_web_password() {
|
||||
pw=$(sed -n 's/^password=//p' "$ADMIN_WEB_AUTH_FILE" | head -1)
|
||||
fi
|
||||
|
||||
if [ -z "$pw" ]; then
|
||||
if command -v openssl >/dev/null 2>&1; then
|
||||
pw=$(openssl rand -base64 24 | tr -d '\n')
|
||||
else
|
||||
pw=$(head -c 24 /dev/urandom | base64 | tr -d '\n')
|
||||
fi
|
||||
fi
|
||||
[ -n "$pw" ] || pw="admin"
|
||||
|
||||
umask 077
|
||||
cat > "$ADMIN_WEB_AUTH_FILE" <<EOF
|
||||
|
||||
Reference in New Issue
Block a user