mirror of
https://github.com/fastapi-practices/fastapi-best-architecture.git
synced 2026-09-21 05:02:49 +00:00
Add user social binding and unbinding (#919)
* Add user social binding and unbinding * Add oauth2 state to binding * Update oauth2 state * Fix imports * Update some interface definitions * Update the authorization type security
This commit is contained in:
@@ -4,9 +4,11 @@ from backend.core.conf import settings
|
||||
from backend.plugin.oauth2.api.v1.github import router as github_router
|
||||
from backend.plugin.oauth2.api.v1.google import router as google_router
|
||||
from backend.plugin.oauth2.api.v1.linux_do import router as linux_do_router
|
||||
from backend.plugin.oauth2.api.v1.user_social import router as user_social_router
|
||||
|
||||
v1 = APIRouter(prefix=f'{settings.FASTAPI_API_V1_PATH}/oauth2')
|
||||
|
||||
v1.include_router(user_social_router, tags=['OAuth2'])
|
||||
v1.include_router(github_router, prefix='/github', tags=['Github OAuth2'])
|
||||
v1.include_router(google_router, prefix='/google', tags=['Google OAuth2'])
|
||||
v1.include_router(linux_do_router, prefix='/linux-do', tags=['LinuxDo OAuth2'])
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, Response
|
||||
@@ -8,7 +11,8 @@ from starlette.responses import RedirectResponse
|
||||
from backend.common.response.response_schema import ResponseSchemaModel, response_base
|
||||
from backend.core.conf import settings
|
||||
from backend.database.db import CurrentSessionTransaction
|
||||
from backend.plugin.oauth2.enums import UserSocialType
|
||||
from backend.database.redis import redis_client
|
||||
from backend.plugin.oauth2.enums import UserSocialAuthType, UserSocialType
|
||||
from backend.plugin.oauth2.service.oauth2_service import oauth2_service
|
||||
|
||||
router = APIRouter()
|
||||
@@ -18,7 +22,15 @@ github_client = GitHubOAuth20(settings.OAUTH2_GITHUB_CLIENT_ID, settings.OAUTH2_
|
||||
|
||||
@router.get('', summary='获取 Github 授权链接')
|
||||
async def get_github_oauth2_url() -> ResponseSchemaModel[str]:
|
||||
auth_url = await github_client.get_authorization_url(redirect_uri=settings.OAUTH2_GITHUB_REDIRECT_URI)
|
||||
state = str(uuid.uuid4())
|
||||
|
||||
await redis_client.setex(
|
||||
f'{settings.OAUTH2_STATE_REDIS_PREFIX}:{state}',
|
||||
settings.OAUTH2_STATE_EXPIRE_SECONDS,
|
||||
json.dumps({'type': UserSocialAuthType.login.value}),
|
||||
)
|
||||
|
||||
auth_url = await github_client.get_authorization_url(redirect_uri=settings.OAUTH2_GITHUB_REDIRECT_URI, state=state)
|
||||
return response_base.success(data=auth_url)
|
||||
|
||||
|
||||
@@ -37,16 +49,23 @@ async def github_oauth2_callback( # noqa: ANN201
|
||||
Depends(FastAPIOAuth20(github_client, redirect_uri=settings.OAUTH2_GITHUB_REDIRECT_URI)),
|
||||
],
|
||||
):
|
||||
token_data, _state = oauth2
|
||||
token_data, state = oauth2
|
||||
access_token = token_data['access_token']
|
||||
user = await github_client.get_userinfo(access_token)
|
||||
data = await oauth2_service.create_with_login(
|
||||
data = await oauth2_service.login_or_binding(
|
||||
db=db,
|
||||
response=response,
|
||||
background_tasks=background_tasks,
|
||||
user=user,
|
||||
social=UserSocialType.github,
|
||||
state=state,
|
||||
)
|
||||
|
||||
# 绑定流程
|
||||
if data is None:
|
||||
return RedirectResponse(url=settings.OAUTH2_FRONTEND_BINDING_REDIRECT_URI)
|
||||
|
||||
# 登录流程
|
||||
return RedirectResponse(
|
||||
url=f'{settings.OAUTH2_FRONTEND_REDIRECT_URI}?access_token={data.access_token}&session_uuid={data.session_uuid}',
|
||||
url=f'{settings.OAUTH2_FRONTEND_LOGIN_REDIRECT_URI}?access_token={data.access_token}&session_uuid={data.session_uuid}',
|
||||
)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, Response
|
||||
@@ -8,7 +11,8 @@ from starlette.responses import RedirectResponse
|
||||
from backend.common.response.response_schema import ResponseSchemaModel, response_base
|
||||
from backend.core.conf import settings
|
||||
from backend.database.db import CurrentSessionTransaction
|
||||
from backend.plugin.oauth2.enums import UserSocialType
|
||||
from backend.database.redis import redis_client
|
||||
from backend.plugin.oauth2.enums import UserSocialAuthType, UserSocialType
|
||||
from backend.plugin.oauth2.service.oauth2_service import oauth2_service
|
||||
|
||||
router = APIRouter()
|
||||
@@ -18,7 +22,15 @@ google_client = GoogleOAuth20(settings.OAUTH2_GOOGLE_CLIENT_ID, settings.OAUTH2_
|
||||
|
||||
@router.get('', summary='获取 google 授权链接')
|
||||
async def get_google_oauth2_url() -> ResponseSchemaModel[str]:
|
||||
auth_url = await google_client.get_authorization_url(redirect_uri=settings.OAUTH2_GOOGLE_REDIRECT_URI)
|
||||
state = str(uuid.uuid4())
|
||||
|
||||
await redis_client.setex(
|
||||
f'{settings.OAUTH2_STATE_REDIS_PREFIX}:{state}',
|
||||
settings.OAUTH2_STATE_EXPIRE_SECONDS,
|
||||
json.dumps({'type': UserSocialAuthType.login.value}),
|
||||
)
|
||||
|
||||
auth_url = await google_client.get_authorization_url(redirect_uri=settings.OAUTH2_GOOGLE_REDIRECT_URI, state=state)
|
||||
return response_base.success(data=auth_url)
|
||||
|
||||
|
||||
@@ -37,16 +49,23 @@ async def google_oauth2_callback( # noqa: ANN201
|
||||
Depends(FastAPIOAuth20(google_client, redirect_uri=settings.OAUTH2_GOOGLE_REDIRECT_URI)),
|
||||
],
|
||||
):
|
||||
token_data, _state = oauth2
|
||||
token_data, state = oauth2
|
||||
access_token = token_data['access_token']
|
||||
user = await google_client.get_userinfo(access_token)
|
||||
data = await oauth2_service.create_with_login(
|
||||
data = await oauth2_service.login_or_binding(
|
||||
db=db,
|
||||
response=response,
|
||||
background_tasks=background_tasks,
|
||||
user=user,
|
||||
social=UserSocialType.google,
|
||||
state=state,
|
||||
)
|
||||
|
||||
# 绑定流程
|
||||
if data is None:
|
||||
return RedirectResponse(url=settings.OAUTH2_FRONTEND_BINDING_REDIRECT_URI)
|
||||
|
||||
# 登录流程
|
||||
return RedirectResponse(
|
||||
url=f'{settings.OAUTH2_FRONTEND_REDIRECT_URI}?access_token={data.access_token}&session_uuid={data.session_uuid}',
|
||||
url=f'{settings.OAUTH2_FRONTEND_LOGIN_REDIRECT_URI}?access_token={data.access_token}&session_uuid={data.session_uuid}',
|
||||
)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, Response
|
||||
@@ -8,7 +11,8 @@ from starlette.responses import RedirectResponse
|
||||
from backend.common.response.response_schema import ResponseSchemaModel, response_base
|
||||
from backend.core.conf import settings
|
||||
from backend.database.db import CurrentSessionTransaction
|
||||
from backend.plugin.oauth2.enums import UserSocialType
|
||||
from backend.database.redis import redis_client
|
||||
from backend.plugin.oauth2.enums import UserSocialAuthType, UserSocialType
|
||||
from backend.plugin.oauth2.service.oauth2_service import oauth2_service
|
||||
|
||||
router = APIRouter()
|
||||
@@ -18,7 +22,17 @@ linux_do_client = LinuxDoOAuth20(settings.OAUTH2_LINUX_DO_CLIENT_ID, settings.OA
|
||||
|
||||
@router.get('', summary='获取 LinuxDo 授权链接')
|
||||
async def get_linux_do_oauth2_url() -> ResponseSchemaModel[str]:
|
||||
auth_url = await linux_do_client.get_authorization_url(redirect_uri=settings.OAUTH2_LINUX_DO_REDIRECT_URI)
|
||||
state = str(uuid.uuid4())
|
||||
|
||||
await redis_client.setex(
|
||||
f'{settings.OAUTH2_STATE_REDIS_PREFIX}:{state}',
|
||||
settings.OAUTH2_STATE_EXPIRE_SECONDS,
|
||||
json.dumps({'type': UserSocialAuthType.login.value}),
|
||||
)
|
||||
|
||||
auth_url = await linux_do_client.get_authorization_url(
|
||||
redirect_uri=settings.OAUTH2_LINUX_DO_REDIRECT_URI, state=state
|
||||
)
|
||||
return response_base.success(data=auth_url)
|
||||
|
||||
|
||||
@@ -37,16 +51,23 @@ async def linux_do_oauth2_callback( # noqa: ANN201
|
||||
Depends(FastAPIOAuth20(linux_do_client, redirect_uri=settings.OAUTH2_LINUX_DO_REDIRECT_URI)),
|
||||
],
|
||||
):
|
||||
token_data, _state = oauth2
|
||||
token_data, state = oauth2
|
||||
access_token = token_data['access_token']
|
||||
user = await linux_do_client.get_userinfo(access_token)
|
||||
data = await oauth2_service.create_with_login(
|
||||
data = await oauth2_service.login_or_binding(
|
||||
db=db,
|
||||
response=response,
|
||||
background_tasks=background_tasks,
|
||||
user=user,
|
||||
social=UserSocialType.linux_do,
|
||||
state=state,
|
||||
)
|
||||
|
||||
# 绑定流程
|
||||
if data is None:
|
||||
return RedirectResponse(url=settings.OAUTH2_FRONTEND_BINDING_REDIRECT_URI)
|
||||
|
||||
# 登录流程
|
||||
return RedirectResponse(
|
||||
url=f'{settings.OAUTH2_FRONTEND_REDIRECT_URI}?access_token={data.access_token}&session_uuid={data.session_uuid}',
|
||||
url=f'{settings.OAUTH2_FRONTEND_LOGIN_REDIRECT_URI}?access_token={data.access_token}&session_uuid={data.session_uuid}',
|
||||
)
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from backend.common.response.response_schema import ResponseModel, response_base
|
||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||
from backend.common.security.jwt import DependsJwtAuth
|
||||
from backend.database.db import CurrentSessionTransaction
|
||||
from backend.database.db import CurrentSession, CurrentSessionTransaction
|
||||
from backend.plugin.oauth2.enums import UserSocialType
|
||||
from backend.plugin.oauth2.service.user_social import user_social_service
|
||||
from backend.plugin.oauth2.service.user_social_service import user_social_service
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.delete('/me', summary='解绑用户社交账号', dependencies=[DependsJwtAuth])
|
||||
@router.get('/me/bindings', summary='获取用户已绑定的社交账号', dependencies=[DependsJwtAuth])
|
||||
async def get_user_bindings(db: CurrentSession, request: Request) -> ResponseSchemaModel[list[str]]:
|
||||
bindings = await user_social_service.get_bindings(db=db, user_id=request.user.id)
|
||||
return response_base.success(data=bindings)
|
||||
|
||||
|
||||
@router.get('/me/binding', summary='获取绑定授权链接', dependencies=[DependsJwtAuth])
|
||||
async def get_binding_auth_url(request: Request, source: UserSocialType) -> ResponseSchemaModel[str]:
|
||||
binding_url = await user_social_service.get_binding_auth_url(user_id=request.user.id, source=source)
|
||||
return response_base.success(data=binding_url)
|
||||
|
||||
|
||||
@router.delete('/me/unbinding', summary='解绑用户社交账号', dependencies=[DependsJwtAuth])
|
||||
async def unbinding_user(db: CurrentSessionTransaction, request: Request, source: UserSocialType) -> ResponseModel:
|
||||
await user_social_service.unbinding(db=db, user_id=request.user.id, source=source)
|
||||
return response_base.success()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from collections.abc import Sequence
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy_crud_plus import CRUDPlus
|
||||
|
||||
@@ -24,12 +26,22 @@ class CRUDUserSocial(CRUDPlus[UserSocial]):
|
||||
通过 sid 获取社交用户
|
||||
|
||||
:param db: 数据库会话
|
||||
:param sid: 第三方用户唯一编码
|
||||
:param sid: 社交账号唯一编码
|
||||
:param source: 社交账号类型
|
||||
:return:
|
||||
"""
|
||||
return await self.select_model_by_column(db, sid=sid, source=source)
|
||||
|
||||
async def get_by_user_id(self, db: AsyncSession, user_id: int) -> Sequence[UserSocial]:
|
||||
"""
|
||||
通过用户 ID 获取所有社交账号绑定
|
||||
|
||||
:param db: 数据库会话
|
||||
:param user_id: 用户 ID
|
||||
:return:
|
||||
"""
|
||||
return await self.select_models(db, user_id=user_id)
|
||||
|
||||
async def create(self, db: AsyncSession, obj: CreateUserSocialParam) -> None:
|
||||
"""
|
||||
创建用户社交账号绑定
|
||||
|
||||
@@ -4,6 +4,13 @@ from backend.common.enums import StrEnum
|
||||
class UserSocialType(StrEnum):
|
||||
"""用户社交类型"""
|
||||
|
||||
github = 'GitHub'
|
||||
github = 'Github'
|
||||
google = 'Google'
|
||||
linux_do = 'LinuxDo'
|
||||
|
||||
|
||||
class UserSocialAuthType(StrEnum):
|
||||
"""用户社交授权类型"""
|
||||
|
||||
login = 'login'
|
||||
binding = 'binding'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[plugin]
|
||||
summary = 'OAuth 2.0'
|
||||
version = '0.0.9'
|
||||
version = '0.0.10'
|
||||
description = '通过 OAuth 2.0 的方式登录系统'
|
||||
author = 'wu-clan'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from pydantic import Field
|
||||
from pydantic import ConfigDict, Field
|
||||
|
||||
from backend.common.schema import SchemaBase
|
||||
from backend.plugin.oauth2.enums import UserSocialType
|
||||
@@ -19,3 +19,11 @@ class CreateUserSocialParam(UserSocialSchemaBase):
|
||||
|
||||
class UpdateUserSocialParam(SchemaBase):
|
||||
"""更新用户社交参数"""
|
||||
|
||||
|
||||
class GetUserSocialDetail(CreateUserSocialParam):
|
||||
"""获取用户社交详情"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int = Field(description='用户社交 ID')
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import json
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fast_captcha import text_captcha
|
||||
@@ -10,13 +12,15 @@ from backend.app.admin.schema.user import AddOAuth2UserParam
|
||||
from backend.app.admin.service.login_log_service import login_log_service
|
||||
from backend.common.context import ctx
|
||||
from backend.common.enums import LoginLogStatusType
|
||||
from backend.common.exception import errors
|
||||
from backend.common.i18n import t
|
||||
from backend.common.security import jwt
|
||||
from backend.core.conf import settings
|
||||
from backend.database.redis import redis_client
|
||||
from backend.plugin.oauth2.crud.crud_user_social import user_social_dao
|
||||
from backend.plugin.oauth2.enums import UserSocialType
|
||||
from backend.plugin.oauth2.enums import UserSocialAuthType, UserSocialType
|
||||
from backend.plugin.oauth2.schema.user_social import CreateUserSocialParam
|
||||
from backend.plugin.oauth2.service.user_social_service import user_social_service
|
||||
from backend.utils.timezone import timezone
|
||||
|
||||
|
||||
@@ -24,47 +28,33 @@ class OAuth2Service:
|
||||
"""OAuth2 认证服务类"""
|
||||
|
||||
@staticmethod
|
||||
async def create_with_login(
|
||||
async def login(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
response: Response,
|
||||
background_tasks: BackgroundTasks,
|
||||
user: dict[str, Any],
|
||||
social: UserSocialType,
|
||||
) -> GetLoginToken | None:
|
||||
sid: str,
|
||||
source: UserSocialType,
|
||||
username: str | None = None,
|
||||
nickname: str | None = None,
|
||||
email: str | None = None,
|
||||
avatar: str | None = None,
|
||||
) -> GetLoginToken:
|
||||
"""
|
||||
创建 OAuth2 用户并登录
|
||||
OAuth2 用户登录
|
||||
|
||||
:param db: 数据库会话
|
||||
:param response: FastAPI 响应对象
|
||||
:param background_tasks: FastAPI 后台任务
|
||||
:param user: OAuth2 用户信息
|
||||
:param social: 社交平台类型
|
||||
:param sid: 社交账号唯一编码
|
||||
:param source: 社交平台
|
||||
:param username: 用户名
|
||||
:param nickname: 昵称
|
||||
:param email: 邮箱
|
||||
:param avatar: 头像地址
|
||||
:return:
|
||||
"""
|
||||
|
||||
sid = user.get('uuid')
|
||||
username = user.get('username')
|
||||
nickname = user.get('nickname')
|
||||
email = user.get('email')
|
||||
avatar = user.get('avatar_url')
|
||||
|
||||
if social == UserSocialType.github:
|
||||
sid = user.get('id')
|
||||
username = user.get('login')
|
||||
nickname = user.get('name')
|
||||
|
||||
if social == UserSocialType.google:
|
||||
sid = user.get('id')
|
||||
username = user.get('name')
|
||||
nickname = user.get('given_name')
|
||||
avatar = user.get('picture')
|
||||
|
||||
if social == UserSocialType.linux_do:
|
||||
sid = user.get('id')
|
||||
nickname = user.get('name')
|
||||
|
||||
user_social = await user_social_dao.get_by_sid(db, str(sid), str(social.value))
|
||||
user_social = await user_social_dao.get_by_sid(db, sid, source.value)
|
||||
if user_social:
|
||||
sys_user = await user_dao.get(db, user_social.user_id)
|
||||
# 更新用户头像
|
||||
@@ -74,7 +64,7 @@ class OAuth2Service:
|
||||
sys_user = None
|
||||
# 检测系统用户是否已存在
|
||||
if email:
|
||||
sys_user = await user_dao.check_email(db, email) # 通过邮箱验证绑定保证邮箱真实性
|
||||
sys_user = await user_dao.check_email(db, email)
|
||||
|
||||
# 创建系统用户
|
||||
if not sys_user:
|
||||
@@ -92,7 +82,7 @@ class OAuth2Service:
|
||||
sys_user = await user_dao.get_by_username(db, username)
|
||||
|
||||
# 绑定社交账号
|
||||
new_user_social = CreateUserSocialParam(sid=str(sid), source=social.value, user_id=sys_user.id)
|
||||
new_user_social = CreateUserSocialParam(sid=sid, source=source.value, user_id=sys_user.id)
|
||||
await user_social_dao.create(db, new_user_social)
|
||||
|
||||
# 创建 token
|
||||
@@ -140,5 +130,88 @@ class OAuth2Service:
|
||||
)
|
||||
return data
|
||||
|
||||
async def login_or_binding(
|
||||
self,
|
||||
*,
|
||||
db: AsyncSession,
|
||||
response: Response,
|
||||
background_tasks: BackgroundTasks,
|
||||
user: dict[str, Any],
|
||||
social: UserSocialType,
|
||||
state: str | None = None,
|
||||
) -> GetLoginToken | None:
|
||||
"""
|
||||
OAuth2 登录或绑定
|
||||
|
||||
:param db: 数据库会话
|
||||
:param response: FastAPI 响应对象
|
||||
:param background_tasks: FastAPI 后台任务
|
||||
:param user: OAuth2 用户信息
|
||||
:param social: 社交平台类型
|
||||
:param state: OAuth2 state 参数
|
||||
:return:
|
||||
"""
|
||||
|
||||
sid = user.get('uuid')
|
||||
username = user.get('username')
|
||||
nickname = user.get('nickname')
|
||||
email = user.get('email')
|
||||
avatar = user.get('avatar_url')
|
||||
|
||||
match social:
|
||||
case UserSocialType.github:
|
||||
sid = user.get('id')
|
||||
username = user.get('login')
|
||||
nickname = user.get('name')
|
||||
case UserSocialType.google:
|
||||
sid = user.get('id')
|
||||
username = user.get('name')
|
||||
nickname = user.get('given_name')
|
||||
avatar = user.get('picture')
|
||||
case UserSocialType.linux_do:
|
||||
sid = user.get('id')
|
||||
nickname = user.get('name')
|
||||
case _:
|
||||
raise errors.ForbiddenError(msg=f'暂不支持 {social} OAuth2 登录')
|
||||
|
||||
if not state:
|
||||
raise errors.ForbiddenError(msg='OAuth2 状态信息缺失')
|
||||
|
||||
state_data = await redis_client.get(f'{settings.OAUTH2_STATE_REDIS_PREFIX}:{state}')
|
||||
if not state_data:
|
||||
raise errors.ForbiddenError(msg='OAuth2 状态信息无效或缺失')
|
||||
|
||||
state_info = json.loads(state_data)
|
||||
await redis_client.delete(f'{settings.OAUTH2_STATE_REDIS_PREFIX}:{state}')
|
||||
|
||||
# 绑定流程
|
||||
if state_info.get('type') == UserSocialAuthType.binding.value:
|
||||
user_id = state_info.get('user_id')
|
||||
if not user_id:
|
||||
raise errors.ForbiddenError(msg='非法操作,OAuth2 状态信息无效')
|
||||
await user_social_service.binding_with_oauth2(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
sid=str(sid),
|
||||
source=social,
|
||||
)
|
||||
return None
|
||||
|
||||
# 登录流程
|
||||
if state_info.get('type') != UserSocialAuthType.login.value:
|
||||
raise errors.ForbiddenError(msg='OAuth2 状态信息无效')
|
||||
|
||||
return await self.login(
|
||||
db=db,
|
||||
response=response,
|
||||
background_tasks=background_tasks,
|
||||
sid=str(sid),
|
||||
source=social,
|
||||
username=username,
|
||||
nickname=nickname,
|
||||
email=email,
|
||||
avatar=avatar,
|
||||
)
|
||||
|
||||
|
||||
oauth2_service: OAuth2Service = OAuth2Service()
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.common.exception import errors
|
||||
from backend.plugin.oauth2.crud.crud_user_social import user_social_dao
|
||||
from backend.plugin.oauth2.enums import UserSocialType
|
||||
|
||||
|
||||
class UserSocialService:
|
||||
@staticmethod
|
||||
async def unbinding(*, db: AsyncSession, user_id: int, source: UserSocialType) -> int:
|
||||
"""
|
||||
解绑用户社交账号
|
||||
|
||||
:param db: 数据库会话
|
||||
:param user_id: 用户 ID
|
||||
:param source: 解绑源
|
||||
:return:
|
||||
"""
|
||||
bind = user_social_dao.check_binding(db, user_id, source.value)
|
||||
if not bind:
|
||||
raise errors.NotFoundError(msg=f'用户未绑定 {source.value} 账号')
|
||||
return await user_social_dao.delete(db, user_id, source.value)
|
||||
|
||||
|
||||
user_social_service: UserSocialService = UserSocialService()
|
||||
@@ -0,0 +1,106 @@
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.common.exception import errors
|
||||
from backend.core.conf import settings
|
||||
from backend.database.redis import redis_client
|
||||
from backend.plugin.oauth2.crud.crud_user_social import user_social_dao
|
||||
from backend.plugin.oauth2.enums import UserSocialAuthType, UserSocialType
|
||||
from backend.plugin.oauth2.schema.user_social import CreateUserSocialParam
|
||||
|
||||
|
||||
class UserSocialService:
|
||||
@staticmethod
|
||||
async def get_bindings(*, db: AsyncSession, user_id: int) -> list[str]:
|
||||
"""
|
||||
获取用户已绑定的社交账号
|
||||
|
||||
:param db: 数据库会话
|
||||
:param user_id: 用户 ID
|
||||
:return: 绑定列表,每个元素包含 sid、source 等信息
|
||||
"""
|
||||
bindings = await user_social_dao.get_by_user_id(db, user_id)
|
||||
return [binding.source for binding in bindings]
|
||||
|
||||
@staticmethod
|
||||
async def binding_with_oauth2(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
sid: str,
|
||||
source: UserSocialType,
|
||||
) -> None:
|
||||
"""
|
||||
通过 OAuth2 流程绑定用户社交账号
|
||||
|
||||
:param db: 数据库会话
|
||||
:param user_id: 用户 ID
|
||||
:param sid: 社交账号唯一编码
|
||||
:param source: 绑定源
|
||||
:return:
|
||||
"""
|
||||
if await user_social_dao.check_binding(db, user_id, source.value):
|
||||
raise errors.RequestError(msg=f'用户已绑定 {source.value} 账号')
|
||||
|
||||
if await user_social_dao.get_by_sid(db, sid, source.value):
|
||||
raise errors.RequestError(msg=f'该 {source.value} 账号已被其他用户绑定')
|
||||
|
||||
new_user_social = CreateUserSocialParam(sid=sid, source=source.value, user_id=user_id)
|
||||
await user_social_dao.create(db, new_user_social)
|
||||
|
||||
@staticmethod
|
||||
async def unbinding(*, db: AsyncSession, user_id: int, source: UserSocialType) -> int:
|
||||
"""
|
||||
解绑用户社交账号
|
||||
|
||||
:param db: 数据库会话
|
||||
:param user_id: 用户 ID
|
||||
:param source: 解绑源
|
||||
:return:
|
||||
"""
|
||||
bind = await user_social_dao.check_binding(db, user_id, source.value)
|
||||
if not bind:
|
||||
raise errors.NotFoundError(msg=f'用户未绑定 {source.value} 账号')
|
||||
return await user_social_dao.delete(db, user_id, source.value)
|
||||
|
||||
@staticmethod
|
||||
async def get_binding_auth_url(*, user_id: int, source: UserSocialType) -> str:
|
||||
state = str(uuid.uuid4())
|
||||
|
||||
await redis_client.setex(
|
||||
f'{settings.OAUTH2_STATE_REDIS_PREFIX}:{state}',
|
||||
settings.OAUTH2_STATE_EXPIRE_SECONDS,
|
||||
json.dumps({'type': UserSocialAuthType.binding.value, 'user_id': user_id}),
|
||||
)
|
||||
|
||||
match source:
|
||||
case UserSocialType.github:
|
||||
from backend.plugin.oauth2.api.v1.github import github_client
|
||||
|
||||
auth_url = await github_client.get_authorization_url(
|
||||
redirect_uri=settings.OAUTH2_GITHUB_REDIRECT_URI,
|
||||
state=state,
|
||||
)
|
||||
case UserSocialType.google:
|
||||
from backend.plugin.oauth2.api.v1.google import google_client
|
||||
|
||||
auth_url = await google_client.get_authorization_url(
|
||||
redirect_uri=settings.OAUTH2_GOOGLE_REDIRECT_URI,
|
||||
state=state,
|
||||
)
|
||||
case UserSocialType.linux_do:
|
||||
from backend.plugin.oauth2.api.v1.linux_do import linux_do_client
|
||||
|
||||
auth_url = await linux_do_client.get_authorization_url(
|
||||
redirect_uri=settings.OAUTH2_LINUX_DO_REDIRECT_URI,
|
||||
state=state,
|
||||
)
|
||||
case _:
|
||||
raise errors.ForbiddenError(msg=f'暂不支持 {source} 绑定')
|
||||
|
||||
return auth_url
|
||||
|
||||
|
||||
user_social_service: UserSocialService = UserSocialService()
|
||||
Reference in New Issue
Block a user