from __future__ import annotations from functools import wraps import logging from flask import Blueprint, current_app, jsonify, render_template, request, session from .batch import BatchInputError, parse_identifier_content, parse_table_content from .services import Office365Service, ServiceConfigurationError, ServiceOperationError from .tasks import BackgroundTaskManager, TaskNotFoundError bp = Blueprint("office365_admin", __name__) logger = logging.getLogger("office365_admin.routes") def _settings(): return current_app.config["SETTINGS"] def _service() -> Office365Service: return current_app.extensions["office365_service"] def _task_manager() -> BackgroundTaskManager: return current_app.extensions["task_manager"] def _success(data=None, message: str = "ok", status: int = 200): return jsonify({"success": True, "message": message, "data": data}), status def _error(message: str, status: int = 400, details=None): payload = {"success": False, "message": message} if details is not None: payload["details"] = details return jsonify(payload), status def _authenticated() -> bool: settings = _settings() if not settings.effective_auth_enabled: return True return bool(session.get("authenticated")) def require_auth(view_func): @wraps(view_func) def wrapped(*args, **kwargs): if not _authenticated(): return _error("请先登录后台管理平台。", status=401) return view_func(*args, **kwargs) return wrapped def _handle_service_call(callback): try: return callback() except BatchInputError as exc: return _error(str(exc), status=400) except ServiceConfigurationError as exc: return _error(str(exc), status=503) except ServiceOperationError as exc: return _error(exc.message, status=exc.status_code, details=exc.details) except ValueError as exc: return _error(str(exc), status=400) def _json_payload() -> dict: return request.get_json(silent=True) or {} def _text_payload() -> str: payload = _json_payload() if payload.get("content"): return str(payload["content"]) if request.form.get("content"): return request.form["content"] uploaded_file = request.files.get("file") if uploaded_file and uploaded_file.filename: return uploaded_file.read().decode("utf-8-sig") return "" @bp.get("/") def index(): return render_template("index.html", bootstrap=_settings().to_public_dict()) @bp.get("/api/health") def health(): settings = _settings() return _success( { "platform": settings.to_public_dict(), "authenticated": _authenticated(), } ) @bp.get("/api/session") def session_info(): return _success( { "authenticated": _authenticated(), "authEnabled": _settings().effective_auth_enabled, } ) @bp.post("/api/login") def login(): settings = _settings() if not settings.effective_auth_enabled: session["authenticated"] = True session.permanent = True return _success({"authenticated": True}, message="当前平台未启用登录保护。") payload = _json_payload() username = str(payload.get("username", "")).strip() password = str(payload.get("password", "")).strip() if username == settings.admin_username and password == settings.admin_password: session["authenticated"] = True session.permanent = True return _success({"authenticated": True}, message="登录成功。") return _error("用户名或密码错误。", status=401) @bp.post("/api/logout") def logout(): session.clear() return _success({"authenticated": False}, message="已退出登录。") @bp.get("/api/config") @require_auth def config_info(): return _success(_settings().to_public_dict()) @bp.get("/api/tasks/") @require_auth def task_status(task_id: str): try: return _success(_task_manager().get_task(task_id)) except TaskNotFoundError: return _error("任务不存在。", status=404) @bp.get("/api/licenses") @require_auth def licenses(): return _handle_service_call(lambda: _success(_service().list_licenses())) @bp.get("/api/users") @require_auth def list_users(): search = request.args.get("search", "").strip() try: page = int(request.args.get("page", "1")) page_size = int(request.args.get("pageSize", str(_settings().default_page_size))) except ValueError: return _error("page 和 pageSize 必须是整数。", status=400) return _handle_service_call( lambda: _success(_service().list_users(search=search, page=page, page_size=page_size)) ) @bp.get("/api/users/selection") @require_auth def list_user_identifiers(): search = request.args.get("search", "").strip() return _handle_service_call( lambda: _success(_service().list_user_identifiers(search=search)) ) @bp.get("/api/users/") @require_auth def get_user(identifier: str): return _handle_service_call(lambda: _success(_service().get_user(identifier))) @bp.post("/api/users") @require_auth def create_user(): payload = _json_payload() return _handle_service_call( lambda: _success(_service().create_user(payload), message="用户创建成功。", status=201) ) @bp.patch("/api/users/") @require_auth def update_user(identifier: str): payload = _json_payload() return _handle_service_call( lambda: _success(_service().update_user(identifier, payload), message="用户更新成功。") ) @bp.delete("/api/users/") @require_auth def delete_user(identifier: str): return _handle_service_call( lambda: _success(_service().delete_user(identifier), message="用户删除成功。") ) @bp.post("/api/users//reset-password") @require_auth def reset_password(identifier: str): payload = _json_payload() return _handle_service_call( lambda: _success(_service().reset_password(identifier, payload), message="密码重置成功。") ) @bp.post("/api/users/batch/create") @require_auth def batch_create(): payload = _json_payload() rows = payload.get("rows") if rows is None: rows = parse_table_content(_text_payload()) return _handle_service_call(lambda: _submit_batch_task("create", rows)) @bp.post("/api/users/batch/update") @require_auth def batch_update(): payload = _json_payload() rows = payload.get("rows") if rows is None: rows = parse_table_content(_text_payload()) return _handle_service_call(lambda: _submit_batch_task("update", rows)) @bp.post("/api/users/batch/delete") @require_auth def batch_delete(): payload = _json_payload() identifiers = payload.get("identifiers") if identifiers is None: identifiers = parse_identifier_content(_text_payload()) return _handle_service_call(lambda: _submit_batch_task("delete", identifiers)) @bp.post("/api/users/batch/reset-password") @require_auth def batch_reset_password(): payload = _json_payload() rows = payload.get("rows") if rows is None: text = _text_payload() try: rows = parse_table_content(text) except BatchInputError: rows = parse_identifier_content(text) return _handle_service_call(lambda: _submit_batch_task("reset-password", rows)) def _submit_batch_task(operation: str, items): service = _service() task_manager = _task_manager() total = len(items) if operation == "create": runner = lambda progress: service.batch_create(items, progress_callback=progress) message = "批量创建任务已提交。" elif operation == "update": runner = lambda progress: service.batch_update(items, progress_callback=progress) message = "批量更新任务已提交。" elif operation == "delete": runner = lambda progress: service.batch_delete(items, progress_callback=progress) message = "批量删除任务已提交。" elif operation == "reset-password": runner = lambda progress: service.batch_reset_password(items, progress_callback=progress) message = "批量重置密码任务已提交。" else: raise ValueError("不支持的批量任务类型。") logger.info("Submitting batch task: operation=%s total=%s", operation, total) task = task_manager.submit(operation=operation, total=total, runner=runner) return _success(task, message=message, status=202)