diff --git a/apps/account/constants.py b/apps/account/constants.py index 5e97f24..f5b00d4 100644 --- a/apps/account/constants.py +++ b/apps/account/constants.py @@ -7,6 +7,8 @@ PHONE_VERIFY_CODE_TIMEOUT = 60 * 60 # second PHONE_VERIFY_CODE_KEY = "phone_verify_code:{phone_number}" +LOGIN_CODE_KEY = "login_code:{code}" + WECHAT_LOGIN_STATE_KEY = "wechat_login_state:{state}" WECHAT_USER_INFO_KEY = "wechat_user_info:{code}" diff --git a/apps/account/models.py b/apps/account/models.py index 4fc2345..54da7f4 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -1,15 +1,12 @@ import abc import hashlib -from importlib import import_module from typing import Union from django.conf import settings -from django.contrib.auth import SESSION_KEY from django.contrib.auth.base_user import AbstractBaseUser from django.contrib.auth.hashers import make_password from django.contrib.auth.models import AbstractUser, AnonymousUser, PermissionsMixin from django.contrib.auth.models import UserManager as _UserManager -from django.contrib.sessions.backends.cache import SessionStore from django.core.cache import cache from django.db import models from django.utils import timezone @@ -21,9 +18,10 @@ SHORT_CHAR_LENGTH, ) from ovinc_client.core.models import SoftDeletedManager, SoftDeletedModel -from ovinc_client.core.utils import num_code +from ovinc_client.core.utils import num_code, uniq_id from apps.account.constants import ( + LOGIN_CODE_KEY, PHONE_VERIFY_CODE_KEY, PHONE_VERIFY_CODE_LENGTH, PHONE_VERIFY_CODE_TIMEOUT, @@ -98,22 +96,27 @@ class Meta: verbose_name_plural = verbose_name ordering = ["username"] + def generate_oauth_code(self) -> str: + """ + Generate OAuth User Code + """ + + code = uniq_id() + cache_key = LOGIN_CODE_KEY.format(code=code) + cache.set(cache_key, self.username, timeout=settings.OAUTH_CODE_TIMEOUT) + return code + @classmethod - def check_ticket(cls, ticket: str) -> (bool, Union[models.Model, None]): + def check_oauth_code(cls, code: str) -> (bool, Union[models.Model, None]): """ - Check Ticket + Check OAuth User Code """ - engine = import_module(settings.SESSION_ENGINE) - session: SessionStore = engine.SessionStore(session_key=ticket) - if not session.load(): - return False, None - user_id = session.get(SESSION_KEY) - if not user_id: - return False, None + cache_key = LOGIN_CODE_KEY.format(code=code) + username = cache.get(cache_key) + cache.delete(cache_key) try: - user = cls.objects.get(pk=user_id) - return True, user + return True, cls.objects.get(username=username) except cls.DoesNotExist: # pylint: disable=E1101 return False, None diff --git a/apps/account/serializers.py b/apps/account/serializers.py index e999dc7..bc896f7 100644 --- a/apps/account/serializers.py +++ b/apps/account/serializers.py @@ -38,6 +38,7 @@ class SignInSerializer(Serializer): username = serializers.CharField(label=gettext_lazy("Username")) password = serializers.CharField(label=gettext_lazy("Password")) + is_oauth = serializers.BooleanField(label=gettext_lazy("Is OAuth"), default=False) wechat_code = serializers.CharField(label=gettext_lazy("WeChat Code"), required=False) tcaptcha = serializers.JSONField(label=gettext_lazy("Tencent Captcha"), required=False, default=dict) @@ -56,6 +57,7 @@ class UserRegistrySerializer(ModelSerializer): """ username = serializers.RegexField(label=gettext_lazy("Username"), regex=USERNAME_REGEX) + is_oauth = serializers.BooleanField(label=gettext_lazy("Is OAuth"), default=False) wechat_code = serializers.CharField(label=gettext_lazy("WeChat Code"), required=False) phone_area = serializers.ChoiceField(label=gettext_lazy("Phone Area"), choices=PhoneNumberAreas.choices) phone_number = serializers.CharField(label=gettext_lazy("Phone Number")) @@ -72,6 +74,7 @@ class Meta: "username", "nick_name", "password", + "is_oauth", "wechat_code", "phone_area", "phone_number", @@ -105,12 +108,12 @@ def validate_phone_number(self, phone_number: str) -> str: return phone_number -class VerifyTicketRequestSerializer(Serializer): +class VerifyCodeRequestSerializer(Serializer): """ - Verify Ticket + Verify Code """ - ticket = serializers.CharField(label=gettext_lazy("Ticket")) + code = serializers.CharField(label=gettext_lazy("Code")) class WeChatLoginReqSerializer(Serializer): @@ -120,6 +123,7 @@ class WeChatLoginReqSerializer(Serializer): code = serializers.CharField(label=gettext_lazy("Code")) state = serializers.CharField(label=gettext_lazy("State")) + is_oauth = serializers.BooleanField(label=gettext_lazy("Is OAuth"), default=False) class ResetPasswordRequestSerializer(Serializer): diff --git a/apps/account/views.py b/apps/account/views.py index 27e1d02..fff7af4 100644 --- a/apps/account/views.py +++ b/apps/account/views.py @@ -36,7 +36,7 @@ SignInSerializer, UserInfoSerializer, UserRegistrySerializer, - VerifyTicketRequestSerializer, + VerifyCodeRequestSerializer, WeChatLoginReqSerializer, ) from core.auth import ApplicationAuthenticate @@ -92,6 +92,10 @@ async def sign_in(self, request, *args, **kwargs): # auth session await database_sync_to_async(auth.login)(request, user) + # oauth + if request_data["is_oauth"]: + return Response({"code": user.generate_oauth_code()}) + return Response() @action(methods=["GET"], detail=False) @@ -130,6 +134,10 @@ async def sign_up(self, request, *args, **kwargs): # login session await database_sync_to_async(auth.login)(request, user) + # oauth + if request_data["is_oauth"]: + return Response({"code": user.generate_oauth_code()}) + # response return Response() @@ -155,19 +163,27 @@ async def phone_verify_code(self, request, *args, **kwargs): # response return Response() + @action(methods=["GET"], detail=False) + async def oauth_code(self, request, *args, **kwargs): + """ + oauth code + """ + + return Response({"code": request.user.generate_oauth_code()}) + @action(methods=["POST"], detail=False, authentication_classes=[ApplicationAuthenticate]) - async def verify_ticket(self, request, *args, **kwargs): + async def verify_code(self, request, *args, **kwargs): """ - verify ticket + verify oauth code """ # validate request - request_serializer = VerifyTicketRequestSerializer(data=request.data) + request_serializer = VerifyCodeRequestSerializer(data=request.data) request_serializer.is_valid(raise_exception=True) request_data = request_serializer.validated_data # load user - is_success, user = await database_sync_to_async(USER_MODEL.check_ticket)(request_data["ticket"]) + is_success, user = await database_sync_to_async(USER_MODEL.check_oauth_code)(request_data["code"]) if is_success: return Response(await UserInfoSerializer(instance=user).adata) raise WrongToken() @@ -262,7 +278,7 @@ async def wechat_login(self, request, *args, **kwargs): if user: await self.update_user_by_wechat(user, code) await database_sync_to_async(auth.login)(request, user) - return Response() + return Response({"code": user.generate_oauth_code() if request_data["is_oauth"] else ""}) # need registry return Response({"wechat_code": code}) diff --git a/entry/settings.py b/entry/settings.py index c2e95c4..13fcb71 100644 --- a/entry/settings.py +++ b/entry/settings.py @@ -234,6 +234,9 @@ # TCI Callback TCI_AUDIT_CALLBACK_PREFIX = os.getenv("TCI_AUDIT_CALLBACK_PREFIX", "") +# OAuth +OAUTH_CODE_TIMEOUT = int(os.getenv("OAUTH_CODE_TIMEOUT", str(60 * 5))) + # WeChat WECHAT_APP_ID = os.getenv("WECHAT_APP_ID") WECHAT_APP_KEY = os.getenv("WECHAT_APP_KEY")