diff --git a/backend/.env.example b/backend/.env.example index 75fcf70d..8316e3a1 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -18,6 +18,8 @@ OPERA_LOG_ENCRYPT_SECRET_KEY='d77b25790a804c2b4a339dd0207941e4cefa5751935a33735b # OAuth2 OAUTH2_GITHUB_CLIENT_ID='test' OAUTH2_GITHUB_CLIENT_SECRET='test' +OAUTH2_LINUX_DO_CLIENT_ID='test' +OAUTH2_LINUX_DO_CLIENT_SECRET='test' # Task # Celery CELERY_BROKER_REDIS_DATABASE=1 diff --git a/backend/app/admin/api/v1/auth2/__init__.py b/backend/app/admin/api/v1/auth2/__init__.py index 8c0642a2..1ea7bb02 100644 --- a/backend/app/admin/api/v1/auth2/__init__.py +++ b/backend/app/admin/api/v1/auth2/__init__.py @@ -3,7 +3,9 @@ from fastapi import APIRouter from backend.app.admin.api.v1.auth2.github import router as github_router +from backend.app.admin.api.v1.auth2.linux_do import router as linux_do_router router = APIRouter(prefix='/auth2') -router.include_router(github_router, prefix='/github', tags=['OAuth2']) +router.include_router(github_router, prefix='/github', tags=['GitHub OAuth2']) +router.include_router(linux_do_router, prefix='/linuxdo', tags=['Linux Do OAuth2']) diff --git a/backend/app/admin/api/v1/auth2/github.py b/backend/app/admin/api/v1/auth2/github.py index 0e4c8563..e7b30b9a 100644 --- a/backend/app/admin/api/v1/auth2/github.py +++ b/backend/app/admin/api/v1/auth2/github.py @@ -1,35 +1,42 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- from fastapi import APIRouter, BackgroundTasks, Depends, Request +from fastapi_limiter.depends import RateLimiter from fastapi_oauth20 import FastAPIOAuth20, GitHubOAuth20 from backend.app.admin.conf import admin_settings -from backend.app.admin.service.github_service import github_service +from backend.app.admin.service.oauth2_service import oauth2_service +from backend.common.enums import UserSocialType from backend.common.response.response_schema import ResponseModel, response_base router = APIRouter() -github_client = GitHubOAuth20(admin_settings.OAUTH2_GITHUB_CLIENT_ID, admin_settings.OAUTH2_GITHUB_CLIENT_SECRET) -github_oauth2 = FastAPIOAuth20(github_client, admin_settings.OAUTH2_GITHUB_REDIRECT_URI) +_github_client = GitHubOAuth20(admin_settings.OAUTH2_GITHUB_CLIENT_ID, admin_settings.OAUTH2_GITHUB_CLIENT_SECRET) +_github_oauth2 = FastAPIOAuth20(_github_client, admin_settings.OAUTH2_GITHUB_REDIRECT_URI) @router.get('', summary='获取 Github 授权链接') async def github_auth2() -> ResponseModel: - auth_url = await github_client.get_authorization_url(redirect_uri=admin_settings.OAUTH2_GITHUB_REDIRECT_URI) + auth_url = await _github_client.get_authorization_url(redirect_uri=admin_settings.OAUTH2_GITHUB_REDIRECT_URI) return await response_base.success(data=auth_url) @router.get( '/callback', - summary='Github 授权重定向', + summary='Github 授权自动重定向', description='Github 授权后,自动重定向到当前地址并获取用户信息,通过用户信息自动创建系统用户', - response_model=None, + dependencies=[Depends(RateLimiter(times=5, minutes=1))], ) async def github_login( - request: Request, background_tasks: BackgroundTasks, oauth: FastAPIOAuth20 = Depends(github_oauth2) + request: Request, background_tasks: BackgroundTasks, oauth2: FastAPIOAuth20 = Depends(_github_oauth2) ) -> ResponseModel: - token, _state = oauth + token, _state = oauth2 access_token = token['access_token'] - user = await github_client.get_userinfo(access_token) - data = await github_service.create_with_login(request, background_tasks, user) + user = await _github_client.get_userinfo(access_token) + data = await oauth2_service.create_with_login( + request=request, + background_tasks=background_tasks, + user=user, + social=UserSocialType.github, + ) return await response_base.success(data=data) diff --git a/backend/app/admin/api/v1/auth2/linux_do.py b/backend/app/admin/api/v1/auth2/linux_do.py new file mode 100644 index 00000000..8b822652 --- /dev/null +++ b/backend/app/admin/api/v1/auth2/linux_do.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from fastapi import APIRouter, BackgroundTasks, Depends, Request +from fastapi_limiter.depends import RateLimiter +from fastapi_oauth20 import FastAPIOAuth20, LinuxDoOAuth20 + +from backend.app.admin.conf import admin_settings +from backend.app.admin.service.oauth2_service import oauth2_service +from backend.common.enums import UserSocialType +from backend.common.response.response_schema import ResponseModel, response_base + +router = APIRouter() + +_linux_do_client = LinuxDoOAuth20( + admin_settings.OAUTH2_LINUX_DO_CLIENT_ID, + admin_settings.OAUTH2_LINUX_DO_CLIENT_SECRET, +) +_linux_do_oauth2 = FastAPIOAuth20(_linux_do_client, admin_settings.OAUTH2_LINUX_DO_REDIRECT_URI) + + +@router.get('', summary='获取 Linux Do 授权链接') +async def linux_do_auth2() -> ResponseModel: + auth_url = await _linux_do_client.get_authorization_url(redirect_uri=admin_settings.OAUTH2_GITHUB_REDIRECT_URI) + return await response_base.success(data=auth_url) + + +@router.get( + '/callback', + summary='Linux Do 授权自动重定向', + description='Linux Do 授权后,自动重定向到当前地址并获取用户信息,通过用户信息自动创建系统用户', + dependencies=[Depends(RateLimiter(times=5, minutes=1))], +) +async def linux_do_login( + request: Request, background_tasks: BackgroundTasks, oauth2: FastAPIOAuth20 = Depends(_linux_do_oauth2) +) -> ResponseModel: + token, _state = oauth2 + access_token = token['access_token'] + user = await _linux_do_client.get_userinfo(access_token) + data = await oauth2_service.create_with_login( + request=request, + background_tasks=background_tasks, + user=user, + social=UserSocialType.linuxdo, + ) + return await response_base.success(data=data) diff --git a/backend/app/admin/conf.py b/backend/app/admin/conf.py index 8314604d..ab5663ba 100644 --- a/backend/app/admin/conf.py +++ b/backend/app/admin/conf.py @@ -13,11 +13,15 @@ class AdminSettings(BaseSettings): model_config = SettingsConfigDict(env_file=f'{BasePath}/.env', env_file_encoding='utf-8', extra='ignore') # OAuth2:https://github.com/fastapi-practices/fastapi_oauth20 + # GitHub OAUTH2_GITHUB_CLIENT_ID: str OAUTH2_GITHUB_CLIENT_SECRET: str + OAUTH2_GITHUB_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/auth2/github/callback' - # OAuth2 - OAUTH2_GITHUB_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/auth/github/callback' + # Linux Do + OAUTH2_LINUX_DO_CLIENT_ID: str + OAUTH2_LINUX_DO_CLIENT_SECRET: str + OAUTH2_LINUX_DO_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/auth2/linuxdo/callback' # Captcha CAPTCHA_LOGIN_REDIS_PREFIX: str = 'fba_login_captcha' diff --git a/backend/app/admin/service/auth_service.py b/backend/app/admin/service/auth_service.py index 073f4b07..59e78afd 100644 --- a/backend/app/admin/service/auth_service.py +++ b/backend/app/admin/service/auth_service.py @@ -29,7 +29,7 @@ from backend.utils.timezone import timezone class AuthService: @staticmethod - async def swagger_login(obj: HTTPBasicCredentials) -> tuple[str, User]: + async def swagger_login(*, obj: HTTPBasicCredentials) -> tuple[str, User]: async with async_db_session.begin() as db: current_user = await user_dao.get_by_username(db, obj.username) if not current_user: diff --git a/backend/app/admin/service/github_service.py b/backend/app/admin/service/oauth2_service.py similarity index 67% rename from backend/app/admin/service/github_service.py rename to backend/app/admin/service/oauth2_service.py index 17af43fa..82397ed9 100644 --- a/backend/app/admin/service/github_service.py +++ b/backend/app/admin/service/oauth2_service.py @@ -18,39 +18,40 @@ from backend.database.db_redis import redis_client from backend.utils.timezone import timezone -class GithubService: +class OAuth2Service: @staticmethod async def create_with_login( - request: Request, background_tasks: BackgroundTasks, user: dict + *, request: Request, background_tasks: BackgroundTasks, user: dict, social: UserSocialType ) -> GetLoginToken | None: async with async_db_session.begin() as db: - github_email = user['email'] - if not github_email: - raise AuthorizationError(msg='授权失败,GitHub 账户未绑定邮箱') - github_id = user['id'] - github_username = user['login'] - github_nickname = user['name'] - sys_user = await user_dao.check_email(db, github_email) + # 获取 OAuth2 平台用户信息 + _id = user.get('id') + _username = user.get('username') + if social == UserSocialType.github: + _username = user.get('login') + _nickname = user.get('name') + _email = user.get('email') + if social == UserSocialType.linuxdo: + _email = f'{_username}@linux.do' + if not _email: + raise AuthorizationError(msg=f'授权失败,{social.value} 账户未绑定邮箱') + # 创建系统用户 + sys_user = await user_dao.check_email(db, _email) if not sys_user: - # 创建系统用户 - sys_user = await user_dao.get_by_username(db, github_username) + sys_user = await user_dao.get_by_username(db, _username) if sys_user: - github_username = f'{github_username}{text_captcha(5)}' - sys_user = await user_dao.get_by_nickname(db, github_nickname) + _username = f'{_username}#{text_captcha(5)}' + sys_user = await user_dao.get_by_nickname(db, _nickname) if sys_user: - github_nickname = f'{github_nickname}{text_captcha(5)}' - new_sys_user = RegisterUserParam( - username=github_username, password=None, nickname=github_nickname, email=github_email - ) + _nickname = f'{_nickname}#{text_captcha(5)}' + new_sys_user = RegisterUserParam(username=_username, password=None, nickname=_nickname, email=_email) await user_dao.create(db, new_sys_user, social=True) await db.flush() - sys_user = await user_dao.check_email(db, github_email) + sys_user = await user_dao.check_email(db, _email) # 绑定社交用户 user_social = await user_social_dao.get(db, sys_user.id, UserSocialType.github) if not user_social: - new_user_social = CreateUserSocialParam( - source=UserSocialType.github, uid=str(github_id), user_id=sys_user.id - ) + new_user_social = CreateUserSocialParam(source=social.value, uid=str(_id), user_id=sys_user.id) await user_social_dao.create(db, new_user_social) # 创建 token access_token, access_token_expire_time = await jwt.create_access_token( @@ -81,4 +82,4 @@ class GithubService: return data -github_service = GithubService() +oauth2_service = OAuth2Service() diff --git a/backend/common/enums.py b/backend/common/enums.py index 95cde903..352f3553 100644 --- a/backend/common/enums.py +++ b/backend/common/enums.py @@ -87,3 +87,4 @@ class UserSocialType(StrEnum): """用户社交类型""" github = 'GitHub' + linuxdo = 'LinuxDo' diff --git a/backend/pdm.lock b/backend/pdm.lock index 6195d3d3..a0f7a83e 100644 --- a/backend/pdm.lock +++ b/backend/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "lint", "deploy"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.4.1" -content_hash = "sha256:c2a3e19bef234f542b76f4874890f8452272fafc6b2fa06dca7ca6a0a3a74f3a" +content_hash = "sha256:3b713f796e95f9f7ce4b66a840c900aa518ddcaa911d73bdc0f40594a9ac5725" [[package]] name = "alembic" @@ -537,7 +537,7 @@ files = [ [[package]] name = "fastapi-oauth20" -version = "0.0.1a1" +version = "0.0.1a2" requires_python = ">=3.10" summary = "在 FastAPI 中异步授权 OAuth2 客户端" groups = ["default"] @@ -546,8 +546,8 @@ dependencies = [ "httpx>=0.18.0", ] files = [ - {file = "fastapi_oauth20-0.0.1a1-py3-none-any.whl", hash = "sha256:02247a49f1c9ffc364d13857dc29abf49783f36bbefa4632e196aca778f58ede"}, - {file = "fastapi_oauth20-0.0.1a1.tar.gz", hash = "sha256:f3d2eda24c10fdfe81735859f0346a8bdb687246193134bc285c8110daf1c221"}, + {file = "fastapi_oauth20-0.0.1a2-py3-none-any.whl", hash = "sha256:2567ad6129646a775f949e78ec33ea1d14de9db4e63572e2424ba4fdb5b6d18f"}, + {file = "fastapi_oauth20-0.0.1a2.tar.gz", hash = "sha256:516ddd5c631f4efc918d863a26e923318f7b4ada80335666cd42b83a761625a7"}, ] [[package]] diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 42dad950..73299924 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -38,7 +38,7 @@ dependencies = [ "user-agents==2.2.0", "uvicorn[standard]==0.29.0", "XdbSearchIP==1.0.2", - "fastapi_oauth20>=0.0.1a1", + "fastapi_oauth20>=0.0.1a2", "flower==2.0.1", "sqlalchemy-crud-plus==0.0.2", ] diff --git a/backend/requirements.txt b/backend/requirements.txt index b49bc1be..c2158c74 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -32,7 +32,7 @@ fast-captcha==0.2.1 fastapi==0.111.0 fastapi-cli==0.0.2 fastapi-limiter==0.1.6 -fastapi-oauth20==0.0.1a1 +fastapi-oauth20==0.0.1a2 fastapi-pagination==0.12.13 filelock==3.13.1 flower==2.0.1