diff --git a/back_end/saolei/accountlink/__init__.py b/back_end/saolei/accountlink/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/back_end/saolei/accountlink/apps.py b/back_end/saolei/accountlink/apps.py new file mode 100644 index 0000000..ab71de7 --- /dev/null +++ b/back_end/saolei/accountlink/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AccountLinkConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "accountlink" \ No newline at end of file diff --git a/back_end/saolei/accountlink/migrations/__init__.py b/back_end/saolei/accountlink/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/back_end/saolei/accountlink/models.py b/back_end/saolei/accountlink/models.py new file mode 100644 index 0000000..25a6598 --- /dev/null +++ b/back_end/saolei/accountlink/models.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +from django.db import models +from userprofile.models import UserProfile + +class Platform(models.TextChoices): + MSGAMES = 'a', ('Authoritative Minesweeper') + SAOLEI = 'c', ('扫雷网') + WOM = 'w', ('Minesweeper.Online') + +# 用于验证的队列 +class AccountLinkQueue(models.Model): + platform = models.CharField(max_length=1, null=False, choices=Platform.choices) + identifier = models.CharField(max_length=128, null=False) + userprofile = models.ForeignKey(UserProfile, on_delete=models.CASCADE) + verified = models.BooleanField(default=False) + +# 网站编码 website code +# a - Authoritative Minesweeper +# c - China ranking (saolei.wang) +# g - Minesweeper GO +# l - League of Minesweeper +# s - Scoreganizer +# w - World of Minesweeper +# B - Bilibili +# D - Discord +# F - Facebook +# G - GitHub +# R - Reddit +# S - Speedrun.com +# T - Tieba +# W - Weibo +# X - X +# Y - YouTube +# Z - Zhihu + +# 扫雷网账号信息 +class AccountSaolei(models.Model): + id = models.PositiveIntegerField(primary_key=True) + parent = models.OneToOneField(UserProfile, on_delete=models.CASCADE, related_name='account_saolei') + update_time = models.DateTimeField(auto_now_add=True) + + name = models.CharField(max_length=10) # 姓名,10应该够了吧 + total_views = models.PositiveIntegerField(null=True) # 综合人气 + + beg_count = models.PositiveSmallIntegerField(null=True) # 初级录像数量 + int_count = models.PositiveSmallIntegerField(null=True) # 中级录像数量 + exp_count = models.PositiveSmallIntegerField(null=True) # 高级录像数量 + + # time纪录,单位毫秒 + b_t_ms = models.PositiveIntegerField(null=True) + i_t_ms = models.PositiveIntegerField(null=True) + e_t_ms = models.PositiveIntegerField(null=True) + s_t_ms = models.PositiveIntegerField(null=True) + + # bvs纪录,单位0.01。大概不会有人bvs超过300吧?大概吧? + b_b_cent = models.PositiveSmallIntegerField(null=True) + i_b_cent = models.PositiveSmallIntegerField(null=True) + e_b_cent = models.PositiveSmallIntegerField(null=True) + s_b_cent = models.PositiveSmallIntegerField(null=True) + +class AccountMinesweeperGames(models.Model): + id = models.PositiveIntegerField(primary_key=True) + parent = models.OneToOneField(UserProfile, on_delete=models.CASCADE, related_name='account_msgames') + update_time = models.DateTimeField(auto_now_add=True) + + name = models.CharField(max_length=128) + local_name = models.CharField(max_length=128) + # country = models.CharField() # country和state应该是二合一的枚举类型 + # state = models.CharField() + joined = models.DateField(null=True) + # mouse_brand = models.CharField(max_length=128) # 枚举 + # mouse_type = models.CharField(max_length=128) # 枚举 + # mouse_model = models.CharField() # 用户自己随便填的,需要审查 + +class AccountWorldOfMinesweeper(models.Model): + id = models.PositiveIntegerField(primary_key=True) + parent = models.OneToOneField(UserProfile, on_delete=models.CASCADE, related_name='account_wom') + update_time = models.DateTimeField(auto_now_add=True) + + # name 有的用户名过不了审 + # country 有争议 + trophy = models.PositiveSmallIntegerField(null=True) + + experience = models.PositiveIntegerField(null=True) + honour = models.PositiveIntegerField(null=True) + + minecoin = models.PositiveIntegerField(null=True) + gem = models.PositiveIntegerField(null=True) + coin = models.PositiveIntegerField(null=True) + arena_ticket = models.PositiveIntegerField(null=True) + equipment = models.PositiveIntegerField(null=True) + part = models.PositiveIntegerField(null=True) + + arena_point = models.PositiveSmallIntegerField(null=True) # 最高80 + max_difficulty = models.PositiveIntegerField(null=True) + win = models.PositiveIntegerField(null=True) + last_season = models.PositiveSmallIntegerField(null=True) + + b_t_ms = models.PositiveIntegerField(null=True) + i_t_ms = models.PositiveIntegerField(null=True) + e_t_ms = models.PositiveIntegerField(null=True) + s_t_ms = models.PositiveIntegerField(null=True) + + b_ioe = models.FloatField(null=True) + i_ioe = models.FloatField(null=True) + e_ioe = models.FloatField(null=True) + s_ioe = models.FloatField(null=True) + + b_mastery = models.PositiveSmallIntegerField(null=True) + i_mastery = models.PositiveSmallIntegerField(null=True) + e_mastery = models.PositiveSmallIntegerField(null=True) + + b_winstreak = models.PositiveSmallIntegerField(null=True) + i_winstreak = models.PositiveSmallIntegerField(null=True) + e_winstreak = models.PositiveSmallIntegerField(null=True) + + # 主页只显示一个 + # b_endurance = models.TimeField() + # i_endurance = models.TimeField() + # e_endurance = models.TimeField() \ No newline at end of file diff --git a/back_end/saolei/accountlink/urls.py b/back_end/saolei/accountlink/urls.py new file mode 100644 index 0000000..9769990 --- /dev/null +++ b/back_end/saolei/accountlink/urls.py @@ -0,0 +1,11 @@ +from django.urls import path + +from . import views +app_name = 'accountlink' +urlpatterns = [ + path('add/', views.add_link), + path('delete/', views.delete_link), + path('get/', views.get_link), + path('verify/', views.verify_link), + path('unverify/', views.unverify_link), +] \ No newline at end of file diff --git a/back_end/saolei/accountlink/utils.py b/back_end/saolei/accountlink/utils.py new file mode 100644 index 0000000..3d266bc --- /dev/null +++ b/back_end/saolei/accountlink/utils.py @@ -0,0 +1,54 @@ +from .models import AccountSaolei, AccountMinesweeperGames, AccountWorldOfMinesweeper, Platform +from datetime import datetime +from userprofile.models import UserProfile + +def update_account(platform: Platform, id, user: UserProfile | None): + if platform == Platform.SAOLEI: + update_saolei_account(id, user) + elif platform == Platform.MSGAMES: + update_msgames_account(id, user) + elif platform == Platform.WOM: + update_wom_account(id, user) + else: + ValueError() + +def delete_account(user: UserProfile, platform: Platform): + if platform == Platform.SAOLEI: + user.account_saolei.delete() + elif platform == Platform.MSGAMES: + user.account_msgames.delete() + elif platform == Platform.WOM: + user.account_wom.delete() + else: + ValueError() + +def update_saolei_account(id, user: UserProfile | None): + account = AccountSaolei.objects.filter(id=id).first() + if not account: + account = AccountSaolei.objects.create(id=id, parent=user) + elif user: + account.parent=user + account.update_time = datetime.now() + # 给account的各attribute赋值 + account.save() + +def update_msgames_account(id, user: UserProfile | None): + account = AccountMinesweeperGames.objects.filter(id=id).first() + if not account: + account = AccountMinesweeperGames.objects.create(id=id, parent=user) + elif user: + account.parent=user + account.update_time = datetime.now() + # 给account的各attribute赋值 + account.save() + +def update_wom_account(id, user: UserProfile | None): + print(user) + account = AccountWorldOfMinesweeper.objects.filter(id=id).first() + if not account: + account = AccountWorldOfMinesweeper.objects.create(id=id, parent=user) + elif user: + account.parent=user + account.update_time = datetime.now() + # 给account的各attribute赋值 + account.save() \ No newline at end of file diff --git a/back_end/saolei/accountlink/views.py b/back_end/saolei/accountlink/views.py new file mode 100644 index 0000000..27b0579 --- /dev/null +++ b/back_end/saolei/accountlink/views.py @@ -0,0 +1,96 @@ +from .models import AccountLinkQueue +from .utils import update_account, delete_account +from userprofile.models import UserProfile +from django.http import HttpResponseForbidden, HttpResponseBadRequest, JsonResponse, HttpResponse, HttpResponseNotFound +from utils.response import HttpResponseConflict +from django.views.decorators.http import require_GET, require_POST +from django_ratelimit.decorators import ratelimit +from userprofile.decorators import login_required_error, staff_required + +private_platforms = [""] # 私人账号平台 + +# 为自己绑定账号,需要指定平台和ID +@ratelimit(key='user', rate='10/d') +@require_POST +@login_required_error +def add_link(request): + user = UserProfile.objects.filter(id=request.user.id).first() + platform = request.POST.get('platform') + if platform == None: + return HttpResponseBadRequest() + accountlink = AccountLinkQueue.objects.filter(platform=platform, userprofile=user).first() + if accountlink: + return HttpResponseConflict() # 每个平台只能绑一个账号 + accountlink = AccountLinkQueue.objects.create(platform=platform, identifier=request.POST.get('identifier'), userprofile=user) + return HttpResponse() + +# 解绑自己的账号,只需要指定平台 +@require_POST +@login_required_error +def delete_link(request): + user = UserProfile.objects.filter(id=request.user.id).first() + platform = request.POST.get('platform') + if platform == None: + return HttpResponseBadRequest() + accountlink = AccountLinkQueue.objects.filter(platform=platform, userprofile=user).first() + if accountlink: + if accountlink.verified: + delete_account(user, platform) + accountlink.delete() + return HttpResponse() + return HttpResponseNotFound() + +@require_GET +def get_link(request): + userid = request.GET.get("id") + user = UserProfile.objects.filter(id=userid).first() + if request.user.is_staff or user == request.user: # 管理员或用户本人可以获得全部数据 + accountlink = AccountLinkQueue.objects.filter(userprofile=user).values("platform","identifier","verified") + else: # 其他人不能获得未绑定账号与私人账号数据 + accountlink = AccountLinkQueue.objects.filter(userprofile=user,verified=True).exclude(platform__in=private_platforms).values("platform","identifier") + return JsonResponse(list(accountlink), safe=False) + +@require_POST +@staff_required +def verify_link(request): + userid = request.POST.get("id") + user = UserProfile.objects.filter(id=userid).first() + if user == None: + return HttpResponseNotFound() + platform = request.POST.get('platform') + if platform == None: + return HttpResponseBadRequest() + identifier = request.POST.get('identifier') + if identifier == None: + return HttpResponseBadRequest() + collision = AccountLinkQueue.objects.filter(platform=platform,identifier=identifier,verified=True).first() + if collision: # 该平台该ID已被绑定 + if collision.userprofile == user: + return HttpResponse() + else: + return HttpResponseConflict() + accountlink = AccountLinkQueue.objects.filter(platform=platform,identifier=identifier).first() + if not accountlink: + return HttpResponseNotFound() + update_account(platform, identifier, user) + accountlink.verified = True + accountlink.save() + return HttpResponse() + +@require_POST +@staff_required +def unverify_link(request): + userid = request.GET.get("id") + user = UserProfile.objects.filter(id=userid).first() + if not user: + return HttpResponseNotFound() + platform = request.POST.get('platform') + identifier = request.POST.get('identifier') + accountlink = AccountLinkQueue.objects.filter(userprofile=user,platform=platform,identifier=identifier).first() + if not accountlink: + return HttpResponseNotFound() + delete_account(user, platform) + accountlink.verified = False + accountlink.save() + return HttpResponse() + \ No newline at end of file diff --git a/back_end/saolei/msuser/views.py b/back_end/saolei/msuser/views.py index c560848..02c1679 100644 --- a/back_end/saolei/msuser/views.py +++ b/back_end/saolei/msuser/views.py @@ -22,6 +22,7 @@ from datetime import datetime, timedelta from utils import verify_text from django_ratelimit.decorators import ratelimit +from django.views.decorators.http import require_GET, require_POST from config.global_settings import * @@ -38,105 +39,93 @@ def default(self, o): # 获取我的地盘里的头像、姓名、个性签名、过审标识 @ratelimit(key='ip', rate='60/h') +@require_GET def get_info(request): - if request.method == 'GET': - if not request.GET.get('id', ''): - return JsonResponse({'status': 101, 'msg': "访问谁?"}) - # 此处要重点防攻击 - # user_id = request.user.id - user_id = request.GET["id"] - try: - user = UserProfile.objects.get(id=user_id) - except: - return JsonResponse({'status': 184, 'msg': "不存在该用户!"}) - # ms_user = UserMS.objects.get(id=request.user.userms_id) - # ms_user = user.userms + user_id = request.GET.get('id') + if not user_id: + return HttpResponseBadRequest() + user = UserProfile.objects.filter(id=user_id).first() + if not user: + return HttpResponseNotFound() - user.popularity += 1 - user.save(update_fields=["popularity"]) + user.popularity += 1 + user.save(update_fields=["popularity"]) - if user.avatar: - avatar_path = os.path.join(settings.MEDIA_ROOT, urllib.parse.unquote(user.avatar.url)[7:]) - image_data = open(avatar_path, "rb").read() - image_data = base64.b64encode(image_data).decode() - else: - image_data = None - response = {"id": user_id, - "username": user.username, - "realname": user.realname, - "avatar": image_data, - "signature": user.signature, - "popularity": user.popularity, - "identifiers": user.userms.identifiers, - "is_banned": user.is_banned, - "country": user.country - } - return JsonResponse(response) + if user.avatar: + avatar_path = os.path.join(settings.MEDIA_ROOT, urllib.parse.unquote(user.avatar.url)[7:]) + image_data = open(avatar_path, "rb").read() + image_data = base64.b64encode(image_data).decode() else: - return HttpResponse("别瞎玩") + image_data = None + response = {"id": user_id, + "username": user.username, + "realname": user.realname, + "avatar": image_data, + "signature": user.signature, + "popularity": user.popularity, + "identifiers": user.userms.identifiers, + "is_banned": user.is_banned, + "country": user.country + } + return JsonResponse(response) # 获取我的地盘里的姓名、全部纪录 @ratelimit(key='ip', rate='60/h') +@require_GET def get_records(request): - if request.method == 'GET': - # 此处要重点防攻击 - if not request.GET.get('id', ''): - return JsonResponse({'status': 101, 'msg': "访问谁?"}) - user_id = request.GET["id"] - # print(user_id) - # print(type(user_id)) - try: - user = UserProfile.objects.get(id=user_id) - except: - return JsonResponse({'status': 184, 'msg': "不存在该用户!"}) - - ms_user = user.userms + user_id = request.GET.get('id') + if not user_id: + return HttpResponseBadRequest() + user = UserProfile.objects.filter(id=user_id).first() + if not user: + return HttpResponseNotFound() + ms_user = user.userms - response = {"id": user_id, "realname": user.realname} - for mode in GameModes: - value = {} - for stat in RankingGameStats: - value[stat] = ms_user.getrecords_level(stat, mode) - value[f"{stat}_id"] = ms_user.getrecordIDs_level(stat, mode) - response[f"{mode}_record"] = json.dumps(value, cls=DecimalEncoder) - return JsonResponse(response) - else: - return HttpResponse("别瞎玩") + response = {"id": user_id, "realname": user.realname} + for mode in GameModes: + value = {} + for stat in RankingGameStats: + value[stat] = ms_user.getrecords_level(stat, mode) + value[f"{stat}_id"] = ms_user.getrecordIDs_level(stat, mode) + response[f"{mode}_record"] = json.dumps(value, cls=DecimalEncoder) + return JsonResponse(response) # 鼠标移到人名上时,展现头像、姓名、id、记录 +@require_GET def get_info_abstract(request): - if request.method == 'GET': - # 此处要防攻击 - user_id = request.GET["id"] - user = UserProfile.objects.get(id=user_id) - ms_user = user.userms - if user.avatar: - avatar_path = os.path.join(settings.MEDIA_ROOT, urllib.parse.unquote(user.avatar.url)[7:]) - image_data = open(avatar_path, "rb").read() - image_data = base64.b64encode(image_data).decode() - else: - image_data = None - - response = { - "id": user_id, - "realname": user.realname, - "avatar": image_data, - "record_abstract": json.dumps({"timems": ms_user.getrecords_level("timems", "std"), - "bvs": ms_user.getrecords_level("bvs", "std"), - "timems_id": ms_user.getrecordIDs_level("timems", "std"), - "bvs_id": ms_user.getrecordIDs_level("bvs", "std")}, - cls=DecimalEncoder), - } - - return JsonResponse(response) + # 此处要防攻击 + user_id = request.GET.get('id') + if not user_id: + return HttpResponseBadRequest() + user = UserProfile.objects.filter(id=user_id).first() + if not user: + return HttpResponseNotFound() + ms_user = user.userms + + if user.avatar: + avatar_path = os.path.join(settings.MEDIA_ROOT, urllib.parse.unquote(user.avatar.url)[7:]) + image_data = open(avatar_path, "rb").read() + image_data = base64.b64encode(image_data).decode() else: - return HttpResponse("别瞎玩") + image_data = None + response = { + "id": user_id, + "realname": user.realname, + "avatar": image_data, + "record_abstract": json.dumps({"timems": ms_user.getrecords_level("timems", "std"), + "bvs": ms_user.getrecords_level("bvs", "std"), + "timems_id": ms_user.getrecordIDs_level("timems", "std"), + "bvs_id": ms_user.getrecordIDs_level("bvs", "std")}, + cls=DecimalEncoder), + } + + return JsonResponse(response) + +@require_GET def get_identifiers(request): - if request.method != 'GET': - return HttpResponseNotAllowed() id = request.GET.get('id') if not id: return HttpResponseBadRequest() @@ -148,90 +137,82 @@ def get_identifiers(request): # 上传或更新我的地盘里的头像、姓名、个性签名 # 应该写到用户的app里,而不是玩家 @login_required(login_url='/') +@require_POST def update_realname(request): - if request.method == 'POST': - user_update_realname_form = UserUpdateRealnameForm( - data=request.POST, request=request) - if user_update_realname_form.is_valid(): - realname = user_update_realname_form.cleaned_data["realname"] - user: UserProfile = request.user - if user.is_banned: - return JsonResponse({"status": 110, "msg": "用户已被封禁"}) - logger.info(f'用户 {user.username}#{user.id} 修改实名 从 "{user.realname}" 到 "{realname}"') - user.realname = realname - try: - user.save(update_fields=["realname", "left_realname_n"]) - except Exception as e: - return JsonResponse({"status": 107, "msg": "未知错误。可能原因:不支持此种字符"}) - update_cache_realname(user.id, realname) - # identifiers = json.loads(user.userms.identifiers) - # user.userms.identifiers = json.dumps(identifiers) - user.userms.save(update_fields=["identifiers"]) - return JsonResponse({"status": 100, "msg": {"n": user.left_realname_n}}) - else: - ErrorDict = json.loads(user_update_realname_form.errors.as_json()) - Error = ErrorDict[next(iter(ErrorDict))][0]['message'] - return JsonResponse({"status": 101, "msg": Error}) + user_update_realname_form = UserUpdateRealnameForm( + data=request.POST, request=request) + if user_update_realname_form.is_valid(): + realname = user_update_realname_form.cleaned_data["realname"] + user: UserProfile = request.user + if user.is_banned: + return JsonResponse({"status": 110, "msg": "用户已被封禁"}) + logger.info(f'用户 {user.username}#{user.id} 修改实名 从 "{user.realname}" 到 "{realname}"') + user.realname = realname + try: + user.save(update_fields=["realname", "left_realname_n"]) + except Exception as e: + return JsonResponse({"status": 107, "msg": "未知错误。可能原因:不支持此种字符"}) + update_cache_realname(user.id, realname) + user.userms.save(update_fields=["identifiers"]) + return JsonResponse({"status": 100, "msg": {"n": user.left_realname_n}}) else: - return HttpResponse("别瞎玩") + ErrorDict = json.loads(user_update_realname_form.errors.as_json()) + Error = ErrorDict[next(iter(ErrorDict))][0]['message'] + return JsonResponse({"status": 101, "msg": Error}) # 上传或更新我的地盘里的头像 # 应该写到用户的app里,而不是玩家 @login_required(login_url='/') +@require_POST def update_avatar(request): - if request.method == 'POST': - if request.user.userms.e_timems_std >= 200000: - return JsonResponse({"status": 177, "msg": "只允许标准高级sub200的玩家修改头像和个性签名!"}) - user_update_form = UserUpdateAvatarForm( - data=request.POST, files=request.FILES, request=request) - if user_update_form.is_valid(): - data = user_update_form.cleaned_data - user = request.user - if user.is_banned: - return JsonResponse({"status": 110, "msg": "用户已被封禁"}) - user.avatar = data["avatar"] - user.save(update_fields=["avatar", "left_avatar_n"]) - logger.info(f'用户 {user.username}#{user.id} 修改头像') - return JsonResponse({"status": 100, "msg": {"n": user.left_avatar_n}}) + if request.user.userms.e_timems_std >= 200000: + return JsonResponse({"status": 177, "msg": "只允许标准高级sub200的玩家修改头像和个性签名!"}) + user_update_form = UserUpdateAvatarForm( + data=request.POST, files=request.FILES, request=request) + if user_update_form.is_valid(): + data = user_update_form.cleaned_data + user = request.user + if user.is_banned: + return JsonResponse({"status": 110, "msg": "用户已被封禁"}) + user.avatar = data["avatar"] + user.save(update_fields=["avatar", "left_avatar_n"]) + logger.info(f'用户 {user.username}#{user.id} 修改头像') + return JsonResponse({"status": 100, "msg": {"n": user.left_avatar_n}}) - else: - ErrorDict = json.loads(user_update_form.errors.as_json()) - Error = ErrorDict[next(iter(ErrorDict))][0]['message'] - return JsonResponse({"status": 101, "msg": Error}) else: - return HttpResponse("别瞎玩") + ErrorDict = json.loads(user_update_form.errors.as_json()) + Error = ErrorDict[next(iter(ErrorDict))][0]['message'] + return JsonResponse({"status": 101, "msg": Error}) # 上传或更新我的地盘里的个性签名 # 应该写到用户的app里,而不是玩家 @login_required(login_url='/') +@require_POST def update_signature(request): - if request.method == 'POST': - if request.user.userms.e_timems_std >= 200000: - return JsonResponse({"status": 177, "msg": "只允许标准高级sub200的玩家修改头像和个性签名!"}) - user_update_form = UserUpdateSignatureForm( - data=request.POST, request=request) - if user_update_form.is_valid(): - signature = user_update_form.cleaned_data["signature"] - user = request.user - if user.is_banned: - return JsonResponse({"status": 110, "msg": "用户已被封禁"}) - if signature: - # 个性签名的修改次数每年增加一次 - user.signature = signature - try: - user.save(update_fields=["signature", "left_signature_n"]) - return JsonResponse({"status": 100, "msg": {"n": user.left_signature_n}}) - except Exception as e: - return JsonResponse({"status": 107, "msg": "未知错误。可能原因:不支持此种字符"}) - else: - ErrorDict = json.loads(user_update_form.errors.as_json()) - # print(ErrorDict) - Error = ErrorDict[next(iter(ErrorDict))][0]['message'] - return JsonResponse({"status": 101, "msg": Error}) + if request.user.userms.e_timems_std >= 200000: + return JsonResponse({"status": 177, "msg": "只允许标准高级sub200的玩家修改头像和个性签名!"}) + user_update_form = UserUpdateSignatureForm( + data=request.POST, request=request) + if user_update_form.is_valid(): + signature = user_update_form.cleaned_data["signature"] + user = request.user + if user.is_banned: + return JsonResponse({"status": 110, "msg": "用户已被封禁"}) + if signature: + # 个性签名的修改次数每年增加一次 + user.signature = signature + try: + user.save(update_fields=["signature", "left_signature_n"]) + return JsonResponse({"status": 100, "msg": {"n": user.left_signature_n}}) + except Exception as e: + return JsonResponse({"status": 107, "msg": "未知错误。可能原因:不支持此种字符"}) else: - return HttpResponse("别瞎玩") + ErrorDict = json.loads(user_update_form.errors.as_json()) + # print(ErrorDict) + Error = ErrorDict[next(iter(ErrorDict))][0]['message'] + return JsonResponse({"status": 101, "msg": Error}) # 用户修改自己的名字后,同步修改redis缓存里的真实姓名,使得排行榜数据同步修改 @@ -259,40 +240,22 @@ def update_cache_realname(user_id, user_realname): # 从redis获取用户排行榜 +@require_GET def player_rank(request): - if request.method == 'GET': - # print(request.GET) - # print(cache.zcard("player_timems_std_ids")) - # print(cache.keys('*')) - # [b'player_stnb_std_ids', b'player_path_std_2', b'player_timems_std_1', b'player_bvs_std_2', - # b'player_bvs_std_1', b'newest_queue', b'player_path_std_ids', b'player_ioe_std_2', - # b'player_stnb_std_2', b'player_timems_std_2', b'player_bvs_std_ids', b'player_ioe_std_ids', - # b'player_stnb_std_1', b'player_path_std_1', b'review_queue', b'player_ioe_std_1', - # b':1:django.contrib.sessions.cachef7wlcpvziwulv829ah1r66afrc1xaae0', b'player_timems_std_ids'] - # print(cache.zrange('player_timems_std_ids', 0, -1)) - # print(cache.zrange('player_timems_std_1', 0, -1)) - # print(cache.zrange('player_timems_std_2', 0, -1)) - # print(cache.zrange('player_path_std_2', 0, -1)) - data=request.GET - # num_player = cache.llen(data["ids"]) - num_player = cache.zcard(data["ids"]) - start_idx = 20 * (int(data["page"]) - 1) - if start_idx >= num_player: - start_idx = num_player // 20 * 20 - if num_player % 20 == 0 and num_player > 0: - start_idx -= 20 - desc_flag = True if data["reverse"] == "true" else False - res = cache.sort(data["ids"], by=data["sort_by"], get=json.loads(data["indexes"]), desc=desc_flag, start=start_idx, num=20) - # print(res) - # print(cache.get("player_timems_std_ids")) - # print(cache.hget("player_timems_std_3", "b")) - response = { - "total_page": num_player // 20 + 1, - "players": res - } - return JsonResponse(response, safe=False, encoder=ComplexEncoder) - else: - return HttpResponse("别瞎玩") + data=request.GET + num_player = cache.zcard(data["ids"]) + start_idx = 20 * (int(data["page"]) - 1) + if start_idx >= num_player: + start_idx = num_player // 20 * 20 + if num_player % 20 == 0 and num_player > 0: + start_idx -= 20 + desc_flag = True if data["reverse"] == "true" else False + res = cache.sort(data["ids"], by=data["sort_by"], get=json.loads(data["indexes"]), desc=desc_flag, start=start_idx, num=20) + response = { + "total_page": num_player // 20 + 1, + "players": res + } + return JsonResponse(response, safe=False, encoder=ComplexEncoder) diff --git a/back_end/saolei/saolei/settings.py b/back_end/saolei/saolei/settings.py index a7950e8..3987983 100644 --- a/back_end/saolei/saolei/settings.py +++ b/back_end/saolei/saolei/settings.py @@ -57,6 +57,7 @@ 'article', "ranking", 'identifier', + 'accountlink', "monitor", 'django_cleanup.apps.CleanupConfig', # 必须放在最后(文档所言) ] diff --git a/back_end/saolei/saolei/urls.py b/back_end/saolei/saolei/urls.py index e87c208..2d4070b 100644 --- a/back_end/saolei/saolei/urls.py +++ b/back_end/saolei/saolei/urls.py @@ -33,6 +33,7 @@ path('monitor/', include('monitor.urls')), path('article/', include('article.urls')), path('identifier/', include('identifier.urls')), + path('accountlink/', include('accountlink.urls')), path(r'', TemplateView.as_view(template_name="index.html")), ] diff --git a/back_end/saolei/userprofile/decorators.py b/back_end/saolei/userprofile/decorators.py new file mode 100644 index 0000000..ad17cc9 --- /dev/null +++ b/back_end/saolei/userprofile/decorators.py @@ -0,0 +1,26 @@ +from functools import wraps +from django.http import HttpResponseForbidden + +def staff_required(view_func): + @wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + if not request.user.is_staff: + return HttpResponseForbidden() # Return 403 Forbidden response + return view_func(request, *args, **kwargs) # Call the view if not banned + return _wrapped_view + +def banned_blocked(view_func): + @wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + if request.user.is_banned: + return HttpResponseForbidden() # Return 403 Forbidden response + return view_func(request, *args, **kwargs) # Call the view if not banned + return _wrapped_view + +def login_required_error(view_func): + @wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + if not request.user.is_authenticated: + return HttpResponseForbidden() # Return 403 Forbidden response + return view_func(request, *args, **kwargs) # Call the view if not banned + return _wrapped_view \ No newline at end of file diff --git a/back_end/saolei/userprofile/forms.py b/back_end/saolei/userprofile/forms.py index 7107f94..8e9ef92 100644 --- a/back_end/saolei/userprofile/forms.py +++ b/back_end/saolei/userprofile/forms.py @@ -3,7 +3,7 @@ from django.contrib.auth import get_user_model from django.utils.translation import gettext_lazy as _ from config.global_settings import * -from config.messages import * +from config.messages import FormErrors User = get_user_model() from captcha.fields import CaptchaField diff --git a/back_end/saolei/userprofile/models.py b/back_end/saolei/userprofile/models.py index 1de6ff0..e2a6633 100644 --- a/back_end/saolei/userprofile/models.py +++ b/back_end/saolei/userprofile/models.py @@ -70,6 +70,7 @@ class UserProfile(AbstractUser): popularity = models.BigIntegerField(null=False, default=0) # vip,0为非vip,理论0~32767。类似于权限 vip = models.PositiveSmallIntegerField(null=False, default=0) + def delete(self, *args, **kwargs): # 删除关联的文件 if self.avatar: diff --git a/back_end/saolei/userprofile/urls.py b/back_end/saolei/userprofile/urls.py index 6a62d96..cc671d3 100644 --- a/back_end/saolei/userprofile/urls.py +++ b/back_end/saolei/userprofile/urls.py @@ -4,6 +4,7 @@ app_name = 'userprofile' urlpatterns = [ path('login/', views.user_login, name='login'), + path('loginauto/', views.user_login_auto), path('logout/', views.user_logout, name='logout'), path('register/', views.user_register, name='register'), path('retrieve/', views.user_retrieve, name='retrieve'), diff --git a/back_end/saolei/userprofile/utils.py b/back_end/saolei/userprofile/utils.py new file mode 100644 index 0000000..21e4b91 --- /dev/null +++ b/back_end/saolei/userprofile/utils.py @@ -0,0 +1,23 @@ +from captcha.models import CaptchaStore +from django.utils import timezone +from .models import EmailVerifyRecord + +# 验证验证码 +def judge_captcha(captchaStr, captchaHashkey): + if captchaStr and captchaHashkey: + get_captcha = CaptchaStore.objects.filter( + hashkey=captchaHashkey).first() + if get_captcha and get_captcha.response == captchaStr.lower(): + # 图形验证码15分钟有效,get_captcha.expiration是过期时间 + if (get_captcha.expiration - timezone.now()).seconds >= 0: + CaptchaStore.objects.filter(hashkey=captchaHashkey).delete() + return True + CaptchaStore.objects.filter(hashkey=captchaHashkey).delete() + return False + +def judge_email_verification(email, email_captcha, emailHashkey): + get_email_captcha = EmailVerifyRecord.objects.filter(hashkey=emailHashkey).first() + if (timezone.now() - get_email_captcha.send_time).seconds <= 3600: + EmailVerifyRecord.objects.filter(hashkey=emailHashkey).delete() + return False + return get_email_captcha and email_captcha and get_email_captcha.code == email_captcha and get_email_captcha.email == email \ No newline at end of file diff --git a/back_end/saolei/userprofile/views.py b/back_end/saolei/userprofile/views.py index a735c8d..47eeb4e 100644 --- a/back_end/saolei/userprofile/views.py +++ b/back_end/saolei/userprofile/views.py @@ -1,85 +1,65 @@ import logging logger = logging.getLogger('userprofile') from django.contrib.auth import authenticate, login, logout -from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotFound +from django.http import HttpResponse, JsonResponse, HttpResponseForbidden, HttpResponseNotFound from .forms import UserLoginForm, UserRegisterForm, UserRetrieveForm, EmailForm from captcha.models import CaptchaStore import json import os from .models import EmailVerifyRecord,UserProfile from utils import send_email -from django.contrib.auth import get_user_model from msuser.models import UserMS from django_ratelimit.decorators import ratelimit +from django.views.decorators.http import require_GET, require_POST +from .decorators import staff_required from django.utils import timezone -from django.conf import settings from config.flags import EMAIL_SKIP +from .utils import judge_captcha, judge_email_verification # Create your views here. @ratelimit(key='ip', rate='60/h') +@require_POST +# 用账号、密码登录 # 此处要分成两个,密码容易碰撞,hash难碰撞 def user_login(request): - if request.method == 'POST': - # print(request.session.get("login")) - # print(request.user) - - # 用cookie登录 - response = {'status': 100, 'msg': None} - if request.user.is_authenticated: - # login(request, request.user) - response['msg'] = response['msg'] = { - "id": request.user.id, "username": request.user.username, - "realname": request.user.realname, "is_banned": request.user.is_banned, "is_staff": request.user.is_staff} - # logger.info(f'用户 {request.user.username}#{request.user.id} 自动登录') - return JsonResponse(response) - - # 用账号、密码登录 - user_login_form = UserLoginForm(data=request.POST) - if user_login_form.is_valid(): - data = user_login_form.cleaned_data - - capt = data["captcha"] # 用户提交的验证码 - key = data["hashkey"] # 验证码hash - username = data["username"] - response = {'status': 100, 'msg': None} - if judge_captcha(capt, key): - # 检验账号、密码是否正确匹配数据库中的某个用户 - # 如果均匹配则返回这个 user 对象 - user = authenticate( - username=username, password=data['password']) - if user: - # 将用户数据保存在 session 中,即实现了登录动作 - login(request, user) - response['msg'] = { - "id": user.id, "username": user.username, - "realname": user.realname, "is_banned": user.is_banned, "is_staff": user.is_staff} - if 'user_id' in data and data['user_id'] != str(user.id): - # 检测到小号 - logger.warning(f'{data["user_id"][:50]} is diffrent from {str(user.id)}.') - # logger.info(f'用户 {user.username}#{user.id} 账密登录') - return JsonResponse(response) - else: - logger.info(f'用户 {username} 账密错误') - return JsonResponse({'status': 105, 'msg': "账号或密码输入有误。请重新输入~"}) - - else: - logger.info(f'用户 {username} 验证码错误') - return JsonResponse({'status': 104, 'msg': "验证码错误!"}) - - else: - - return JsonResponse({'status': 106, 'msg': "表单错误!"}) + user_login_form = UserLoginForm(data=request.POST) + if not user_login_form.is_valid(): + return JsonResponse({'status': 106, 'msg': "表单错误!"}) + data = user_login_form.cleaned_data + + capt = data["captcha"] # 用户提交的验证码 + key = data["hashkey"] # 验证码hash + username = data["username"] + response = {'status': 100, 'msg': None} + if not judge_captcha(capt, key): + logger.info(f'用户 {username} 验证码错误') + return JsonResponse({'status': 104, 'msg': "验证码错误!"}) + # 检验账号、密码是否正确匹配数据库中的某个用户 + # 如果均匹配则返回这个 user 对象 + user = authenticate( + username=username, password=data['password']) + if not user: + logger.info(f'用户 {username} 账密错误') + return JsonResponse({'status': 105, 'msg': "账号或密码输入有误。请重新输入~"}) + # 将用户数据保存在 session 中,即实现了登录动作 + login(request, user) + response['msg'] = { + "id": user.id, "username": user.username, + "realname": user.realname, "is_banned": user.is_banned, "is_staff": user.is_staff} + if 'user_id' in data and data['user_id'] != str(user.id): + # 检测到小号 + logger.warning(f'{data["user_id"][:50]} is diffrent from {str(user.id)}.') + return JsonResponse(response) - elif request.method == 'GET': - return HttpResponse("别瞎玩") - # user_login_form = UserLoginForm() - # context = {'form': user_login_form} - # return render(request, 'userprofile/login.html', context) - else: - return HttpResponse("别瞎玩") +@require_GET +# 用cookie登录 +def user_login_auto(request): + if request.user.is_authenticated: + return JsonResponse({'id': request.user.id, 'username': request.user.username, 'realname': request.user.realname, 'is_banned': request.user.is_banned, 'is_staff': request.user.is_staff}) + return HttpResponse() def user_logout(request): logout(request) @@ -88,142 +68,113 @@ def user_logout(request): # 用户找回密码 @ratelimit(key='ip', rate='60/h') +@require_POST def user_retrieve(request): - if request.method == 'POST': - user_retrieve_form = UserRetrieveForm(data=request.POST) - if user_retrieve_form.is_valid(): - emailHashkey = request.POST.get("email_key", None) - email_captcha = request.POST.get("email_captcha", None) - get_email_captcha = EmailVerifyRecord.objects.filter( - hashkey=emailHashkey).first() - # print(emailHashkey) - # print(email_captcha) - if get_email_captcha and email_captcha and get_email_captcha.code == email_captcha and\ - get_email_captcha.email == request.POST.get("email", None): - if (timezone.now() - get_email_captcha.send_time).seconds <= 3600: - user = UserProfile.objects.filter(email=user_retrieve_form.cleaned_data['email']).first() - if not user: - return JsonResponse({'status': 109, 'msg': "该邮箱尚未注册,请先注册!"}) - # 设置密码(哈希) - user.set_password( - user_retrieve_form.cleaned_data['password']) - user.save() - # 保存好数据后立即登录 - login(request, user) - logger.info(f'用户 {user.username}#{user.id} 邮箱找回密码') - EmailVerifyRecord.objects.filter(hashkey=emailHashkey).delete() - return JsonResponse({'status': 100, 'msg': user.realname}) - else: - # 顺手把过期的验证码删了 - EmailVerifyRecord.objects.filter(hashkey=emailHashkey).delete() - return JsonResponse({'status': 150, 'msg': "邮箱验证码已过期!" }) - else: - return JsonResponse({'status': 102, 'msg': "邮箱验证码不正确!"}) - else: - return JsonResponse({'status': 101, 'msg': user_retrieve_form.errors.\ - as_text().split("*")[-1]}) + user_retrieve_form = UserRetrieveForm(data=request.POST) + if not user_retrieve_form.is_valid(): + return JsonResponse({'status': 101, 'msg': user_retrieve_form.errors.\ + as_text().split("*")[-1]}) + emailHashkey = request.POST.get("email_key") + email_captcha = request.POST.get("email_captcha") + email = request.POST.get("email") + if judge_email_verification(email, email_captcha, emailHashkey): + user = UserProfile.objects.filter(email=user_retrieve_form.cleaned_data['email']).first() + if not user: + return JsonResponse({'status': 109, 'msg': "该邮箱尚未注册,请先注册!"}) + # 设置密码(哈希) + user.set_password( + user_retrieve_form.cleaned_data['password']) + user.save() + # 保存好数据后立即登录 + login(request, user) + logger.info(f'用户 {user.username}#{user.id} 邮箱找回密码') + EmailVerifyRecord.objects.filter(hashkey=emailHashkey).delete() + return JsonResponse({'status': 100, 'msg': user.realname}) else: - return HttpResponse("别瞎玩") + return JsonResponse({'status': 102, 'msg': "邮箱验证码不正确或已过期!"}) # 用户注册 # @method_decorator(ensure_csrf_cookie) @ratelimit(key='ip', rate='6/h') +@require_POST def user_register(request): - # print(request.POST) - if request.method == 'POST': - user_register_form = UserRegisterForm(data=request.POST) - # print(request.POST) - # print(user_register_form.cleaned_data.get('username')) - # print(user_register_form.cleaned_data) - if user_register_form.is_valid(): - emailHashkey = request.POST.get("email_key", None) - email_captcha = request.POST.get("email_captcha", None) - get_email_captcha = EmailVerifyRecord.objects.filter(hashkey=emailHashkey).first() - # get_email_captcha = EmailVerifyRecord.objects.filter(hashkey="5f0db744-180b-4d9f-af5a-2986f4a78769").first() - if EMAIL_SKIP or (get_email_captcha and email_captcha and get_email_captcha.code == email_captcha and\ - get_email_captcha.email == request.POST.get("email", None)): - if EMAIL_SKIP or (timezone.now() - get_email_captcha.send_time).seconds <= 3600: - new_user = user_register_form.save(commit=False) - # 设置密码(哈希) - new_user.set_password( - user_register_form.cleaned_data['password']) - new_user.is_active = True # 自动激活 - user_ms = UserMS.objects.create() - new_user.userms = user_ms - user_ms.save() - new_user.save() - # 保存好数据后立即登录 - login(request, new_user) - logger.info(f'用户 {new_user.username}#{new_user.id} 注册') - # 顺手把过期的验证码删了 - EmailVerifyRecord.objects.filter(hashkey=emailHashkey).delete() - return JsonResponse({'status': 100, 'msg': { - "id": new_user.id, "username": new_user.username, - "realname": new_user.realname, "is_banned": False} - }) - else: - # 顺手把过期的验证码删了 - EmailVerifyRecord.objects.filter(hashkey=emailHashkey).delete() - return JsonResponse({'status': 150, 'msg': "邮箱验证码已过期!" }) - else: - return JsonResponse({'status': 102, 'msg': "邮箱验证码不正确!"}) + user_register_form = UserRegisterForm(data=request.POST) + if user_register_form.is_valid(): + emailHashkey = request.POST.get("email_key") + email_captcha = request.POST.get("email_captcha") + email = request.POST.get("email") + if EMAIL_SKIP or judge_email_verification(email, email_captcha, emailHashkey): + new_user = user_register_form.save(commit=False) + # 设置密码(哈希) + new_user.set_password( + user_register_form.cleaned_data['password']) + new_user.is_active = True # 自动激活 + user_ms = UserMS.objects.create() + new_user.userms = user_ms + user_ms.save() + new_user.save() + # 保存好数据后立即登录 + login(request, new_user) + logger.info(f'用户 {new_user.username}#{new_user.id} 注册') + # 顺手把过期的验证码删了 + EmailVerifyRecord.objects.filter(hashkey=emailHashkey).delete() + return JsonResponse({'status': 100, 'msg': { + "id": new_user.id, "username": new_user.username, + "realname": new_user.realname, "is_banned": False} + }) else: - if "email" not in user_register_form.cleaned_data or "username" not in user_register_form.cleaned_data: - # 可能发生前端验证正确,而后端验证不正确(后端更严格),此时clean会直接删除email字段 - # 重复的邮箱、用户名也会被删掉 - errors_dict = json.loads(user_register_form.errors.as_json()) - errors = list(errors_dict.values())[0] - return JsonResponse({'status': 105, 'msg': errors[0]["message"]}) - # print(user_register_form.errors.as_json()) - else: - # print(user_register_form.errors) - return JsonResponse({'status': 101, 'msg': user_register_form.errors.\ - as_text().split("*")[-1]}) + return JsonResponse({'status': 102, 'msg': "邮箱验证码不正确或已过期!"}) else: - return HttpResponse("别瞎玩") + if "email" not in user_register_form.cleaned_data or "username" not in user_register_form.cleaned_data: + # 可能发生前端验证正确,而后端验证不正确(后端更严格),此时clean会直接删除email字段 + # 重复的邮箱、用户名也会被删掉 + errors_dict = json.loads(user_register_form.errors.as_json()) + errors = list(errors_dict.values())[0] + return JsonResponse({'status': 105, 'msg': errors[0]["message"]}) + else: + return JsonResponse({'status': 101, 'msg': user_register_form.errors.\ + as_text().split("*")[-1]}) # 【站长】任命解除管理员 # http://127.0.0.1:8000/userprofile/set_staff/?id=1&is_staff=True +@require_GET def set_staff(request): - if request.user.is_superuser and request.method == 'GET': - user = UserProfile.objects.get(id=request.GET["id"]) - user.is_staff = request.GET["is_staff"] - logger.info(f'{request.user.id} set_staff {request.GET["id"]} {request.GET["is_staff"]}') - if request.GET["is_staff"] == "True": - user.is_staff = True - user.save() - logger.info(f'用户 {user.username}#{user.id} 成为管理员') - return HttpResponse(f"设置\"{user.realname}\"为管理员成功!") - elif request.GET["is_staff"] == "False": - user.is_staff = False - user.save() - logger.info(f'用户 {user.username}#{user.id} 卸任管理员') - return HttpResponse(f"解除\"{user.realname}\"的管理员权限!") - else: - return HttpResponse("失败!is_staff需要为\"True\"或\"False\"(首字母大写)") + if not request.user.is_superuser: + return HttpResponseForbidden() + user = UserProfile.objects.get(id=request.GET["id"]) + user.is_staff = request.GET["is_staff"] + logger.info(f'{request.user.id} set_staff {request.GET["id"]} {request.GET["is_staff"]}') + if request.GET["is_staff"] == "True": + user.is_staff = True + user.save() + logger.info(f'用户 {user.username}#{user.id} 成为管理员') + return HttpResponse(f"设置\"{user.realname}\"为管理员成功!") + elif request.GET["is_staff"] == "False": + user.is_staff = False + user.save() + logger.info(f'用户 {user.username}#{user.id} 卸任管理员') + return HttpResponse(f"解除\"{user.realname}\"的管理员权限!") else: - return HttpResponse("别瞎玩") + return HttpResponse("失败!is_staff需要为\"True\"或\"False\"(首字母大写)") # 【管理员】删除用户的个人信息,从服务器磁盘上完全删除,但不影响是否封禁 # 站长可以删除管理员信息(如果站长也是管理员)。 # http://127.0.0.1:8000/userprofile/del_user_info/?id=1 +@require_GET +@staff_required def del_user_info(request): - if request.user.is_staff and request.method == 'GET': - user = UserProfile.objects.get(id=request.GET["id"]) - if user.is_staff and not request.user.is_superuser: - return HttpResponse("没有删除管理员信息的权限!") - logger.info(f'管理员 {request.user.username}#{request.user.id} 删除用户 {user.username}#{user.id}') - user.realname = "" - user.signature = "" - if user.avatar: - if os.path.isfile(user.avatar.path): - os.remove(user.avatar.path) - user.avatar = None - - else: - return HttpResponse("别瞎玩") + user = UserProfile.objects.get(id=request.GET["id"]) + if user.is_staff and not request.user.is_superuser: + return HttpResponse("没有删除管理员信息的权限!") + logger.info(f'管理员 {request.user.username}#{request.user.id} 删除用户 {user.username}#{user.id}') + user.realname = "" + user.signature = "" + if user.avatar: + if os.path.isfile(user.avatar.path): + os.remove(user.avatar.path) + user.avatar = None # 创建验证码 @ratelimit(key='ip', rate='60/h') @@ -244,80 +195,56 @@ def refresh_captcha(request): # 验证验证码,若通过,发送email @ratelimit(key='ip', rate='20/h') +@require_POST def get_email_captcha(request): - if request.method == 'POST': - email_form = EmailForm(data=request.POST) - if email_form.is_valid(): - capt = request.POST.get("captcha", None) # 用户提交的验证码 - key = request.POST.get("hashkey", None) # 验证码hash - response = {'status': 100, 'msg': None, "hashkey": None} - if judge_captcha(capt, key): - hashkey = send_email(request.POST.get("email", None), request.POST.get("type", None)) - if hashkey: - response['hashkey'] = hashkey - return JsonResponse(response) - else: - response['status'] = 103 - response['msg'] = "发送邮件失败" - return JsonResponse(response) + email_form = EmailForm(data=request.POST) + if email_form.is_valid(): + capt = request.POST.get("captcha", None) # 用户提交的验证码 + key = request.POST.get("hashkey", None) # 验证码hash + response = {'status': 100, 'msg': None, "hashkey": None} + if judge_captcha(capt, key): + hashkey = send_email(request.POST.get("email", None), request.POST.get("type", None)) + if hashkey: + response['hashkey'] = hashkey + return JsonResponse(response) else: - response['status'] = 104 - response['msg'] = "验证码错误" + response['status'] = 103 + response['msg'] = "发送邮件失败" return JsonResponse(response) else: - # print(email_form) - return JsonResponse({'status': 110, 'msg': email_form.errors.\ - as_text().split("*")[-1]}) + response['status'] = 104 + response['msg'] = "验证码错误" + return JsonResponse(response) else: - return HttpResponse("只能post。。。") - -# 验证验证码 - - -def judge_captcha(captchaStr, captchaHashkey): - if captchaStr and captchaHashkey: - get_captcha = CaptchaStore.objects.filter( - hashkey=captchaHashkey).first() - if get_captcha and get_captcha.response == captchaStr.lower(): - # 图形验证码15分钟有效,get_captcha.expiration是过期时间 - if (get_captcha.expiration - timezone.now()).seconds >= 0: - CaptchaStore.objects.filter(hashkey=captchaHashkey).delete() - return True - CaptchaStore.objects.filter(hashkey=captchaHashkey).delete() - return False + return JsonResponse({'status': 110, 'msg': email_form.errors.\ + as_text().split("*")[-1]}) # 管理员使用的操作接口,调用方式见前端的StaffView.vue get_userProfile_fields = ["id", "userms__identifiers", "userms__video_num_limit", "username", "first_name", "last_name", "email", "realname", "signature", "country", "left_realname_n", "left_avatar_n", "left_signature_n", "is_banned"] # 可获取的域列表 +@require_GET +@staff_required def get_userProfile(request): - if request.method != 'GET': - return HttpResponseBadRequest() - if request.user.is_staff: - userlist = UserProfile.objects.filter(id=request.GET["id"]).values(*get_userProfile_fields) - if not userlist: - return HttpResponseNotFound() - return JsonResponse(userlist[0]) - else: - return HttpResponseForbidden() + userlist = UserProfile.objects.filter(id=request.GET["id"]).values(*get_userProfile_fields) + if not userlist: + return HttpResponseNotFound() + return JsonResponse(userlist[0]) # 管理员使用的操作接口,调用方式见前端的StaffView.vue set_userProfile_fields = ["userms__identifiers", "userms__video_num_limit", "username", "first_name", "last_name", "email", "realname", "signature", "country", "left_realname_n", "left_avatar_n", "left_signature_n", "is_banned"] # 可修改的域列表 +@require_POST +@staff_required def set_userProfile(request): - if request.method == 'POST': - if not request.user.is_staff: - return HttpResponseForbidden() # 非管理员不能使用该api - userid = request.POST.get("id") - user = UserProfile.objects.get(id=userid) - if user.is_staff and user != request.user: - return HttpResponseForbidden() # 不能修改除自己以外管理员的信息 - field = request.POST.get("field") - if field not in set_userProfile_fields: - return HttpResponseForbidden() # 只能修改特定的域 - if field == "is_banned" and user.is_superuser: - return HttpResponseForbidden() # 站长不可被封禁 - value = request.POST.get("value") - logger.warning(f'管理员 {request.user.username}#{request.user.id} 修改用户 {user.username}#{user.id} 域 {field} 从 {getattr(user, field)} 到 {value}') - setattr(user, field, value) - user.save() - return HttpResponse() - else: - return HttpResponseBadRequest() \ No newline at end of file + userid = request.POST.get("id") + user = UserProfile.objects.get(id=userid) + if user.is_staff and user != request.user: + return HttpResponseForbidden() # 不能修改除自己以外管理员的信息 + field = request.POST.get("field") + if field not in set_userProfile_fields: + return HttpResponseForbidden() # 只能修改特定的域 + if field == "is_banned" and user.is_superuser: + return HttpResponseForbidden() # 站长不可被封禁 + value = request.POST.get("value") + logger.warning(f'管理员 {request.user.username}#{request.user.id} 修改用户 {user.username}#{user.id} 域 {field} 从 {getattr(user, field)} 到 {value}') + setattr(user, field, value) + user.save() + return HttpResponse() \ No newline at end of file diff --git a/back_end/saolei/videomanager/views.py b/back_end/saolei/videomanager/views.py index 370bee0..b4a79bd 100644 --- a/back_end/saolei/videomanager/views.py +++ b/back_end/saolei/videomanager/views.py @@ -26,17 +26,16 @@ from django.conf import settings from identifier.utils import verify_identifier from django.views.decorators.http import require_GET, require_POST +from userprofile.decorators import banned_blocked, staff_required @login_required(login_url='/') @require_POST +@banned_blocked def video_upload(request): - if request.user.is_banned: - return HttpResponseForbidden() # 用户被封禁 if request.user.userms.video_num_total >= request.user.userms.video_num_limit: return HttpResponse(status = 402) # 录像仓库已满 video_form = UploadVideoForm(data=request.POST, files=request.FILES) - # print(video_form) if not video_form.is_valid(): return HttpResponseBadRequest() data = video_form.cleaned_data @@ -58,7 +57,6 @@ def video_upload(request): @require_GET def get_software(request): video = VideoModel.objects.get(id=request.GET["id"]) - # print({"status": 100, "msg": video.software}) return JsonResponse({"msg": video.software}) # 给预览用的接口,区别是结尾是文件后缀 @@ -235,16 +233,14 @@ def approve_identifier(userid, identifier): # 返回"True","False"(已经是通过的状态),"Null"(不存在该录像) # http://127.0.0.1:8000/video/approve?ids=[18,19,999] @require_GET +@staff_required def approve(request): - if request.user.is_staff: - ids = json.loads(request.GET["ids"]) - res = [] - for _id in ids: - logger.info(f'管理员 {request.user.username}#{request.user.id} 过审录像#{_id}') - res.append(approve_single(_id)) - return JsonResponse(res, safe=False) - else: - return HttpResponseNotAllowed() + ids = json.loads(request.GET["ids"]) + res = [] + for _id in ids: + logger.info(f'管理员 {request.user.username}#{request.user.id} 过审录像#{_id}') + res.append(approve_single(_id)) + return JsonResponse(res, safe=False) # 【管理员】冻结队列里的录像,未审核或审核通过的录像可以冻结 # 两种用法,冻结指定的录像id,或冻结某用户的所有录像 @@ -253,10 +249,8 @@ def approve(request): # http://127.0.0.1:8000/video/freeze?ids=12 # http://127.0.0.1:8000/video/freeze?user_id=20 @require_GET +@staff_required def freeze(request): - if not request.user.is_staff: - return HttpResponseForbidden() - if ids := request.GET.get("ids"): res = [] for id in ids: @@ -285,20 +279,17 @@ def freeze(request): get_videoModel_fields.append("video__" + name) @require_GET +@staff_required def get_videoModel(request): - if request.user.is_staff: - videolist = VideoModel.objects.filter(id=request.GET["id"]).values(*get_videoModel_fields) - if not videolist: - return HttpResponseNotFound() - return JsonResponse(videolist[0]) - else: - return HttpResponseForbidden() + videolist = VideoModel.objects.filter(id=request.GET["id"]).values(*get_videoModel_fields) + if not videolist: + return HttpResponseNotFound() + return JsonResponse(videolist[0]) set_videoModel_fields = ["player", "upload_time", "state"] # 可修改的域列表 @require_POST +@staff_required def set_videoModel(request): - if not request.user.is_staff: - return HttpResponseForbidden() # 非管理员不能使用该api videoid = request.POST.get("id") video = VideoModel.objects.get(id=videoid) user = video.player diff --git a/front_end/src/assets/IdGuideMsgames1.png b/front_end/src/assets/IdGuideMsgames1.png new file mode 100644 index 0000000..192e295 Binary files /dev/null and b/front_end/src/assets/IdGuideMsgames1.png differ diff --git a/front_end/src/assets/IdGuideMsgames2.png b/front_end/src/assets/IdGuideMsgames2.png new file mode 100644 index 0000000..b0ea348 Binary files /dev/null and b/front_end/src/assets/IdGuideMsgames2.png differ diff --git a/front_end/src/assets/IdGuideSaolei.png b/front_end/src/assets/IdGuideSaolei.png new file mode 100644 index 0000000..732d1da Binary files /dev/null and b/front_end/src/assets/IdGuideSaolei.png differ diff --git a/front_end/src/assets/IdGuideWom.png b/front_end/src/assets/IdGuideWom.png new file mode 100644 index 0000000..9e88950 Binary files /dev/null and b/front_end/src/assets/IdGuideWom.png differ diff --git a/front_end/src/components/AccountLinkManager.vue b/front_end/src/components/AccountLinkManager.vue new file mode 100644 index 0000000..5d6e442 --- /dev/null +++ b/front_end/src/components/AccountLinkManager.vue @@ -0,0 +1,145 @@ + + + \ No newline at end of file diff --git a/front_end/src/components/Login.vue b/front_end/src/components/Login.vue index 24ccfcb..88bc24f 100644 --- a/front_end/src/components/Login.vue +++ b/front_end/src/components/Login.vue @@ -214,7 +214,7 @@ const init_refvalues = () => { onMounted(() => { if (store.login_status == LoginStatus.Undefined) { - login(); + login_auto(); } else if (store.login_status == LoginStatus.IsLogin) { // 解决改变窗口宽度,使得账号信息在显示和省略之间切换时,用户名不能显示的问题 hint_message.value = "" @@ -254,21 +254,30 @@ const closeLogin = () => { } } +const login_auto = async () => { + proxy.$axios.get('/userprofile/loginauto/').then(function (response) { + if (response.data.id) { + store.user = deepCopy(response.data); // 直接赋值会导致user和player共用一个字典!! + store.player = deepCopy(response.data); + store.login_status = LoginStatus.IsLogin; + } + else { + store.login_status = LoginStatus.NotLogin; + } + }) +} + const login = async () => { - // 先用cookie尝试登录,可能登不上 - // 再用用户名密码 + // 用户名密码登录 var params = new URLSearchParams() const _id = localStorage.getItem("history_user_id"); - params.append('user_id', _id ? _id : ""); + if (_id) { + params.append('user_id', _id); + } params.append('username', user_name.value) params.append('password', user_password.value) - if (valid_code.value) { - params.append('captcha', valid_code.value) - params.append('hashkey', refValidCode.value?.hashkey) - } else { - params.append('captcha', "") - params.append('hashkey', "") - } + params.append('captcha', valid_code.value) + params.append('hashkey', refValidCode.value?.hashkey) await proxy.$axios.post('/userprofile/login/', params, diff --git a/front_end/src/components/dialogs/AccountLinkGuide.vue b/front_end/src/components/dialogs/AccountLinkGuide.vue new file mode 100644 index 0000000..47b62c1 --- /dev/null +++ b/front_end/src/components/dialogs/AccountLinkGuide.vue @@ -0,0 +1,35 @@ + + + \ No newline at end of file diff --git a/front_end/src/components/staff/StaffAccountLink.vue b/front_end/src/components/staff/StaffAccountLink.vue new file mode 100644 index 0000000..b00e903 --- /dev/null +++ b/front_end/src/components/staff/StaffAccountLink.vue @@ -0,0 +1,62 @@ + + + \ No newline at end of file diff --git a/front_end/src/components/widgets/IdentifierManager.vue b/front_end/src/components/widgets/IdentifierManager.vue index 073032c..f935fa9 100644 --- a/front_end/src/components/widgets/IdentifierManager.vue +++ b/front_end/src/components/widgets/IdentifierManager.vue @@ -53,7 +53,6 @@ function delIdentifier(identifier: string) { } function addIdentifier(identifier: string) { - console.log(identifier) proxy.$axios.post('identifier/add/', { identifier: identifier, diff --git a/front_end/src/components/widgets/PlatformIcon.vue b/front_end/src/components/widgets/PlatformIcon.vue new file mode 100644 index 0000000..b196ceb --- /dev/null +++ b/front_end/src/components/widgets/PlatformIcon.vue @@ -0,0 +1,16 @@ + + + \ No newline at end of file diff --git a/front_end/src/i18n/locales/en.ts b/front_end/src/i18n/locales/en.ts index 31b17ac..0d258cd 100644 --- a/front_end/src/i18n/locales/en.ts +++ b/front_end/src/i18n/locales/en.ts @@ -14,6 +14,10 @@ export const en = { uploadFile: 'upload file', videoQuery: 'fetch video data', }, + button: { + cancel: 'Cancel', + confirm: 'Confirm', + }, filter: 'Filter', hide: 'Hide', level: { @@ -93,11 +97,27 @@ export const en = { }, toDo: 'TODO', }, + accountlink: { + title: 'Account Links', + addLink: 'Link New Account', + deleteLinkMessage: 'Are you sure to unlink this account?', + guideMsgames1: 'Find yourself on the ', + guideMsgames2: ' ranking:', + guideMsgames3: 'Click the link next to your name (see image above) to get to your profile (see image below). The number at the end of the url is your ID.', + guideSaolei1: 'Go to your profile page on ', + guideSaolei2: '. The positions of the ID are shown below.', + guideTitle: 'How to locate the ID', + guideWom1: 'Go to your profile page on ', + guideWom2: '. The number at the end of the url is your ID.', + platform: 'Platform', + unverified: 'Pending. Please contact the moderator.', + verified: 'Verified', + }, footer: { contact: 'Contact', donate: 'Donate', team: 'Team', - links: 'LInks', + links: 'Links', about: 'About', }, forgetPassword: { @@ -173,6 +193,9 @@ export const en = { realnameChange: 'Real name change complete! {0} times left', signatureChange: 'Signature change complete! {0} times left', }, + profile: { + title: 'Profile', + }, records: { title: 'Personal Records', modeRecord: ' mode record: ' diff --git a/front_end/src/i18n/locales/zh-cn.ts b/front_end/src/i18n/locales/zh-cn.ts index ed8fdbe..d75086d 100644 --- a/front_end/src/i18n/locales/zh-cn.ts +++ b/front_end/src/i18n/locales/zh-cn.ts @@ -14,6 +14,10 @@ export const zhCn = { uploadFile: '上传文件', videoQuery: '查询录像', }, + button: { + cancel: '取消', + confirm: '确认', + }, filter: '筛选', hide: '隐藏', level: { @@ -94,6 +98,22 @@ export const zhCn = { }, toDo: '敬请期待', }, + accountlink: { + title: '账号关联', + addLink: '添加关联账号', + deleteLinkMessage: '确认删除以下账号关联吗?', + guideMsgames1: '在', + guideMsgames2: '排行榜找到你的位置:', + guideMsgames3: '在上图点击名字右边的链接,进入如下的个人主页,网址结尾的数字就是ID。', + guideSaolei1: '登录', + guideSaolei2: ',进入“我的地盘”,ID位置如下图所示。', + guideTitle: '如何找到ID', + guideWom1: '在', + guideWom2: '进入你的个人主页,网址结尾的数字就是你的ID。', + platform: '平台', + unverified: '未验证,请联系管理员', + verified: '已验证', + }, footer: { contact: '联系我们', donate: '捐赠', @@ -174,6 +194,9 @@ export const zhCn = { realnameChange: '姓名修改成功!剩余{0}次', signatureChange: '个性签名修改成功!剩余{0}次', }, + profile: { + title: '个人信息', + }, records: { title: '个人纪录', modeRecord: '模式纪录:' diff --git a/front_end/src/utils/common/accountLinkPlatforms.ts b/front_end/src/utils/common/accountLinkPlatforms.ts new file mode 100644 index 0000000..83bb0cd --- /dev/null +++ b/front_end/src/utils/common/accountLinkPlatforms.ts @@ -0,0 +1,17 @@ +const saoleiProfile = (id: string | number) => { + return "http://saolei.wang/Player/Index.asp?Id=" + id; +} +const msgamesProfile = (id: string | number) => { + return "https://minesweepergame.com/profile.php?pid=" + id; +} +const womProfile = (id: string | number) => { + return "https://minesweeper.online/player/" + id; +} + +export declare type Platform = 'a' | 'c' | 'w'; +export const platformlist:{[key in Platform]: any;} = { + a: { name: 'Authoritative Minesweeper', url: 'https://minesweepergame.com/', profile: msgamesProfile }, + c: { name: '扫雷网', url: 'http://saolei.wang/', profile: saoleiProfile }, + w: { name: 'Minesweeper.Online', url: 'https://minesweeper.online/', profile: womProfile }, +}; + diff --git a/front_end/src/views/HomeView.vue b/front_end/src/views/HomeView.vue index 9a863b4..b8c95fd 100644 --- a/front_end/src/views/HomeView.vue +++ b/front_end/src/views/HomeView.vue @@ -3,14 +3,30 @@ - + +
{{ utc_to_local_format(news.time) }} - + - {{ $t('news.breakRecordTo', {mode: $t('common.mode.'+news.mode), level: $t('common.level.'+news.level), stat: $t('common.prop.'+news.index)}) }} + {{ $t('news.breakRecordTo', { + mode: $t('common.mode.' + news.mode), level: + $t('common.level.' + news.level), stat: $t('common.prop.' + news.index) + }) }} @@ -20,12 +36,25 @@ - - - + + + + + - - + + @@ -37,7 +66,9 @@
- 下载中心 + + + 下载中心
@@ -46,14 +77,18 @@
- 帮助中心 + + + 帮助中心
- 关于我们 + + + 关于我们
@@ -91,40 +126,41 @@ const t = useI18n(); const review_queue = ref([]); const newest_queue = ref([]); const news_queue = ref([]); +const active_tab = ref('newest'); -const review_queue_updating = ref(false); +const review_queue_updating = ref(true); + +// 0: 可以刷新, 1: 正在刷新, 2: 刷新完毕,冷却中 +const newest_queue_status = ref(1); +const news_queue_status = ref(1); onMounted(() => { update_review_queue() update_newest_queue() - proxy.$axios.get('/video/news_queue/', - { - params: {} - } - ).then(function (response) { - news_queue.value = response.data.map((v: string) => { return JSON.parse(v) }) - }) + update_news_queue() }) const update_review_queue = async () => { - review_queue_updating.value = true + review_queue_updating.value = true; await proxy.$axios.get('/video/review_queue/', { params: {} } ).then(function (response) { - review_queue.value.splice(0,review_queue.value.length) + review_queue.value.splice(0, review_queue.value.length) for (let key in response.data) { response.data[key] = JSON.parse(response.data[key] as string); response.data[key]["key"] = Number.parseInt(key); review_queue.value.push(response.data[key]); } }) - review_queue_updating.value = false + review_queue_updating.value = false; } const update_newest_queue = async () => { - proxy.$axios.get('/video/newest_queue/', + newest_queue_status.value = 1; + setTimeout(() => { newest_queue_status.value = 0; }, 5000) + await proxy.$axios.get('/video/newest_queue/', { params: {} } @@ -138,30 +174,43 @@ const update_newest_queue = async () => { } } }) + if (newest_queue_status.value == 1) { + newest_queue_status.value = 2; + } +} + +const update_news_queue = async () => { + news_queue_status.value = 1; + setTimeout(() => { news_queue_status.value = 0; }, 5000) + await proxy.$axios.get('/video/news_queue/', + { + params: {} + } + ).then(function (response) { + news_queue.value = response.data.map((v: string) => { return JSON.parse(v) }) + }) + if (news_queue_status.value == 1) { + news_queue_status.value = 2; + } } diff --git a/front_end/src/views/PlayerProfileView.vue b/front_end/src/views/PlayerProfileView.vue new file mode 100644 index 0000000..05957a0 --- /dev/null +++ b/front_end/src/views/PlayerProfileView.vue @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/front_end/src/views/PlayerView.vue b/front_end/src/views/PlayerView.vue index ca38ffa..5084b79 100644 --- a/front_end/src/views/PlayerView.vue +++ b/front_end/src/views/PlayerView.vue @@ -59,13 +59,16 @@ - + + + + - + - @@ -82,6 +85,7 @@ import { onMounted, ref, watch } from 'vue' import useCurrentInstance from "@/utils/common/useCurrentInstance"; import PlayerRecordView from '@/views/PlayerRecordView.vue'; import PlayerVideosView from '@/views/PlayerVideosView.vue'; +import PlayerProfileView from './PlayerProfileView.vue'; import UploadView from './UploadView.vue'; // const AsyncPlayerVideosView = defineAsyncComponent(() => import('@/views/PlayerVideosView.vue')); import "../../node_modules/flag-icon-css/css/flag-icons.min.css"; @@ -123,7 +127,7 @@ const is_editing = ref(false); const visible = ref(false); // 标签默认切在第一页 -const activeName = ref('first') +const activeName = ref('profile') const player = { id: -1, }; diff --git a/front_end/src/views/SettingView.vue b/front_end/src/views/SettingView.vue index 5ecae76..67848c7 100644 --- a/front_end/src/views/SettingView.vue +++ b/front_end/src/views/SettingView.vue @@ -38,15 +38,11 @@ {{ $t('common.toDo') }} {{ $t('common.toDo') }} + - - - - - \ No newline at end of file diff --git a/front_end/src/views/StaffView.vue b/front_end/src/views/StaffView.vue index 305b216..6ef3090 100644 --- a/front_end/src/views/StaffView.vue +++ b/front_end/src/views/StaffView.vue @@ -39,6 +39,8 @@ {{ value }} + +