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