From 474a3131b6c8fc60b88c9a3bd676a6a767ef2728 Mon Sep 17 00:00:00 2001 From: Ehco1996 Date: Sat, 13 Jan 2024 22:03:32 +0800 Subject: [PATCH] feat: support tg login --- apps/sspanel/forms.py | 12 +++ ...21_alter_ticketmessage_options_and_more.py | 38 ++++++++++ apps/sspanel/models.py | 19 +++-- apps/sspanel/views.py | 73 +++++++++++-------- configs/default/sites.py | 1 + templates/web/login.html | 2 +- templates/web/telegram_login.html | 2 +- 7 files changed, 107 insertions(+), 40 deletions(-) create mode 100644 apps/sspanel/migrations/0021_alter_ticketmessage_options_and_more.py diff --git a/apps/sspanel/forms.py b/apps/sspanel/forms.py index bf6569c0e2..95af4e03de 100644 --- a/apps/sspanel/forms.py +++ b/apps/sspanel/forms.py @@ -149,6 +149,18 @@ class TGLoginForm(forms.Form): ), ) + tg_user_id = forms.CharField( + required=True, + label="TG ID", + widget=forms.TextInput( + attrs={ + "class": "input is-primary", + "placeholder": "TG ID", + "readonly": "readonly", + } + ), + ) + def clean(self): if not self.is_valid(): raise forms.ValidationError("用户名和密码为必填项") diff --git a/apps/sspanel/migrations/0021_alter_ticketmessage_options_and_more.py b/apps/sspanel/migrations/0021_alter_ticketmessage_options_and_more.py new file mode 100644 index 0000000000..3e7701061d --- /dev/null +++ b/apps/sspanel/migrations/0021_alter_ticketmessage_options_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.6 on 2024-01-13 13:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("sspanel", "0020_ticketmessage"), + ] + + operations = [ + migrations.AlterModelOptions( + name="ticketmessage", + options={ + "ordering": ("ticket", "created_at"), + "verbose_name": "工单回复", + "verbose_name_plural": "工单回复", + }, + ), + migrations.AlterUniqueTogether( + name="usersocialprofile", + unique_together=set(), + ), + migrations.AddField( + model_name="usersocialprofile", + name="platform_user_id", + field=models.CharField( + blank=True, max_length=32, null=True, verbose_name="用户ID" + ), + ), + migrations.AlterUniqueTogether( + name="usersocialprofile", + unique_together={ + ("platform", "platform_user_id", "user_id"), + ("platform", "platform_user_id"), + }, + ), + ] diff --git a/apps/sspanel/models.py b/apps/sspanel/models.py index 621c1cf1bb..5c3d6db4c3 100644 --- a/apps/sspanel/models.py +++ b/apps/sspanel/models.py @@ -299,6 +299,7 @@ class UserSocialProfile(models.Model, UserMixin): "平台", default=TYPE_TG, choices=TYPE_CHOICES, max_length=32 ) platform_username = models.CharField("用户名", max_length=32) + platform_user_id = models.CharField("用户ID", max_length=32, null=True, blank=True) created_at = models.DateTimeField( auto_now_add=True, db_index=True, help_text="创建时间", verbose_name="创建时间" ) @@ -309,16 +310,18 @@ class Meta: verbose_name = "用户社交资料" verbose_name_plural = "用户社交资料" unique_together = [ - ["platform", "platform_username"], - ["platform", "platform_username", "user_id"], + ["platform", "platform_user_id"], + ["platform", "platform_user_id", "user_id"], ] @classmethod - def get_or_create_and_update_info(cls, platform, username, data): + def get_or_create_and_update_info( + cls, platform, platform_user_id, platform_username, data + ): usp, _ = cls.objects.get_or_create( platform=platform, - platform_username=username, - defaults={"raw_auth_data": data}, + platform_user_id=platform_user_id, + defaults={"raw_auth_data": data, "platform_username": platform_username}, ) # update auth info usp.raw_auth_data = data @@ -330,8 +333,10 @@ def list_by_user_id(cls, user_id): return cls.objects.filter(user_id=user_id) @classmethod - def get_by_platform(cls, platform, username): - return cls.objects.filter(platform=platform, platform_username=username).first() + def get_by_platform_user_id(cls, platform, platform_user_id): + return cls.objects.filter( + platform=platform, platform_user_id=platform_user_id + ).first() def bind(self, user): self.user_id = user.id diff --git a/apps/sspanel/views.py b/apps/sspanel/views.py index 7a5fe0e1b8..2f4d974030 100644 --- a/apps/sspanel/views.py +++ b/apps/sspanel/views.py @@ -104,9 +104,15 @@ def get(self, request): class TelegramLoginView(View): + def _find_tg_username(self, auth_data): + for key in ["username", "first_name", "last_name"]: + if auth_data.get(key): + return auth_data[key] + return "" + def get(self, request): try: - result = verify_telegram_authentication( + auth_data = verify_telegram_authentication( bot_token=settings.TELEGRAM_BOT_TOKEN, request_data=request.GET ) except TelegramDataIsOutdatedError: @@ -117,19 +123,16 @@ def get(self, request): return HttpResponseBadRequest("The data is not related to Telegram!") except Exception as e: return HttpResponseBadRequest(str(e)) - - if "username" in result: - tg_username = result["username"] - else: - tg_username = ( - result.get("first_name", "") + " " + result.get("last_name", "") - ) - + try: + tg_user_id = auth_data["id"] + except KeyError: + return HttpResponseBadRequest(f"The data is not valid {auth_data}") + tg_username = self._find_tg_username(auth_data) # 已经绑定过了 usp = UserSocialProfile.get_or_create_and_update_info( - UserSocialProfile.TYPE_TG, tg_username, result + UserSocialProfile.TYPE_TG, tg_user_id, tg_username, auth_data ) - if usp.user_id: + if usp.user_id and usp.user.is_active: login( request, usp.user, @@ -137,32 +140,40 @@ def get(self, request): ) messages.success(request, "自动跳转到用户中心", extra_tags="登录成功!") return HttpResponseRedirect(reverse("sspanel:userinfo")) - # 需要渲染绑定页面 + # user not found 渲染绑定页面 context = { - "form": TGLoginForm(initial={"tg_username": tg_username}), + "form": TGLoginForm( + initial={"tg_username": tg_username, "tg_user_id": tg_user_id} + ), } return render(request, "web/telegram_login.html", context) def post(self, request): form = TGLoginForm(request.POST) - if form.is_valid(): - user = authenticate( - username=form.cleaned_data["username"], - password=form.cleaned_data["password"], - ) - usp = UserSocialProfile.get_by_platform( - UserSocialProfile.TYPE_TG, form.cleaned_data["tg_username"] - ) - if user and user.is_active and usp: - with transaction.atomic(): - login(request, user) - usp.bind(user) - messages.success(request, "自动跳转到用户中心", extra_tags="绑定成功!") - return HttpResponseRedirect(reverse("sspanel:userinfo")) - else: - messages.error(request, "账户不存在(请先注册)/密码不正确!", extra_tags="绑定失败!") - - return HttpResponseRedirect(reverse("sspanel:login")) + if not form.is_valid(): + return HttpResponseBadRequest("表单数据不合法") + user = authenticate( + username=form.cleaned_data["username"], + password=form.cleaned_data["password"], + ) + if not user: + messages.error(request, "账户不存在(请先注册)/密码不正确!", extra_tags="绑定失败!") + return HttpResponseRedirect(reverse("sspanel:login")) + usp = UserSocialProfile.get_by_platform_user_id( + UserSocialProfile.TYPE_TG, form.cleaned_data["tg_user_id"] + ) + if not usp: + messages.error(request, "请先登录TG账号!", extra_tags="绑定失败!") + return HttpResponseRedirect(reverse("sspanel:login")) + elif usp.user_id: + messages.error(request, "该TG账号已经绑定过了!", extra_tags="绑定失败!") + return HttpResponseRedirect(reverse("sspanel:login")) + else: + with transaction.atomic(): + login(request, user) + usp.bind(user) + messages.success(request, "自动跳转到用户中心", extra_tags="绑定成功!") + return HttpResponseRedirect(reverse("sspanel:userinfo")) class UserLogOutView(View): diff --git a/configs/default/sites.py b/configs/default/sites.py index c36f7c3dd0..7c90f71894 100644 --- a/configs/default/sites.py +++ b/configs/default/sites.py @@ -13,6 +13,7 @@ SITE_HOST = os.getenv("SITE_HOST", "http://127.0.0.1:8000") CORS_ALLOWED_ORIGINS = [SITE_HOST] # django-cors-headers CSRF_TRUSTED_ORIGINS = [SITE_HOST] # django built-in +SECURE_CROSS_ORIGIN_OPENER_POLICY = "same-origin-allow-popups" # 网站密钥 SECRET_KEY = os.getenv("SECRET_KEY", "aasdasdas") diff --git a/templates/web/login.html b/templates/web/login.html index 8e493d854b..bf635dce36 100644 --- a/templates/web/login.html +++ b/templates/web/login.html @@ -40,7 +40,7 @@

登录:

{% settings_value "TELEGRAM_BOT_NAME" as tg_bot_name %} {% settings_value "TELEGRAM_LOGIN_REDIRECT_URL" as tg_redirect_url %} - {% if tg_bot_name %} + {% if tg_bot_name and request.get_host in tg_redirect_url %}