portal: fix access requests and account status
This commit is contained in:
parent
8b5a8bda3d
commit
8edc680503
@ -47,15 +47,19 @@ def register(app) -> None:
|
||||
return jsonify({"error": "server not configured"}), 503
|
||||
|
||||
ip = _client_ip()
|
||||
username, email, note = _extract_request_payload()
|
||||
|
||||
rate_key = ip
|
||||
if username:
|
||||
rate_key = f"{ip}:{username}"
|
||||
if not rate_limit_allow(
|
||||
ip,
|
||||
rate_key,
|
||||
key="access_request_submit",
|
||||
limit=settings.ACCESS_REQUEST_SUBMIT_RATE_LIMIT,
|
||||
window_sec=settings.ACCESS_REQUEST_SUBMIT_RATE_WINDOW_SEC,
|
||||
):
|
||||
return jsonify({"error": "rate limited"}), 429
|
||||
|
||||
username, email, note = _extract_request_payload()
|
||||
if not username:
|
||||
return jsonify({"error": "username is required"}), 400
|
||||
|
||||
@ -136,6 +140,15 @@ def register(app) -> None:
|
||||
if not code:
|
||||
return jsonify({"error": "request_code is required"}), 400
|
||||
|
||||
# Additional per-code limiter to avoid global NAT rate-limit blowups.
|
||||
if not rate_limit_allow(
|
||||
f"{ip}:{code}",
|
||||
key="access_request_status_code",
|
||||
limit=max(20, settings.ACCESS_REQUEST_STATUS_RATE_LIMIT),
|
||||
window_sec=settings.ACCESS_REQUEST_STATUS_RATE_WINDOW_SEC,
|
||||
):
|
||||
return jsonify({"error": "rate limited"}), 429
|
||||
|
||||
try:
|
||||
with connect() as conn:
|
||||
row = conn.execute(
|
||||
|
||||
@ -22,37 +22,48 @@ def register(app) -> None:
|
||||
username = g.keycloak_username
|
||||
keycloak_email = g.keycloak_email or ""
|
||||
mailu_app_password = ""
|
||||
|
||||
if admin_client().ready() and username:
|
||||
try:
|
||||
user = admin_client().find_user(username) or {}
|
||||
user_id = user.get("id") or ""
|
||||
if user_id:
|
||||
full = admin_client().get_user(str(user_id))
|
||||
if not keycloak_email:
|
||||
keycloak_email = str(full.get("email") or "")
|
||||
attrs = full.get("attributes") or {}
|
||||
if isinstance(attrs, dict):
|
||||
raw_pw = attrs.get("mailu_app_password")
|
||||
if isinstance(raw_pw, list) and raw_pw:
|
||||
mailu_app_password = str(raw_pw[0])
|
||||
elif isinstance(raw_pw, str):
|
||||
mailu_app_password = raw_pw
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
mailu_username = ""
|
||||
if keycloak_email and keycloak_email.lower().endswith(f"@{settings.MAILU_DOMAIN.lower()}"):
|
||||
mailu_username = keycloak_email
|
||||
elif username:
|
||||
mailu_username = f"{username}@{settings.MAILU_DOMAIN}"
|
||||
|
||||
mailu_status = "ready"
|
||||
jellyfin_status = "ready"
|
||||
|
||||
if not admin_client().ready():
|
||||
mailu_status = "server not configured"
|
||||
jellyfin_status = "server not configured"
|
||||
elif username:
|
||||
try:
|
||||
user = admin_client().find_user(username) or {}
|
||||
if not keycloak_email:
|
||||
keycloak_email = str(user.get("email") or "")
|
||||
|
||||
attrs = user.get("attributes") if isinstance(user, dict) else None
|
||||
if isinstance(attrs, dict):
|
||||
raw_pw = attrs.get("mailu_app_password")
|
||||
if isinstance(raw_pw, list) and raw_pw:
|
||||
mailu_app_password = str(raw_pw[0])
|
||||
elif isinstance(raw_pw, str) and raw_pw:
|
||||
mailu_app_password = raw_pw
|
||||
|
||||
user_id = user.get("id") if isinstance(user, dict) else None
|
||||
if user_id and (not keycloak_email or not mailu_app_password):
|
||||
full = admin_client().get_user(str(user_id))
|
||||
if not keycloak_email:
|
||||
keycloak_email = str(full.get("email") or "")
|
||||
if not mailu_app_password:
|
||||
attrs = full.get("attributes") or {}
|
||||
if isinstance(attrs, dict):
|
||||
raw_pw = attrs.get("mailu_app_password")
|
||||
if isinstance(raw_pw, list) and raw_pw:
|
||||
mailu_app_password = str(raw_pw[0])
|
||||
elif isinstance(raw_pw, str) and raw_pw:
|
||||
mailu_app_password = raw_pw
|
||||
except Exception:
|
||||
mailu_status = "keycloak admin error"
|
||||
jellyfin_status = "keycloak admin error"
|
||||
|
||||
mailu_username = ""
|
||||
if keycloak_email and keycloak_email.lower().endswith(f"@{settings.MAILU_DOMAIN.lower()}"):
|
||||
mailu_username = keycloak_email
|
||||
elif username:
|
||||
mailu_username = f"{username}@{settings.MAILU_DOMAIN}"
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
@ -81,9 +92,10 @@ def register(app) -> None:
|
||||
except Exception:
|
||||
return jsonify({"error": "failed to update mail password"}), 502
|
||||
|
||||
sync_enabled = bool(settings.MAILU_SYNC_URL)
|
||||
sync_ok = False
|
||||
sync_error = ""
|
||||
if settings.MAILU_SYNC_URL:
|
||||
if sync_enabled:
|
||||
try:
|
||||
with httpx.Client(timeout=30) as client:
|
||||
resp = client.post(
|
||||
@ -96,4 +108,11 @@ def register(app) -> None:
|
||||
except Exception:
|
||||
sync_error = "sync request failed"
|
||||
|
||||
return jsonify({"password": password, "sync_ok": sync_ok, "sync_error": sync_error})
|
||||
return jsonify(
|
||||
{
|
||||
"password": password,
|
||||
"sync_enabled": sync_enabled,
|
||||
"sync_ok": sync_ok,
|
||||
"sync_error": sync_error,
|
||||
}
|
||||
)
|
||||
|
||||
@ -219,11 +219,14 @@ async function refreshOverview() {
|
||||
mailu.error = "";
|
||||
jellyfin.error = "";
|
||||
try {
|
||||
const resp = await authFetch("/api/account/overview", { headers: { Accept: "application/json" } });
|
||||
const resp = await authFetch("/api/account/overview", {
|
||||
headers: { Accept: "application/json" },
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!resp.ok) throw new Error(`status ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
mailu.status = data.mailu?.status || "ready";
|
||||
mailu.username = data.mailu?.username || auth.username;
|
||||
mailu.username = data.mailu?.username || auth.email || auth.username;
|
||||
mailu.currentPassword = data.mailu?.app_password || "";
|
||||
jellyfin.status = data.jellyfin?.status || "ready";
|
||||
jellyfin.username = data.jellyfin?.username || auth.username;
|
||||
@ -239,7 +242,10 @@ async function refreshAdminRequests() {
|
||||
admin.error = "";
|
||||
admin.loading = true;
|
||||
try {
|
||||
const resp = await authFetch("/api/admin/access/requests", { headers: { Accept: "application/json" } });
|
||||
const resp = await authFetch("/api/admin/access/requests", {
|
||||
headers: { Accept: "application/json" },
|
||||
cache: "no-store",
|
||||
});
|
||||
if (resp.status === 403) {
|
||||
admin.enabled = false;
|
||||
admin.requests = [];
|
||||
@ -267,9 +273,22 @@ async function rotateMailu() {
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) throw new Error(data.error || `status ${resp.status}`);
|
||||
mailu.newPassword = data.password || "";
|
||||
mailu.currentPassword = mailu.newPassword;
|
||||
mailu.revealPassword = true;
|
||||
mailu.status = "updated";
|
||||
if (mailu.newPassword) {
|
||||
mailu.currentPassword = mailu.newPassword;
|
||||
mailu.revealPassword = true;
|
||||
}
|
||||
const syncEnabled = Boolean(data.sync_enabled);
|
||||
const syncOk = Boolean(data.sync_ok);
|
||||
const syncError = data.sync_error || "";
|
||||
if (!syncEnabled) {
|
||||
mailu.status = "updated";
|
||||
mailu.error = "Mail sync is not configured; password may not take effect until an admin sync runs.";
|
||||
} else if (!syncOk) {
|
||||
mailu.status = "sync pending";
|
||||
mailu.error = syncError || "Mail sync did not confirm success yet. Try again in a moment.";
|
||||
} else {
|
||||
mailu.status = "updated";
|
||||
}
|
||||
} catch (err) {
|
||||
mailu.error = err.message || "Rotation failed";
|
||||
} finally {
|
||||
|
||||
@ -148,6 +148,7 @@ async function submit() {
|
||||
const resp = await fetch("/api/access/request", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
cache: "no-store",
|
||||
body: JSON.stringify({
|
||||
username: form.username.trim(),
|
||||
email: form.email.trim(),
|
||||
@ -200,6 +201,7 @@ async function checkStatus() {
|
||||
const resp = await fetch("/api/access/request/status", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
cache: "no-store",
|
||||
body: JSON.stringify({ request_code: statusForm.request_code.trim() }),
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
@ -208,6 +210,8 @@ async function checkStatus() {
|
||||
onboardingUrl.value = data.onboarding_url || "";
|
||||
} catch (err) {
|
||||
error.value = err.message || "Failed to check status";
|
||||
status.value = "unknown";
|
||||
onboardingUrl.value = "";
|
||||
} finally {
|
||||
checking.value = false;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user