from __future__ import annotations import secrets from datetime import datetime, timezone, timedelta from functools import wraps from flask import Blueprint, current_app, jsonify, render_template, request, session from sqlalchemy import func from . import db from .models import RedemptionCode from .services import Office365Service, ServiceConfigurationError, ServiceOperationError bp_admin = Blueprint("admin", __name__, url_prefix="/admin") bp_user = Blueprint("user", __name__) def _settings(): return current_app.config["SETTINGS"] def _service() -> Office365Service: return current_app.extensions["office365_service"] 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 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 {} @bp_admin.get("/") def admin_index(): if not _authenticated(): return render_template("admin_login.html", settings=_settings()) return render_template("admin_dashboard.html", settings=_settings()) @bp_admin.get("/api/health") def health(): settings = _settings() return _success( { "platform": settings.to_public_dict(), "authenticated": _authenticated(), } ) @bp_admin.get("/api/session") def session_info(): return _success( { "authenticated": _authenticated(), "authEnabled": _settings().effective_auth_enabled, } ) @bp_admin.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_admin.post("/api/logout") def logout(): session.clear() return _success({"authenticated": False}, message="已退出登录。") @bp_admin.get("/api/config") @require_auth def config_info(): return _success(_settings().to_public_dict()) @bp_admin.get("/api/codes") @require_auth def list_codes(): status = request.args.get("status") query = db.select(RedemptionCode) if status == "available": query = query.where(RedemptionCode.status == "available") elif status == "used": query = query.where(RedemptionCode.status == "used") result = db.session.execute(query.order_by(RedemptionCode.created_at.desc())).scalars().all() codes = [code.to_dict() for code in result] return _success({"codes": codes, "total": len(codes)}) @bp_admin.post("/api/codes/generate") @require_auth def generate_codes(): payload = _json_payload() count = payload.get("count", 1) if count < 1: count = 1 if count > 100: count = 100 codes = [] for _ in range(count): code = secrets.token_urlsafe(12) while RedemptionCode.query.filter_by(code=code).first(): code = secrets.token_urlsafe(12) redemption_code = RedemptionCode(code=code) db.session.add(redemption_code) codes.append(code) db.session.commit() return _success({"codes": codes, "count": len(codes)}, f"成功生成 {count} 个兑换码。") @bp_admin.delete("/api/codes/") @require_auth def delete_code(code: str): redemption_code = RedemptionCode.query.filter_by(code=code).first() if not redemption_code: return _error("兑换码不存在。", status=404) db.session.delete(redemption_code) db.session.commit() return _success(message="兑换码已删除。") @bp_admin.get("/api/records") @require_auth def list_records(): page = int(request.args.get("page", "1")) page_size = int(request.args.get("pageSize", "25")) query = db.select(RedemptionCode).where(RedemptionCode.status == "used") result = db.session.execute(query.order_by(RedemptionCode.used_at.desc())).scalars().all() total = len(result) start = (page - 1) * page_size end = start + page_size records = result[start:end] return _success({ "records": [code.to_dict() for code in records], "page": page, "pageSize": page_size, "total": total, }) @bp_user.get("/") def index(): return render_template("user_redemption.html", settings=_settings()) @bp_user.post("/api/redeem") def redeem(): payload = _json_payload() code = str(payload.get("code", "")).strip().upper() username = str(payload.get("username", "")).strip().lower() if not code: return _error("请输入兑换码。", status=400) if not username: return _error("请输入用户名。", status=400) redemption_code = RedemptionCode.query.filter( func.lower(RedemptionCode.code) == code.lower(), RedemptionCode.status == "available" ).first() if not redemption_code: return _error("兑换码无效或已被使用。", status=404) try: user_result = _service().create_user(username=username) except ServiceOperationError as exc: return _error(str(exc), status=500) except Exception as exc: return _error(f"创建账号失败: {exc}", status=500) redemption_code.status = "used" redemption_code.used_at = datetime.now(timezone.utc) redemption_code.used_by_username = username redemption_code.used_by_principal_name = user_result.get("userPrincipalName") db.session.commit() return _success({ "userPrincipalName": user_result.get("userPrincipalName"), "temporaryPassword": user_result.get("temporaryPassword"), }, "账号开通成功!", status=201) @bp_user.get("/api/config") def config(): return _success(_settings().to_public_dict())