diff --git a/server/model/schema.py b/server/model/schema.py index ae3cd27f..9cc5de89 100644 --- a/server/model/schema.py +++ b/server/model/schema.py @@ -128,6 +128,9 @@ class BindUser(Base): email = db.Column(db.String(128), nullable=True, comment="邮箱") name = db.Column(db.String(128), nullable=True, comment="用户名") avatar = db.Column(db.String(128), nullable=True, comment="头像") + access_token = db.Column( + db.String(128), nullable=True, comment="GitHub access_token" + ) extra = db.Column( JSONStr(1024), nullable=True, server_default=text("'{}'"), comment="用户其他字段" ) diff --git a/server/routes/github.py b/server/routes/github.py index 446d85ff..f6751450 100644 --- a/server/routes/github.py +++ b/server/routes/github.py @@ -1,11 +1,14 @@ -import logging import os from app import app from flask import Blueprint, abort, redirect, request, session from model.schema import Team from utils.auth import authenticated -from utils.github.common import get_installation_token, get_jwt, verify_github_signature +from utils.github.application import ( + get_installation_token, + get_jwt, + verify_github_signature, +) from utils.user import register bp = Blueprint("github", __name__, url_prefix="/api/github") @@ -25,7 +28,7 @@ def github_install(): ) -@bp.route("/register", methods=["GET"]) +@bp.route("/oauth", methods=["GET"]) def github_register(): """GitHub OAuth register. @@ -57,9 +60,9 @@ def github_hook(): x_github_event = request.headers.get("x-github-event", None) - logging.info(x_github_event) + app.logger.info(x_github_event) - logging.debug(request.json) + app.logger.debug(request.json) return "Receive Success!" diff --git a/server/utils/github/account.py b/server/utils/github/account.py new file mode 100644 index 00000000..d064a356 --- /dev/null +++ b/server/utils/github/account.py @@ -0,0 +1,69 @@ +import httpx +from app import app + + +def get_user_info(access_token: str) -> dict | None: + """Get user info by access token. + + Args: + access_token (str): The user access token. + + Returns: + dict: User info. + """ + + with httpx.Client() as client: + response = client.get( + "https://api.github.com/user", + headers={ + "Accept": "application/vnd.github.v3+json", + "Authorization": f"token {access_token}", + }, + ) + if response.status_code != 200: + app.logger.debug(f"Failed to get user info. {response.text}") + return None + + user_info = response.json() + return user_info + + app.logger.debug("Failed to get user info.") + return None + + +def get_email(access_token: str) -> str | None: + """Get user email by access token. + + Args: + access_token (str): The user access token. + + Returns: + str: User email. + """ + + with httpx.Client() as client: + response = client.get( + "https://api.github.com/user/emails", + headers={ + "Accept": "application/vnd.github.v3+json", + "Authorization": f"Bearer {access_token}", + "X-GitHub-Api-Version": "2022-11-28", + }, + ) + if response.status_code != 200: + app.logger.debug(f"Failed to get user email. {response.text}") + return None + + user_emails = response.json() + if len(user_emails) == 0: + app.logger.debug("Failed to get user email.") + return None + + for user_email in user_emails: + if user_email["primary"]: + return user_email["email"] + + return user_emails[0]["email"] + + app.logger.debug("Failed to get user email.") + return None diff --git a/server/utils/github/common.py b/server/utils/github/application.py similarity index 79% rename from server/utils/github/common.py rename to server/utils/github/application.py index 4b3e811f..5f59fefb 100644 --- a/server/utils/github/common.py +++ b/server/utils/github/application.py @@ -1,12 +1,12 @@ import hashlib import hmac -import logging import os import time from functools import wraps from urllib.parse import parse_qs import httpx +from app import app from flask import abort, request from jwt import JWT, jwk_from_pem @@ -63,7 +63,7 @@ def get_installation_token(jwt: str, installation_id: str) -> str | None: }, ) if response.status_code != 200: - logging.debug(f"Failed to get installation token. {response.text}") + app.logger.debug(f"Failed to get installation token. {response.text}") return None installation_token = response.json().get("token", None) @@ -92,12 +92,13 @@ def oauth_by_code(code: str) -> dict | None: }, ) if response.status_code != 200: + app.logger.debug(f"Failed to get access token. {response.text}") return None try: oauth_info = parse_qs(response.text) except Exception as e: - logging.debug(e) + app.logger.debug(e) return None return oauth_info @@ -132,10 +133,10 @@ def wrapper(*args, **kwargs): ) expected_signature = "sha256=" + hash_object.hexdigest() - logging.debug(f"{expected_signature} {signature}") + app.logger.debug(f"{expected_signature} {signature}") if not hmac.compare_digest(expected_signature, signature): - logging.debug("Invalid signature.") + app.logger.debug("Invalid signature.") abort(403, "Invalid signature.") return func(*args, **kwargs) @@ -143,31 +144,3 @@ def wrapper(*args, **kwargs): return wrapper return decorator - - -def get_user_info(access_token: str): - """Get user info by access token. - - Args: - access_token (str): The user access token. - - Returns: - dict: User info. - """ - - with httpx.Client() as client: - response = client.get( - "https://api.github.com/user", - headers={ - "Accept": "application/vnd.github.v3+json", - "Authorization": f"token {access_token}", - }, - ) - if response.status_code != 200: - logging.debug(f"Failed to get user info. {response.text}") - return None - - user_info = response.json() - return user_info - - return None diff --git a/server/utils/user.py b/server/utils/user.py index 36ff98fa..eb112e7b 100644 --- a/server/utils/user.py +++ b/server/utils/user.py @@ -1,6 +1,8 @@ from app import app, db +from flask import abort from model.schema import BindUser, ObjID, User -from utils.github.common import get_user_info, oauth_by_code +from utils.github.account import get_email, get_user_info +from utils.github.application import oauth_by_code def register(code: str) -> str | None: @@ -10,6 +12,8 @@ def register(code: str) -> str | None: """ oauth_info = oauth_by_code(code) # 获取 access token + if oauth_info is None: + abort(500) access_token = oauth_info.get("access_token", None)[0] # 这里要考虑取哪个,为什么会有多个? @@ -23,26 +27,28 @@ def register(code: str) -> str | None: if user is not None: return user.id + email = get_email(access_token) + new_user = User( id=ObjID.new_id(), github_id=github_id, - email=user_info.get( - "email", None - ), # 这里的邮箱其实是公开邮箱,可能会获取不到 TODO: 换成使用用户邮箱 API 来获取 + email=email, # 这里的邮箱其实是公开邮箱,可能会获取不到 TODO: 换成使用用户邮箱 API 来获取 name=user_info.get("login", None), avatar=user_info.get("avatar_url", None), extra=user_info, ) db.session.add(new_user) - db.session.commit() + db.session.flush() new_bind_user = BindUser( id=ObjID.new_id(), user_id=new_user.id, platform="github", - email=user_info.get("email", None), + email=email, + name=user_info.get("login", None), avatar=user_info.get("avatar_url", None), + access_token=access_token, extra=oauth_info, )