feat: 迁移 oauth 模块到 controllers 层
- 迁移 2 个 oauth 相关 API 到 controllers/oauth.py: - api_get_oauth_auth_url - api_exchange_oauth_token - 更新 routes/oauth.py 移除 impl 参数 - OAuth 配置常量从 config 模块读取 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
2ab1721946
commit
6110badd7a
3 files changed
+128
-4
No files matched your search
+1
-1
@@ -75,7 +75,7 @@ def create_app(*, autostart_scheduler: Optional[bool] = None):
|
||||
app.register_blueprint(accounts.create_blueprint(impl=legacy))
|
||||
app.register_blueprint(emails.create_blueprint(impl=legacy))
|
||||
app.register_blueprint(temp_emails.create_blueprint(impl=legacy))
|
||||
app.register_blueprint(oauth.create_blueprint(impl=legacy))
|
||||
app.register_blueprint(oauth.create_blueprint())
|
||||
app.register_blueprint(settings.create_blueprint())
|
||||
app.register_blueprint(scheduler.create_blueprint())
|
||||
app.register_blueprint(system.create_blueprint())
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import urllib.parse
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from flask import jsonify, request
|
||||
|
||||
from outlook_web import config
|
||||
from outlook_web.audit import log_audit
|
||||
from outlook_web.security.auth import (
|
||||
check_export_verify_token,
|
||||
consume_export_verify_token,
|
||||
login_required,
|
||||
)
|
||||
|
||||
|
||||
# OAuth 配置
|
||||
OAUTH_SCOPES = [
|
||||
"offline_access",
|
||||
"https://graph.microsoft.com/Mail.Read",
|
||||
"https://graph.microsoft.com/Mail.ReadWrite",
|
||||
"https://graph.microsoft.com/User.Read"
|
||||
]
|
||||
|
||||
|
||||
# ==================== OAuth API ====================
|
||||
|
||||
|
||||
@login_required
|
||||
def api_get_oauth_auth_url() -> Any:
|
||||
"""生成 OAuth 授权 URL"""
|
||||
oauth_client_id = config.get_oauth_client_id()
|
||||
oauth_redirect_uri = config.get_oauth_redirect_uri()
|
||||
|
||||
base_auth_url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
|
||||
params = {
|
||||
"client_id": oauth_client_id,
|
||||
"response_type": "code",
|
||||
"redirect_uri": oauth_redirect_uri,
|
||||
"response_mode": "query",
|
||||
"scope": " ".join(OAUTH_SCOPES),
|
||||
"state": "12345"
|
||||
}
|
||||
auth_url = f"{base_auth_url}?{urllib.parse.urlencode(params)}"
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'auth_url': auth_url,
|
||||
'client_id': oauth_client_id,
|
||||
'redirect_uri': oauth_redirect_uri
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def api_exchange_oauth_token() -> Any:
|
||||
"""使用授权码换取 Refresh Token"""
|
||||
oauth_client_id = config.get_oauth_client_id()
|
||||
oauth_redirect_uri = config.get_oauth_redirect_uri()
|
||||
|
||||
data = request.json
|
||||
redirected_url = data.get('redirected_url', '').strip()
|
||||
verify_token = data.get('verify_token')
|
||||
|
||||
if not redirected_url:
|
||||
return jsonify({'success': False, 'error': '请提供授权后的完整 URL'})
|
||||
|
||||
# 从 URL 中提取 code
|
||||
try:
|
||||
parsed_url = urllib.parse.urlparse(redirected_url)
|
||||
query_params = urllib.parse.parse_qs(parsed_url.query)
|
||||
auth_code = query_params['code'][0]
|
||||
except (KeyError, IndexError):
|
||||
return jsonify({'success': False, 'error': '无法从 URL 中提取授权码,请检查 URL 是否正确'})
|
||||
|
||||
# 二次验证(敏感信息:refresh_token 不默认明文返回)
|
||||
ok, error_message = check_export_verify_token(verify_token)
|
||||
if not ok:
|
||||
return jsonify({'success': False, 'error': error_message, 'need_verify': True}), 401
|
||||
|
||||
# 使用 Code 换取 Token (Public Client 不需要 client_secret)
|
||||
token_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
|
||||
token_data = {
|
||||
"client_id": oauth_client_id,
|
||||
"code": auth_code,
|
||||
"redirect_uri": oauth_redirect_uri,
|
||||
"grant_type": "authorization_code",
|
||||
"scope": " ".join(OAUTH_SCOPES)
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(token_url, data=token_data, timeout=30)
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': f'请求失败: {str(e)}'})
|
||||
|
||||
if response.status_code == 200:
|
||||
tokens = response.json()
|
||||
refresh_token = tokens.get('refresh_token')
|
||||
|
||||
if not refresh_token:
|
||||
return jsonify({'success': False, 'error': '未能获取 Refresh Token'})
|
||||
|
||||
# 成功后再消费一次性验证 token(避免失败时消耗 token)
|
||||
ok, error_message = consume_export_verify_token(verify_token)
|
||||
if not ok:
|
||||
return jsonify({'success': False, 'error': error_message, 'need_verify': True}), 401
|
||||
|
||||
log_audit('oauth_exchange', 'oauth', None, "换取 Refresh Token 成功(已二次验证)")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'refresh_token': refresh_token,
|
||||
'client_id': oauth_client_id,
|
||||
'token_type': tokens.get('token_type'),
|
||||
'expires_in': tokens.get('expires_in'),
|
||||
'scope': tokens.get('scope')
|
||||
})
|
||||
else:
|
||||
error_data = response.json() if response.headers.get('content-type', '').startswith('application/json') else {}
|
||||
error_msg = error_data.get('error_description', response.text)
|
||||
return jsonify({'success': False, 'error': f'获取令牌失败: {error_msg}'})
|
||||
@@ -2,10 +2,13 @@ from __future__ import annotations
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
from outlook_web.controllers import oauth as oauth_controller
|
||||
|
||||
def create_blueprint(*, impl) -> Blueprint:
|
||||
|
||||
def create_blueprint() -> Blueprint:
|
||||
"""创建 oauth Blueprint"""
|
||||
bp = Blueprint("oauth", __name__)
|
||||
bp.add_url_rule("/api/oauth/auth-url", view_func=impl.api_get_oauth_auth_url, methods=["GET"])
|
||||
bp.add_url_rule("/api/oauth/exchange-token", view_func=impl.api_exchange_oauth_token, methods=["POST"])
|
||||
bp.add_url_rule("/api/oauth/auth-url", view_func=oauth_controller.api_get_oauth_auth_url, methods=["GET"])
|
||||
bp.add_url_rule("/api/oauth/exchange-token", view_func=oauth_controller.api_exchange_oauth_token, methods=["POST"])
|
||||
return bp
|
||||
|
||||
Reference in new issue
Block a user