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
|
return jsonify({"error": "server not configured"}), 503
|
||||||
|
|
||||||
ip = _client_ip()
|
ip = _client_ip()
|
||||||
|
username, email, note = _extract_request_payload()
|
||||||
|
|
||||||
|
rate_key = ip
|
||||||
|
if username:
|
||||||
|
rate_key = f"{ip}:{username}"
|
||||||
if not rate_limit_allow(
|
if not rate_limit_allow(
|
||||||
ip,
|
rate_key,
|
||||||
key="access_request_submit",
|
key="access_request_submit",
|
||||||
limit=settings.ACCESS_REQUEST_SUBMIT_RATE_LIMIT,
|
limit=settings.ACCESS_REQUEST_SUBMIT_RATE_LIMIT,
|
||||||
window_sec=settings.ACCESS_REQUEST_SUBMIT_RATE_WINDOW_SEC,
|
window_sec=settings.ACCESS_REQUEST_SUBMIT_RATE_WINDOW_SEC,
|
||||||
):
|
):
|
||||||
return jsonify({"error": "rate limited"}), 429
|
return jsonify({"error": "rate limited"}), 429
|
||||||
|
|
||||||
username, email, note = _extract_request_payload()
|
|
||||||
if not username:
|
if not username:
|
||||||
return jsonify({"error": "username is required"}), 400
|
return jsonify({"error": "username is required"}), 400
|
||||||
|
|
||||||
@ -136,6 +140,15 @@ def register(app) -> None:
|
|||||||
if not code:
|
if not code:
|
||||||
return jsonify({"error": "request_code is required"}), 400
|
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:
|
try:
|
||||||
with connect() as conn:
|
with connect() as conn:
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
|
|||||||
@ -22,37 +22,48 @@ def register(app) -> None:
|
|||||||
username = g.keycloak_username
|
username = g.keycloak_username
|
||||||
keycloak_email = g.keycloak_email or ""
|
keycloak_email = g.keycloak_email or ""
|
||||||
mailu_app_password = ""
|
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"
|
mailu_status = "ready"
|
||||||
jellyfin_status = "ready"
|
jellyfin_status = "ready"
|
||||||
|
|
||||||
if not admin_client().ready():
|
if not admin_client().ready():
|
||||||
mailu_status = "server not configured"
|
mailu_status = "server not configured"
|
||||||
jellyfin_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(
|
return jsonify(
|
||||||
{
|
{
|
||||||
@ -81,9 +92,10 @@ def register(app) -> None:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return jsonify({"error": "failed to update mail password"}), 502
|
return jsonify({"error": "failed to update mail password"}), 502
|
||||||
|
|
||||||
|
sync_enabled = bool(settings.MAILU_SYNC_URL)
|
||||||
sync_ok = False
|
sync_ok = False
|
||||||
sync_error = ""
|
sync_error = ""
|
||||||
if settings.MAILU_SYNC_URL:
|
if sync_enabled:
|
||||||
try:
|
try:
|
||||||
with httpx.Client(timeout=30) as client:
|
with httpx.Client(timeout=30) as client:
|
||||||
resp = client.post(
|
resp = client.post(
|
||||||
@ -96,4 +108,11 @@ def register(app) -> None:
|
|||||||
except Exception:
|
except Exception:
|
||||||
sync_error = "sync request failed"
|
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 = "";
|
mailu.error = "";
|
||||||
jellyfin.error = "";
|
jellyfin.error = "";
|
||||||
try {
|
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}`);
|
if (!resp.ok) throw new Error(`status ${resp.status}`);
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
mailu.status = data.mailu?.status || "ready";
|
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 || "";
|
mailu.currentPassword = data.mailu?.app_password || "";
|
||||||
jellyfin.status = data.jellyfin?.status || "ready";
|
jellyfin.status = data.jellyfin?.status || "ready";
|
||||||
jellyfin.username = data.jellyfin?.username || auth.username;
|
jellyfin.username = data.jellyfin?.username || auth.username;
|
||||||
@ -239,7 +242,10 @@ async function refreshAdminRequests() {
|
|||||||
admin.error = "";
|
admin.error = "";
|
||||||
admin.loading = true;
|
admin.loading = true;
|
||||||
try {
|
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) {
|
if (resp.status === 403) {
|
||||||
admin.enabled = false;
|
admin.enabled = false;
|
||||||
admin.requests = [];
|
admin.requests = [];
|
||||||
@ -267,9 +273,22 @@ async function rotateMailu() {
|
|||||||
const data = await resp.json().catch(() => ({}));
|
const data = await resp.json().catch(() => ({}));
|
||||||
if (!resp.ok) throw new Error(data.error || `status ${resp.status}`);
|
if (!resp.ok) throw new Error(data.error || `status ${resp.status}`);
|
||||||
mailu.newPassword = data.password || "";
|
mailu.newPassword = data.password || "";
|
||||||
mailu.currentPassword = mailu.newPassword;
|
if (mailu.newPassword) {
|
||||||
mailu.revealPassword = true;
|
mailu.currentPassword = mailu.newPassword;
|
||||||
mailu.status = "updated";
|
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) {
|
} catch (err) {
|
||||||
mailu.error = err.message || "Rotation failed";
|
mailu.error = err.message || "Rotation failed";
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -148,6 +148,7 @@ async function submit() {
|
|||||||
const resp = await fetch("/api/access/request", {
|
const resp = await fetch("/api/access/request", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
cache: "no-store",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
username: form.username.trim(),
|
username: form.username.trim(),
|
||||||
email: form.email.trim(),
|
email: form.email.trim(),
|
||||||
@ -200,6 +201,7 @@ async function checkStatus() {
|
|||||||
const resp = await fetch("/api/access/request/status", {
|
const resp = await fetch("/api/access/request/status", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
cache: "no-store",
|
||||||
body: JSON.stringify({ request_code: statusForm.request_code.trim() }),
|
body: JSON.stringify({ request_code: statusForm.request_code.trim() }),
|
||||||
});
|
});
|
||||||
const data = await resp.json().catch(() => ({}));
|
const data = await resp.json().catch(() => ({}));
|
||||||
@ -208,6 +210,8 @@ async function checkStatus() {
|
|||||||
onboardingUrl.value = data.onboarding_url || "";
|
onboardingUrl.value = data.onboarding_url || "";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.message || "Failed to check status";
|
error.value = err.message || "Failed to check status";
|
||||||
|
status.value = "unknown";
|
||||||
|
onboardingUrl.value = "";
|
||||||
} finally {
|
} finally {
|
||||||
checking.value = false;
|
checking.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user