import json from django.conf import settings from django.http import HttpRequest, JsonResponse, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect from django.utils import timezone from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_GET, require_http_methods, require_POST from .models import DesktopLoginSession, LoginCode, User from .services.feishu_oauth import ( build_login_url, build_qr_goto, build_qr_login_config, build_tauri_callback, create_desktop_login_session, exchange_tmp_code_for_oauth_code, handle_feishu_callback, ) from .services.jwt_service import issue_jwt from vpn.services.offboarding import revoke_user_vpn_access class TauriDeepLinkRedirect(HttpResponseRedirect): allowed_schemes = HttpResponseRedirect.allowed_schemes + [settings.TAURI_DEEP_LINK_SCHEME] @require_GET def feishu_login_url(_request: HttpRequest) -> JsonResponse: return JsonResponse(build_login_url()) @require_GET def feishu_qr_config(_request: HttpRequest) -> JsonResponse: return JsonResponse(build_qr_login_config()) @require_GET def feishu_qr_page(_request: HttpRequest) -> HttpResponse: config = build_qr_login_config() html = f""" AbyssInfo VPN 飞书扫码登录

飞书扫码登录

请使用飞书 App 扫描二维码。授权完成后会自动回到 AbyssInfo VPN。

二维码 5 分钟内有效,过期请刷新页面。

""" return HttpResponse(html) @csrf_exempt @require_POST def desktop_session_create(_request: HttpRequest) -> JsonResponse: session = create_desktop_login_session() return JsonResponse( { "session_id": session.session_id, "goto": build_qr_goto(session.state), "state": session.state, "expires_in": settings.OAUTH_STATE_EXPIRES_SECONDS, } ) @require_GET def desktop_session_status(_request: HttpRequest, session_id: str) -> JsonResponse: session = DesktopLoginSession.objects.select_related("user").filter(session_id=session_id).first() if session is None: return JsonResponse({"error": "session_not_found"}, status=404) if session.is_expired and session.status == DesktopLoginSession.STATUS_PENDING: session.status = DesktopLoginSession.STATUS_EXPIRED session.error = "session_expired" session.save(update_fields=["status", "error", "updated_at"]) if session.status != DesktopLoginSession.STATUS_AUTHENTICATED: return JsonResponse( { "status": session.status, "error": session.error, "expires_at": session.expires_at.isoformat(), } ) if session.user is None or session.user.status != session.user.STATUS_ACTIVE: return JsonResponse({"status": DesktopLoginSession.STATUS_FAILED, "error": "user_disabled"}, status=403) return JsonResponse( { "status": session.status, "token": issue_jwt(session.user.id), "expires_in": settings.JWT_EXPIRES_SECONDS, "user": { "id": session.user.id, "name": session.user.name, "email": session.user.email, }, } ) @csrf_exempt @require_POST def desktop_session_complete(request: HttpRequest, session_id: str) -> JsonResponse: session = DesktopLoginSession.objects.select_related("user").filter(session_id=session_id).first() if session is None: return JsonResponse({"error": "session_not_found"}, status=404) if session.is_expired: session.status = DesktopLoginSession.STATUS_EXPIRED session.error = "session_expired" session.save(update_fields=["status", "error", "updated_at"]) return JsonResponse({"error": "session_expired"}, status=401) try: body = json.loads(request.body or b"{}") except json.JSONDecodeError: return JsonResponse({"error": "invalid_json"}, status=400) tmp_code = body.get("tmp_code") or "" code = body.get("code") or "" if not code: if not tmp_code: return JsonResponse({"error": "missing_tmp_code"}, status=400) try: code = exchange_tmp_code_for_oauth_code(tmp_code, session.state) except Exception as error: session.status = DesktopLoginSession.STATUS_FAILED session.error = str(error)[:255] session.save(update_fields=["status", "error", "updated_at"]) return JsonResponse({"error": str(error)}, status=400) try: login_code = handle_feishu_callback(code, session.state) except PermissionError: session.status = DesktopLoginSession.STATUS_FAILED session.error = "user_disabled" session.save(update_fields=["status", "error", "updated_at"]) return JsonResponse({"error": "user_disabled"}, status=403) except Exception as error: session.status = DesktopLoginSession.STATUS_FAILED session.error = str(error)[:255] session.save(update_fields=["status", "error", "updated_at"]) return JsonResponse({"error": str(error)}, status=400) session.user = login_code.user session.status = DesktopLoginSession.STATUS_AUTHENTICATED session.error = "" session.authenticated_at = timezone.now() session.save(update_fields=["user", "status", "error", "authenticated_at", "updated_at"]) return JsonResponse({"status": session.status}) @xframe_options_exempt @require_GET def desktop_session_qr_page(request: HttpRequest, session_id: str) -> HttpResponse: session = DesktopLoginSession.objects.filter(session_id=session_id).first() if session is None: return HttpResponseBadRequest("session_not_found") if session.is_expired: return HttpResponseBadRequest("session_expired") embedded = request.GET.get("embedded") == "1" body_class = "embedded" if embedded else "" goto = build_qr_goto(session.state) html = f""" AbyssInfo VPN 飞书扫码登录

飞书扫码登录

请使用飞书 App 扫描二维码。授权完成后可回到 AbyssInfo VPN。

二维码 5 分钟内有效,过期请回到客户端刷新。

""" return HttpResponse(html) def desktop_session_success_page() -> HttpResponse: return HttpResponse( """ AbyssInfo VPN 登录成功

登录成功

可以回到 AbyssInfo VPN 客户端。

""" ) @require_GET def feishu_callback(request: HttpRequest) -> HttpResponseRedirect: code = request.GET.get("code", "") state = request.GET.get("state", "") desktop_session = DesktopLoginSession.objects.filter(state=state).first() try: login_code = handle_feishu_callback(code, state) except PermissionError: if desktop_session: desktop_session.status = DesktopLoginSession.STATUS_FAILED desktop_session.error = "user_disabled" desktop_session.save(update_fields=["status", "error", "updated_at"]) return HttpResponseBadRequest("user_disabled") except Exception as error: if desktop_session: desktop_session.status = DesktopLoginSession.STATUS_FAILED desktop_session.error = str(error)[:255] desktop_session.save(update_fields=["status", "error", "updated_at"]) return HttpResponseBadRequest(str(error)) if desktop_session: desktop_session.user = login_code.user desktop_session.status = DesktopLoginSession.STATUS_AUTHENTICATED desktop_session.error = "" desktop_session.authenticated_at = timezone.now() desktop_session.save(update_fields=["user", "status", "error", "authenticated_at", "updated_at"]) return desktop_session_success_page() redirect_url = build_tauri_callback(login_code.code, state) return TauriDeepLinkRedirect(redirect_url) @csrf_exempt @require_http_methods(["GET", "POST"]) def feishu_event_callback(request: HttpRequest) -> JsonResponse: if request.method == "GET": return JsonResponse({"status": "ok"}) try: body = json.loads(request.body or b"{}") except json.JSONDecodeError: return JsonResponse({"status": "ok"}) challenge = body.get("challenge") if challenge: if settings.FEISHU_EVENT_VERIFICATION_TOKEN and not _feishu_event_token_is_valid(body): return JsonResponse({"error": "invalid_event_token"}, status=403) return JsonResponse({"challenge": challenge}) if not _feishu_event_token_is_valid(body): return JsonResponse({"error": "invalid_event_token"}, status=403) event_type = _extract_feishu_event_type(body) if not _is_feishu_user_disabled_event(event_type, body): return JsonResponse({"status": "ignored", "event_type": event_type}) open_id = _extract_feishu_open_id(body) if not open_id: return JsonResponse({"status": "ignored", "event_type": event_type, "reason": "missing_open_id"}) user = User.objects.filter(feishu_open_id=open_id).first() if user is None: return JsonResponse({"status": "ok", "event_type": event_type, "action": "user_not_found"}) summary = revoke_user_vpn_access( user, reason=event_type or "feishu_user_disabled", source="feishu", event=_build_feishu_event_audit_detail(body, event_type, open_id), ) return JsonResponse( { "status": "ok", "event_type": event_type, "action": "user_disabled", "summary": summary, } ) def _feishu_event_token_is_valid(body: dict) -> bool: expected = settings.FEISHU_EVENT_VERIFICATION_TOKEN if not expected: return bool(settings.DEBUG) header = body.get("header") if isinstance(body.get("header"), dict) else {} token = body.get("token") or header.get("token") return token == expected def _extract_feishu_event_type(body: dict) -> str: header = body.get("header") if isinstance(body.get("header"), dict) else {} event = body.get("event") if isinstance(body.get("event"), dict) else {} return ( header.get("event_type") or event.get("type") or body.get("event_type") or body.get("type") or "" ) def _extract_feishu_open_id(body: dict) -> str: event = body.get("event") if isinstance(body.get("event"), dict) else {} candidates = [ event.get("open_id"), _nested_get(event, "user", "open_id"), _nested_get(event, "user_id", "open_id"), _nested_get(event, "object", "open_id"), _nested_get(event, "object", "user_id", "open_id"), _nested_get(event, "employee", "open_id"), _nested_get(body, "open_id"), event.get("union_id"), ] for value in candidates: if isinstance(value, str) and value.strip(): return value.strip() return "" def _nested_get(value: dict, *keys: str): current = value for key in keys: if not isinstance(current, dict): return None current = current.get(key) return current def _is_feishu_user_disabled_event(event_type: str, body: dict) -> bool: normalized = (event_type or "").lower() if normalized in { "contact.user.deleted_v3", "contact.user.deleted", "user.deleted", "user_deleted", }: return True if "user" in normalized and any(keyword in normalized for keyword in ("delete", "disabled", "disable", "frozen", "resigned")): return True event = body.get("event") if isinstance(body.get("event"), dict) else {} status_sources = [ event, event.get("user") if isinstance(event.get("user"), dict) else {}, event.get("object") if isinstance(event.get("object"), dict) else {}, event.get("employee") if isinstance(event.get("employee"), dict) else {}, ] return any(_status_source_is_disabled(source) for source in status_sources) def _status_source_is_disabled(source: dict) -> bool: if not isinstance(source, dict): return False disabled_flags = { "is_deleted", "is_delete", "is_disabled", "is_frozen", "is_resigned", "is_inactive", "deleted", "disabled", "frozen", "resigned", } for key in disabled_flags: if source.get(key) is True: return True status = source.get("status") if isinstance(status, str): return status.lower() in {"deleted", "disabled", "disable", "frozen", "inactive", "resigned"} if isinstance(status, dict): for key in disabled_flags: if status.get(key) is True: return True text_values = [value.lower() for value in status.values() if isinstance(value, str)] return any(value in {"deleted", "disabled", "disable", "frozen", "inactive", "resigned"} for value in text_values) return False def _build_feishu_event_audit_detail(body: dict, event_type: str, open_id: str) -> dict: header = body.get("header") if isinstance(body.get("header"), dict) else {} return { "event_type": event_type, "event_id": header.get("event_id") or body.get("uuid") or "", "open_id": open_id, "create_time": header.get("create_time") or body.get("ts") or "", } @csrf_exempt @require_POST def exchange(request: HttpRequest) -> JsonResponse: body = json.loads(request.body or b"{}") code = body.get("code", "") login_code = LoginCode.objects.select_related("user").filter(code=code).first() if login_code is None or not login_code.is_valid: return JsonResponse({"error": "invalid_login_code"}, status=401) if login_code.user.status != login_code.user.STATUS_ACTIVE: return JsonResponse({"error": "user_disabled"}, status=403) login_code.used_at = timezone.now() login_code.save(update_fields=["used_at"]) return JsonResponse( { "token": issue_jwt(login_code.user.id), "expires_in": settings.JWT_EXPIRES_SECONDS, "user": { "id": login_code.user.id, "name": login_code.user.name, "email": login_code.user.email, }, } )