Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion backend/core/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,10 +231,13 @@ class Settings(BaseSettings):
OAUTH2_LINUX_DO_CLIENT_SECRET: str

# 基础配置
OAUTH2_STATE_REDIS_PREFIX: str = 'fba:oauth2:state'
OAUTH2_STATE_EXPIRE_SECONDS: int = 60 * 3 # 3 分钟
OAUTH2_GITHUB_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/oauth2/github/callback'
OAUTH2_GOOGLE_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/oauth2/google/callback'
OAUTH2_LINUX_DO_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/oauth2/linux-do/callback'
OAUTH2_FRONTEND_REDIRECT_URI: str = 'http://localhost:5173/oauth2/callback'
OAUTH2_FRONTEND_LOGIN_REDIRECT_URI: str = 'http://localhost:5173/oauth2/callback'
OAUTH2_FRONTEND_BINDING_REDIRECT_URI: str = 'http://localhost:5173/profile'

##################################################
# [ Plugin ] email
Expand Down
2 changes: 2 additions & 0 deletions backend/plugin/oauth2/api/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
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'])
29 changes: 24 additions & 5 deletions backend/plugin/oauth2/api/v1/github.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import json
import uuid

from typing import Annotated

from fastapi import APIRouter, BackgroundTasks, Depends, Response
Expand All @@ -8,7 +11,8 @@
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()
Expand All @@ -18,7 +22,15 @@

@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)


Expand All @@ -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}',
)
29 changes: 24 additions & 5 deletions backend/plugin/oauth2/api/v1/google.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import json
import uuid

from typing import Annotated

from fastapi import APIRouter, BackgroundTasks, Depends, Response
Expand All @@ -8,7 +11,8 @@
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()
Expand All @@ -18,7 +22,15 @@

@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)


Expand All @@ -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}',
)
31 changes: 26 additions & 5 deletions backend/plugin/oauth2/api/v1/linux_do.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import json
import uuid

from typing import Annotated

from fastapi import APIRouter, BackgroundTasks, Depends, Response
Expand All @@ -8,7 +11,8 @@
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()
Expand All @@ -18,7 +22,17 @@

@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)


Expand All @@ -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}',
)
20 changes: 16 additions & 4 deletions backend/plugin/oauth2/api/v1/user_social.py
Original file line number Diff line number Diff line change
@@ -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()
14 changes: 13 additions & 1 deletion backend/plugin/oauth2/crud/crud_user_social.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from collections.abc import Sequence

from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus

Expand All @@ -24,12 +26,22 @@ async def get_by_sid(self, db: AsyncSession, sid: str, source: str) -> UserSocia
通过 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:
"""
创建用户社交账号绑定
Expand Down
9 changes: 8 additions & 1 deletion backend/plugin/oauth2/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
class UserSocialType(StrEnum):
"""用户社交类型"""

github = 'GitHub'
github = 'Github'
google = 'Google'
linux_do = 'LinuxDo'


class UserSocialAuthType(StrEnum):
"""用户社交授权类型"""

login = 'login'
binding = 'binding'
2 changes: 1 addition & 1 deletion backend/plugin/oauth2/plugin.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[plugin]
summary = 'OAuth 2.0'
version = '0.0.9'
version = '0.0.10'
description = '通过 OAuth 2.0 的方式登录系统'
author = 'wu-clan'

Expand Down
10 changes: 9 additions & 1 deletion backend/plugin/oauth2/schema/user_social.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,3 +19,11 @@ class CreateUserSocialParam(UserSocialSchemaBase):

class UpdateUserSocialParam(SchemaBase):
"""更新用户社交参数"""


class GetUserSocialDetail(CreateUserSocialParam):
"""获取用户社交详情"""

model_config = ConfigDict(from_attributes=True)

id: int = Field(description='用户社交 ID')
Loading