完成部分Github登录回调

This commit is contained in:
2025-08-27 13:49:17 +08:00
parent bf3b0e5f7e
commit 9f93f6040b
7 changed files with 203 additions and 22 deletions

View File

@@ -54,3 +54,16 @@ class SiteConfigModel(ResponseModel):
logo_dark: Optional[str] = Field(default=None, description="网站Logo URL深色模式") logo_dark: Optional[str] = Field(default=None, description="网站Logo URL深色模式")
captcha_type: Literal['none', 'default', 'gcaptcha', 'cloudflare turnstile'] = Field(default='none', description="验证码类型") captcha_type: Literal['none', 'default', 'gcaptcha', 'cloudflare turnstile'] = Field(default='none', description="验证码类型")
captcha_key: Optional[str] = Field(default=None, description="验证码密钥") captcha_key: Optional[str] = Field(default=None, description="验证码密钥")
class AuthnModel(BaseModel):
id: str = Field(default=None, description="ID")
fingerprint: str = Field(default=None, description="指纹")
class UserSettingModel(BaseModel):
authn: Optional[AuthnModel] = Field(default=None, description="认证信息")
group_expires: Optional[datetime] = Field(default=None, description="用户组过期时间")
prefer_theme: str = Field(default="#607D8B", description="用户首选主题")
qq: str | bool = Field(default=False, description="QQ号")
themes: dict = Field(default_factory=dict, description="用户主题配置")
two_factor: bool = Field(default=False, description="是否启用两步验证")
uid: int = Field(default=0, description="用户UID")

View File

