250 lines
9.3 KiB
Python

from __future__ import annotations
import socket
import time
from urllib.parse import quote
from typing import Any
import httpx
from flask import jsonify, g, request
from .. import settings
from .. import ariadne_client
from ..db import connect
from ..keycloak import admin_client, require_auth, require_account_access
from ..nextcloud_mail_sync import trigger as trigger_nextcloud_mail_sync
from ..utils import random_password
from ..firefly_user_sync import trigger as trigger_firefly_user_sync
from ..wger_user_sync import trigger as trigger_wger_user_sync
def _tcp_check(host: str, port: int, timeout_sec: float) -> bool:
if not host or port <= 0:
return False
try:
with socket.create_connection((host, port), timeout=timeout_sec):
return True
except OSError:
return False
def register_account_actions(app) -> None:
"""Register account mutation and admin-action endpoints."""
@app.route("/api/account/mailu/rotate", methods=["POST"])
@require_auth
def account_mailu_rotate() -> Any:
"""Rotate the user's Mailu app password and trigger dependent syncs."""
ok, resp = require_account_access()
if not ok:
return resp
if ariadne_client.enabled():
return ariadne_client.proxy("POST", "/api/account/mailu/rotate")
if not admin_client().ready():
return jsonify({"error": "server not configured"}), 503
username = g.keycloak_username
if not username:
return jsonify({"error": "missing username"}), 400
password = random_password()
try:
admin_client().set_user_attribute(username, "mailu_app_password", password)
except Exception:
return jsonify({"error": "failed to update mail password"}), 502
sync_enabled = bool(settings.MAILU_SYNC_URL)
sync_ok = False
sync_error = ""
if sync_enabled:
try:
with httpx.Client(timeout=30) as client:
resp = client.post(
settings.MAILU_SYNC_URL,
json={"ts": int(time.time()), "wait": True, "reason": "portal_mailu_rotate"},
)
sync_ok = resp.status_code == 200
if not sync_ok:
sync_error = f"sync status {resp.status_code}"
except Exception:
sync_error = "sync request failed"
nextcloud_sync: dict[str, Any] = {"status": "skipped"}
try:
nextcloud_sync = trigger_nextcloud_mail_sync(username, wait=True)
except Exception:
nextcloud_sync = {"status": "error"}
return jsonify(
{
"password": password,
"sync_enabled": sync_enabled,
"sync_ok": sync_ok,
"sync_error": sync_error,
"nextcloud_sync": nextcloud_sync,
}
)
@app.route("/api/account/wger/reset", methods=["POST"])
@require_auth
def account_wger_reset() -> Any:
"""Reset the user's Wger password through the sync Job path."""
ok, resp = require_account_access()
if not ok:
return resp
if ariadne_client.enabled():
return ariadne_client.proxy("POST", "/api/account/wger/reset")
if not admin_client().ready():
return jsonify({"error": "server not configured"}), 503
username = g.keycloak_username
if not username:
return jsonify({"error": "missing username"}), 400
keycloak_email = g.keycloak_email or ""
mailu_email = ""
try:
user = admin_client().find_user(username) or {}
attrs = user.get("attributes") if isinstance(user, dict) else None
if isinstance(attrs, dict):
raw_mailu = attrs.get("mailu_email")
if isinstance(raw_mailu, list) and raw_mailu:
mailu_email = str(raw_mailu[0])
elif isinstance(raw_mailu, str) and raw_mailu:
mailu_email = raw_mailu
except Exception:
pass
email = mailu_email or f"{username}@{settings.MAILU_DOMAIN}"
password = random_password()
try:
result = trigger_wger_user_sync(username, email, password, wait=True)
status_val = result.get("status") if isinstance(result, dict) else "error"
if status_val != "ok":
raise RuntimeError(f"wger sync {status_val}")
except Exception as exc:
message = str(exc).strip() or "wger sync failed"
return jsonify({"error": message}), 502
try:
admin_client().set_user_attribute(username, "wger_password", password)
admin_client().set_user_attribute(
username,
"wger_password_updated_at",
time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
)
except Exception:
return jsonify({"error": "failed to store wger password"}), 502
return jsonify({"status": "ok", "password": password})
@app.route("/api/account/wger/rotation/check", methods=["POST"])
@require_auth
def account_wger_rotation_check() -> Any:
"""Proxy or reject Wger rotation status checks for this account."""
ok, resp = require_account_access()
if not ok:
return resp
if ariadne_client.enabled():
return ariadne_client.proxy("POST", "/api/account/wger/rotation/check")
return jsonify({"error": "server not configured"}), 503
@app.route("/api/account/firefly/reset", methods=["POST"])
@require_auth
def account_firefly_reset() -> Any:
"""Reset the user's Firefly password through the sync Job path."""
ok, resp = require_account_access()
if not ok:
return resp
if ariadne_client.enabled():
return ariadne_client.proxy("POST", "/api/account/firefly/reset")
if not admin_client().ready():
return jsonify({"error": "server not configured"}), 503
username = g.keycloak_username
if not username:
return jsonify({"error": "missing username"}), 400
keycloak_email = g.keycloak_email or ""
mailu_email = ""
try:
user = admin_client().find_user(username) or {}
attrs = user.get("attributes") if isinstance(user, dict) else None
if isinstance(attrs, dict):
raw_mailu = attrs.get("mailu_email")
if isinstance(raw_mailu, list) and raw_mailu:
mailu_email = str(raw_mailu[0])
elif isinstance(raw_mailu, str) and raw_mailu:
mailu_email = raw_mailu
except Exception:
pass
email = mailu_email or f"{username}@{settings.MAILU_DOMAIN}"
password = random_password(24)
try:
result = trigger_firefly_user_sync(username, email, password, wait=True)
status_val = result.get("status") if isinstance(result, dict) else "error"
if status_val != "ok":
raise RuntimeError(f"firefly sync {status_val}")
except Exception as exc:
message = str(exc).strip() or "firefly sync failed"
return jsonify({"error": message}), 502
try:
admin_client().set_user_attribute(username, "firefly_password", password)
admin_client().set_user_attribute(
username,
"firefly_password_updated_at",
time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
)
except Exception:
return jsonify({"error": "failed to store firefly password"}), 502
return jsonify({"status": "ok", "password": password})
@app.route("/api/account/firefly/rotation/check", methods=["POST"])
@require_auth
def account_firefly_rotation_check() -> Any:
"""Proxy or reject Firefly rotation status checks for this account."""
ok, resp = require_account_access()
if not ok:
return resp
if ariadne_client.enabled():
return ariadne_client.proxy("POST", "/api/account/firefly/rotation/check")
return jsonify({"error": "server not configured"}), 503
@app.route("/api/account/nextcloud/mail/sync", methods=["POST"])
@require_auth
def account_nextcloud_mail_sync() -> Any:
"""Trigger a targeted Nextcloud mail sync for the signed-in user."""
ok, resp = require_account_access()
if not ok:
return resp
if ariadne_client.enabled():
payload = request.get_json(silent=True) or {}
return ariadne_client.proxy("POST", "/api/account/nextcloud/mail/sync", payload=payload)
if not admin_client().ready():
return jsonify({"error": "server not configured"}), 503
username = g.keycloak_username
if not username:
return jsonify({"error": "missing username"}), 400
payload = request.get_json(silent=True) or {}
wait = bool(payload.get("wait", True))
try:
result = trigger_nextcloud_mail_sync(username, wait=wait)
return jsonify(result)
except Exception as exc:
message = str(exc).strip() or "failed to sync nextcloud mail"
return jsonify({"error": message}), 502