@@ -1,7 +1,8 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, Query
from fastapi.responses import PlainTextResponse from fastapi.responses import PlainTextResponse, RedirectResponse
from middleware.auth import SignRequired from middleware.auth import SignRequired
from models.response import ResponseModel from models.response import ResponseModel
import service.oauth
callback_router = APIRouter( callback_router = APIRouter(
prefix='/callback', prefix='/callback',
@@ -41,19 +42,79 @@ def router_callback_qq() -> ResponseModel:
""" """
pass pass
@oauth_router.post( @oauth_router.get(
path='/github', path='/github',
summary='GitHub OAuth 回调', summary='GitHub OAuth 回调',
description='Handle GitHub OAuth callback and return user information.', description='Handle GitHub OAuth callback and return user information.',
) )
def router_callback_github() -> ResponseModel: async def router_callback_github(
code: str = Query(description="The token received from GitHub for authentication.")) -> PlainTextResponse:
""" """
Handle GitHub OAuth callback and return user information. GitHub OAuth 回调处理
- Github 成功响应:
- JWT: {"access_token": "gho_xxxxxxxx", "token_type": "bearer", "scope": ""}
- User Info:{
"code": "grfessg1312432313421fdgs",
"user_data": {
"login": "Yuerchu",
"id": 114514,
"node_id": "xxxxx",
"avatar_url": "https://avatars.githubusercontent.com/u/114514?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/Yuerchu",
"html_url": "https://github.com/Yuerchu",
"followers_url": "https://api.github.com/users/Yuerchu/followers",
"following_url": "https://api.github.com/users/Yuerchu/following{/other_user}",
"gists_url": "https://api.github.com/users/Yuerchu/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Yuerchu/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Yuerchu/subscriptions",
"organizations_url": "https://api.github.com/users/Yuerchu/orgs",
"repos_url": "https://api.github.com/users/Yuerchu/repos",
"events_url": "https://api.github.com/users/Yuerchu/events{/privacy}",
"received_events_url": "https://api.github.com/users/Yuerchu/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false,
"name": "于小丘",
"company": null,
"blog": "https://www.yxqi.cn",
"location": "ChangSha, HuNan, China",
"email": "admin@yuxiaoqiu.cn",
"hireable": null,
"bio": null,
"twitter_username": null,
"notification_email": "admin@yuxiaoqiu.cn",
"public_repos": 17,
"public_gists": 0,
"followers": 8,
"following": 8,
"created_at": "2019-04-13T11:17:33Z",
"updated_at": "2025-08-20T03:03:16Z"
}
}
- 错误响应示例:
- {
'error': 'bad_verification_code',
'error_description': 'The code passed is incorrect or expired.',
'error_uri': 'https://docs.github.com/apps/managing-oauth-apps/troubleshooting-oauth-app-access-token-request-errors/#bad-verification-code'
}
Returns: Returns:
ResponseModel: A model containing the response data for the GitHub OAuth callback. PlainTextResponse: A response containing the user information from GitHub.
""" """
pass try:
access_token = await service.oauth.github.get_access_token(code)
# [TODO] 把access_token写数据库里
if not access_token:
return PlainTextResponse("Failed to retrieve access token from GitHub.", status_code=400)
user_data = await service.oauth.github.get_user_info(access_token.access_token)
# [TODO] 把user_data写数据库里
return PlainTextResponse(f"User information processed successfully, code: {code}, user_data: {user_data.json_dump()}", status_code=200)
except Exception as e:
return PlainTextResponse(f"An error occurred: {str(e)}", status_code=500)
@pay_router.post( @pay_router.post(
path='/alipay', path='/alipay',

View File

@@ -3,10 +3,10 @@ from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from middleware.auth import AuthRequired, SignRequired from middleware.auth import AuthRequired, SignRequired
import models import models
from models.response import ResponseModel, TokenModel, userModel, groupModel from models.response import ResponseModel, TokenModel, userModel, groupModel, UserSettingModel
from deprecated import deprecated from deprecated import deprecated
from pkg.log import log from pkg.log import log
import service.user.login import service
user_router = APIRouter( user_router = APIRouter(
prefix="/user", prefix="/user",
@@ -30,7 +30,7 @@ async def router_user_session(
username = form_data.username username = form_data.username
password = form_data.password password = form_data.password
user = await service.user.login.login(username=username, password=password) user = await service.user.Login(username=username, password=password)
if user is None: if user is None:
raise HTTPException(status_code=400, detail="Invalid username or password") raise HTTPException(status_code=400, detail="Invalid username or password")
@@ -38,8 +38,11 @@ async def router_user_session(
raise HTTPException(status_code=400, detail="User account is not fully registered") raise HTTPException(status_code=400, detail="User account is not fully registered")
elif user == 2: elif user == 2:
raise HTTPException(status_code=403, detail="User account is banned") raise HTTPException(status_code=403, detail="User account is banned")
elif isinstance(user, TokenModel):
return user return user
else:
log.error(f"Unexpected return type from login service: {type(user)}")
raise HTTPException(status_code=500, detail="Internal server error during login")
@user_router.post( @user_router.post(
path='/', path='/',
@@ -83,6 +86,10 @@ def router_user_reset() -> ResponseModel:
""" """
pass pass
@deprecated(
version="0.0.1",
reason="邮件中带链接的激活易使得被收件服务器误判为垃圾邮件,新版更换为验证码方式"
)
@user_router.patch( @user_router.patch(
path='/reset', path='/reset',
summary='通过邮件里的链接重设密码', summary='通过邮件里的链接重设密码',
@@ -216,10 +223,10 @@ async def router_user_me(
user: Annotated[models.user.User, Depends(AuthRequired)], user: Annotated[models.user.User, Depends(AuthRequired)],
) -> ResponseModel: ) -> ResponseModel:
""" """
Get user information. 获取用户信息.
Returns: :return: ResponseModel containing user information.
dict: A dictionary containing user information. :rtype: ResponseModel
""" """
group = await models.Group.get(id=user.group_id) group = await models.Group.get(id=user.group_id)
@@ -252,14 +259,22 @@ async def router_user_me(
description='Get user storage information.', description='Get user storage information.',
dependencies=[Depends(SignRequired)], dependencies=[Depends(SignRequired)],
) )
def router_user_storage() -> ResponseModel: def router_user_storage(
user: Annotated[models.user.User, Depends(AuthRequired)],
) -> ResponseModel:
""" """
Get user storage information. Get user storage information.
Returns: Returns:
dict: A dictionary containing user storage information. dict: A dictionary containing user storage information.
""" """
pass return ResponseModel(
data={
"used": 0,
"free": 0,
"total": 0,
}
)
@user_router.put( @user_router.put(
path='/authn/start', path='/authn/start',
@@ -346,9 +361,9 @@ def router_user_settings() -> ResponseModel:
Get current user settings. Get current user settings.
Returns: Returns:
dict: A dictionary containing the user's current settings. dict: A dictionary containing the current user settings.
""" """
pass return ResponseModel(data=UserSettingModel().model_dump())
@user_settings_router.post( @user_settings_router.post(
path='/avatar', path='/avatar',

5
service/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
"""
服务层
"""
from .user import login

79
service/oauth/github.py Normal file
View File

@@ -0,0 +1,79 @@
from pydantic import BaseModel
import aiohttp
class GithubAccessToken(BaseModel):
access_token: str
token_type: str
scope: str
class GithubUserData(BaseModel):
login: str
id: int
node_id: str
avatar_url: str
gravatar_id: str | None
url: str
html_url: str
followers_url: str
following_url: str
gists_url: str
starred_url: str
subscriptions_url: str
organizations_url: str
repos_url: str
events_url: str
received_events_url: str
type: str
site_admin: bool
name: str | None
company: str | None
blog: str | None
location: str | None
email: str | None
hireable: bool | None
bio: str | None
twitter_username: str | None
public_repos: int
public_gists: int
followers: int
following: int
created_at: str # ISO 8601 format date-time string
updated_at: str # ISO 8601 format date-time string
class GithubUserInfoResponse(BaseModel):
code: str
user_data: GithubUserData
async def get_access_token(code: str) -> GithubAccessToken:
async with aiohttp.ClientSession() as session:
async with session.post(
url='https://github.com/login/oauth/access_token',
params={
'client_id': '',
'client_secret': '',
'code': code
},
headers={'accept': 'application/json'},
proxy='socks5://127.0.0.1:7890'
) as access_resp:
access_data = await access_resp.json()
return GithubAccessToken(
access_token=access_data.get('access_token'),
token_type=access_data.get('token_type'),
scope=access_data.get('scope')
)
async def get_user_info(access_token: str | GithubAccessToken) -> GithubUserInfoResponse:
if isinstance(access_token, GithubAccessToken):
access_token = access_token.access_token
async with aiohttp.ClientSession() as session:
async with session.get(
url='https://api.github.com/user',
headers={
'accept': 'application/json',
'Authorization': f'token {access_token}'},
proxy='socks5://127.0.0.1:7890'
) as resp:
user_data = await resp.json()
return GithubUserInfoResponse(**user_data)

1
service/user/__init__.py Normal file
View File

@@ -0,0 +1 @@
from .login import Login

View File

@@ -1,11 +1,14 @@
from typing import Optional
from models.setting import Setting from models.setting import Setting
from models.response import TokenModel from models.response import TokenModel
from models.user import User from models.user import User
from pkg.log import log from pkg.log import log
async def login( async def Login(
username: str, username: str,
password: str password: str,
captcha: Optional[str] = None,
twoFaCode: Optional[str] = None
) -> TokenModel | int | None: ) -> TokenModel | int | None:
""" """
根据账号密码进行登录。 根据账号密码进行登录。
@@ -18,6 +21,10 @@ async def login(
:type username: str :type username: str
:param password: 用户密码 :param password: 用户密码
:type password: str :type password: str
:param captcha: 验证码
:type captcha: Optional[str]
:param twoFaCode: 二次验证代码
:type twoFaCode: Optional[str]
:return: TokenModel 对象或状态码或 None :return: TokenModel 对象或状态码或 None
:rtype: TokenModel | int | None :rtype: TokenModel | int | None