From 3b1f02c35654e2e4ef7560c1a03b59845c34d0ca Mon Sep 17 00:00:00 2001 From: zemal Date: Sun, 20 Aug 2017 08:35:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B4=E7=90=86=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account/serializers.py | 2 +- account/urls/oj.py | 9 +- account/urls/user.py | 12 -- account/views/oj.py | 180 ++++++++++++++++++++++--- account/views/user.py | 179 ------------------------ oj/local_settings.py | 23 ---- oj/settings.py | 2 + oj/urls.py | 1 - utils/captcha/__init__.py | 58 ++++---- utils/management/commands/initadmin.py | 2 +- 10 files changed, 200 insertions(+), 268 deletions(-) delete mode 100644 account/urls/user.py delete mode 100644 account/views/user.py diff --git a/account/serializers.py b/account/serializers.py index 68ba2b2..69e312a 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -15,6 +15,7 @@ class UsernameOrEmailCheckSerializer(serializers.Serializer): username = serializers.CharField(max_length=30, required=False) email = serializers.EmailField(max_length=30, required=False) + class UserRegisterSerializer(serializers.Serializer): username = serializers.CharField(max_length=30) password = serializers.CharField(max_length=30, min_length=6) @@ -46,7 +47,6 @@ class UserProfileSerializer(serializers.ModelSerializer): class UserInfoSerializer(serializers.ModelSerializer): - class Meta: model = UserProfile diff --git a/account/urls/oj.py b/account/urls/oj.py index a0b8c72..1d65729 100644 --- a/account/urls/oj.py +++ b/account/urls/oj.py @@ -2,7 +2,8 @@ from django.conf.urls import url from ..views.oj import (ApplyResetPasswordAPI, ResetPasswordAPI, UserChangePasswordAPI, UserRegisterAPI, - UserLoginAPI, UserLogoutAPI, UsernameOrEmailCheck) + UserLoginAPI, UserLogoutAPI, UsernameOrEmailCheck, + SSOAPI, AvatarUploadAPI, TwoFactorAuthAPI, UserProfileAPI) from utils.captcha.views import CaptchaAPIView @@ -14,5 +15,9 @@ urlpatterns = [ url(r"^apply_reset_password/?$", ApplyResetPasswordAPI.as_view(), name="apply_reset_password_api"), url(r"^reset_password/?$", ResetPasswordAPI.as_view(), name="apply_reset_password_api"), url(r"^captcha/?$", CaptchaAPIView.as_view(), name="show_captcha"), - url(r"^check_username_or_email", UsernameOrEmailCheck.as_view(), name="check_username_or_email") + url(r"^check_username_or_email", UsernameOrEmailCheck.as_view(), name="check_username_or_email"), + url(r"^profile/?$", UserProfileAPI.as_view(), name="user_profile_api"), + url(r"^avatar/upload/?$", AvatarUploadAPI.as_view(), name="avatar_upload_api"), + url(r"^sso/?$", SSOAPI.as_view(), name="sso_api"), + url(r"^two_factor_auth/?$", TwoFactorAuthAPI.as_view(), name="two_factor_auth_api") ] diff --git a/account/urls/user.py b/account/urls/user.py deleted file mode 100644 index 8fdb7db..0000000 --- a/account/urls/user.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.conf.urls import url - -from ..views.user import (SSOAPI, AvatarUploadAPI, TwoFactorAuthAPI, - UserProfileAPI) - -urlpatterns = [ - # url(r"^username/?$", UserNameAPI.as_view(), name="user_name_api"), - url(r"^profile/?$", UserProfileAPI.as_view(), name="user_profile_api"), - url(r"^avatar/upload/?$", AvatarUploadAPI.as_view(), name="avatar_upload_api"), - url(r"^sso/?$", SSOAPI.as_view(), name="sso_api"), - url(r"^two_factor_auth/?$", TwoFactorAuthAPI.as_view(), name="two_factor_auth_api") -] diff --git a/account/views/oj.py b/account/views/oj.py index 3bfb259..351224c 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -1,13 +1,18 @@ +import os +import qrcode +from io import BytesIO from datetime import timedelta +from otpauth import OtpAuth from django.conf import settings from django.contrib import auth -from django.core.exceptions import MultipleObjectsReturned from django.utils.timezone import now -from otpauth import OtpAuth +from django.http import HttpResponse +from django.views.decorators.csrf import ensure_csrf_cookie +from django.utils.decorators import method_decorator from conf.models import WebsiteConfig -from utils.api import APIView, validate_serializer +from utils.api import APIView, validate_serializer, CSRFExemptAPIView from utils.captcha import Captcha from utils.shortcuts import rand_str @@ -17,9 +22,153 @@ from ..serializers import (ApplyResetPasswordSerializer, ResetPasswordSerializer, UserChangePasswordSerializer, UserLoginSerializer, UserRegisterSerializer, UsernameOrEmailCheckSerializer) +from ..serializers import (SSOSerializer, TwoFactorAuthCodeSerializer, + UserProfileSerializer, + EditUserProfileSerializer, AvatarUploadForm) from ..tasks import send_email_async +class UserProfileAPI(APIView): + """ + 判断是否登录, 若登录返回用户信息 + """ + @method_decorator(ensure_csrf_cookie) + def get(self, request, **kwargs): + user = request.user + if not user.is_authenticated(): + return self.success(0) + + username = request.GET.get("username") + try: + if username: + user = User.objects.get(username=username, is_disabled=False) + else: + user = request.user + except User.DoesNotExist: + return self.error("User does not exist") + profile = UserProfile.objects.get(user=user) + return self.success(UserProfileSerializer(profile).data) + + @validate_serializer(EditUserProfileSerializer) + @login_required + def put(self, request): + data = request.data + user_profile = request.user.userprofile + print(data) + if data.get("avatar"): + user_profile.avatar = data["avatar"] + else: + user_profile.mood = data["mood"] + user_profile.blog = data["blog"] + user_profile.school = data["school"] + user_profile.student_id = data["student_id"] + user_profile.phone_number = data["phone_number"] + user_profile.major = data["major"] + # Timezone & language 暂时不加 + user_profile.save() + return self.success("Succeeded") + + +class AvatarUploadAPI(CSRFExemptAPIView): + request_parsers = () + + def post(self, request): + form = AvatarUploadForm(request.POST, request.FILES) + if form.is_valid(): + avatar = form.cleaned_data["file"] + else: + return self.error("Upload failed") + if avatar.size > 1024 * 1024: + return self.error("Picture too large") + if os.path.splitext(avatar.name)[-1].lower() not in [".gif", ".jpg", ".jpeg", ".bmp", ".png"]: + return self.error("Unsupported file format") + + name = "avatar_" + rand_str(5) + os.path.splitext(avatar.name)[-1] + with open(os.path.join(settings.IMAGE_UPLOAD_DIR, name), "wb") as img: + for chunk in avatar: + img.write(chunk) + print(os.path.join(settings.IMAGE_UPLOAD_DIR, name)) + return self.success({"path": "/static/upload/" + name}) + + +class SSOAPI(APIView): + @login_required + def get(self, request): + callback = request.GET.get("callback", None) + if not callback: + return self.error("Parameter Error") + token = rand_str() + request.user.auth_token = token + request.user.save() + return self.success({"redirect_url": callback + "?token=" + token, + "callback": callback}) + + @validate_serializer(SSOSerializer) + def post(self, request): + data = request.data + try: + User.objects.get(open_api_appkey=data["appkey"]) + except User.DoesNotExist: + return self.error("Invalid appkey") + try: + user = User.objects.get(auth_token=data["token"]) + user.auth_token = None + user.save() + return self.success({"username": user.username, + "id": user.id, + "admin_type": user.admin_type, + "avatar": user.userprofile.avatar}) + except User.DoesNotExist: + return self.error("User does not exist") + + +class TwoFactorAuthAPI(APIView): + @login_required + def get(self, request): + """ + Get QR code + """ + user = request.user + if user.two_factor_auth: + return self.error("Already open 2FA") + token = rand_str() + user.tfa_token = token + user.save() + + config = WebsiteConfig.objects.first() + image = qrcode.make(OtpAuth(token).to_uri("totp", config.base_url, config.name)) + buf = BytesIO() + image.save(buf, "gif") + + return HttpResponse(buf.getvalue(), "image/gif") + + @login_required + @validate_serializer(TwoFactorAuthCodeSerializer) + def post(self, request): + """ + Open 2FA + """ + code = request.data["code"] + user = request.user + if OtpAuth(user.tfa_token).valid_totp(code): + user.two_factor_auth = True + user.save() + return self.success("Succeeded") + else: + return self.error("Invalid captcha") + + @login_required + @validate_serializer(TwoFactorAuthCodeSerializer) + def put(self, request): + code = request.data["code"] + user = request.user + if OtpAuth(user.tfa_token).valid_totp(code): + user.two_factor_auth = False + user.save() + else: + return self.error("Invalid captcha") + + class UserLoginAPI(APIView): @validate_serializer(UserLoginSerializer) def post(self, request): @@ -89,23 +238,16 @@ class UserRegisterAPI(APIView): captcha = Captcha(request) if not captcha.validate(data["captcha"]): return self.error("Invalid captcha") - try: - User.objects.get(username=data["username"]) + if User.objects.filter(username=data["username"]).exists(): return self.error("Username already exists") - except User.DoesNotExist: - pass - try: - User.objects.get(email=data["email"]) + if User.objects.filter(email=data["email"]).exists(): return self.error("Email already exists") - # Some old data has duplicate email - except MultipleObjectsReturned: - return self.error("Email already exists") - except User.DoesNotExist: - user = User.objects.create(username=data["username"], email=data["email"]) - user.set_password(data["password"]) - user.save() - UserProfile.objects.create(user=user) - return self.success("Succeeded") + + user = User.objects.create(username=data["username"], email=data["email"]) + user.set_password(data["password"]) + user.save() + UserProfile.objects.create(user=user, time_zone=settings.USER_DEFAULT_TZ) + return self.success("Succeeded") class UserChangePasswordAPI(APIView): @@ -117,7 +259,7 @@ class UserChangePasswordAPI(APIView): """ data = request.data captcha = Captcha(request) - if not captcha.check(data["captcha"]): + if not captcha.validate(data["captcha"]): return self.error("Invalid captcha") username = request.user.username user = auth.authenticate(username=username, password=data["old_password"]) diff --git a/account/views/user.py b/account/views/user.py deleted file mode 100644 index 9bbbf03..0000000 --- a/account/views/user.py +++ /dev/null @@ -1,179 +0,0 @@ -import os -from io import BytesIO - -import qrcode -from django.conf import settings -from django.http import HttpResponse -from django.views.decorators.csrf import ensure_csrf_cookie -from django.utils.decorators import method_decorator -from otpauth import OtpAuth - -from conf.models import WebsiteConfig -from utils.api import APIView, validate_serializer, CSRFExemptAPIView -from utils.shortcuts import rand_str - -from ..decorators import login_required -from ..models import User, UserProfile -from ..serializers import (SSOSerializer, TwoFactorAuthCodeSerializer, - UserProfileSerializer, - EditUserProfileSerializer, AvatarUploadForm) - - -class UserNameAPI(APIView): - @method_decorator(ensure_csrf_cookie) - def get(self, request): - """ - Return Username to valid login status - """ - try: - user = User.objects.get(id=request.user.id) - except User.DoesNotExist: - return self.success({ - "username": "User does not exist", - "isLogin": False - }) - return self.success({ - "username": user.username, - "isLogin": True - }) - - -class UserProfileAPI(APIView): - """ - 判断是否登录, 若登录返回用户信息 - """ - @method_decorator(ensure_csrf_cookie) - def get(self, request, **kwargs): - user = request.user - if not user.is_authenticated(): - return self.success(0) - - username = request.GET.get("username") - try: - if username: - user = User.objects.get(username=username, is_disabled=False) - else: - user = request.user - except User.DoesNotExist: - return self.error("User does not exist") - profile = UserProfile.objects.get(user=user) - return self.success(UserProfileSerializer(profile).data) - - @validate_serializer(EditUserProfileSerializer) - @login_required - def put(self, request): - data = request.data - user_profile = request.user.userprofile - print(data) - if data.get("avatar"): - user_profile.avatar = data["avatar"] - else: - user_profile.mood = data["mood"] - user_profile.blog = data["blog"] - user_profile.school = data["school"] - user_profile.student_id = data["student_id"] - user_profile.phone_number = data["phone_number"] - user_profile.major = data["major"] - # Timezone & language 暂时不加 - user_profile.save() - return self.success("Succeeded") - - -class AvatarUploadAPI(CSRFExemptAPIView): - request_parsers = () - - def post(self, request): - form = AvatarUploadForm(request.POST, request.FILES) - if form.is_valid(): - avatar = form.cleaned_data["file"] - else: - return self.error("Upload failed") - if avatar.size > 1024 * 1024: - return self.error("Picture too large") - if os.path.splitext(avatar.name)[-1].lower() not in [".gif", ".jpg", ".jpeg", ".bmp", ".png"]: - return self.error("Unsupported file format") - - name = "avatar_" + rand_str(5) + os.path.splitext(avatar.name)[-1] - with open(os.path.join(settings.IMAGE_UPLOAD_DIR, name), "wb") as img: - for chunk in avatar: - img.write(chunk) - print(os.path.join(settings.IMAGE_UPLOAD_DIR, name)) - return self.success({"path": "/static/upload/" + name}) - - -class SSOAPI(APIView): - @login_required - def get(self, request): - callback = request.GET.get("callback", None) - if not callback: - return self.error("Parameter Error") - token = rand_str() - request.user.auth_token = token - request.user.save() - return self.success({"redirect_url": callback + "?token=" + token, - "callback": callback}) - - @validate_serializer(SSOSerializer) - def post(self, request): - data = request.data - try: - User.objects.get(open_api_appkey=data["appkey"]) - except User.DoesNotExist: - return self.error("Invalid appkey") - try: - user = User.objects.get(auth_token=data["token"]) - user.auth_token = None - user.save() - return self.success({"username": user.username, - "id": user.id, - "admin_type": user.admin_type, - "avatar": user.userprofile.avatar}) - except User.DoesNotExist: - return self.error("User does not exist") - - -class TwoFactorAuthAPI(APIView): - @login_required - def get(self, request): - """ - Get QR code - """ - user = request.user - if user.two_factor_auth: - return self.error("Already open 2FA") - token = rand_str() - user.tfa_token = token - user.save() - - config = WebsiteConfig.objects.first() - image = qrcode.make(OtpAuth(token).to_uri("totp", config.base_url, config.name)) - buf = BytesIO() - image.save(buf, "gif") - - return HttpResponse(buf.getvalue(), "image/gif") - - @login_required - @validate_serializer(TwoFactorAuthCodeSerializer) - def post(self, request): - """ - Open 2FA - """ - code = request.data["code"] - user = request.user - if OtpAuth(user.tfa_token).valid_totp(code): - user.two_factor_auth = True - user.save() - return self.success("Succeeded") - else: - return self.error("Invalid captcha") - - @login_required - @validate_serializer(TwoFactorAuthCodeSerializer) - def put(self, request): - code = request.data["code"] - user = request.user - if OtpAuth(user.tfa_token).valid_totp(code): - user.two_factor_auth = False - user.save() - else: - return self.error("Invalid captcha") diff --git a/oj/local_settings.py b/oj/local_settings.py index dfad51b..562f1f0 100644 --- a/oj/local_settings.py +++ b/oj/local_settings.py @@ -10,29 +10,6 @@ DATABASES = { } } -CACHES = { - "default": { - "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": "redis://127.0.0.1:6379/1", - "OPTIONS": { - "CLIENT_CLASS": "django_redis.client.DefaultClient", - } - }, - "JudgeQueue": { - "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": "redis://127.0.0.1:6379/2", - "OPTIONS": { - "CLIENT_CLASS": "django_redis.client.DefaultClient", - } - }, - "Throttling": { - "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": "redis://127.0.0.1:6379/3", - "OPTIONS": { - "CLIENT_CLASS": "django_redis.client.DefaultClient", - } - } -} # For celery REDIS_QUEUE = { diff --git a/oj/settings.py b/oj/settings.py index e6a6524..5b75a83 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -97,6 +97,8 @@ USE_L10N = True USE_TZ = True +# in user's profile +USER_DEFAULT_TZ = 'Asia/Shanghai' # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.8/howto/static-files/ diff --git a/oj/urls.py b/oj/urls.py index 1e79aab..5eda628 100644 --- a/oj/urls.py +++ b/oj/urls.py @@ -3,7 +3,6 @@ from django.conf.urls import include, url urlpatterns = [ url(r"^api/", include("account.urls.oj")), url(r"^api/admin/", include("account.urls.admin")), - url(r"^api/account/", include("account.urls.user")), url(r"^api/admin/", include("announcement.urls.admin")), url(r"^api/", include("conf.urls.oj")), url(r"^api/admin/", include("conf.urls.admin")), diff --git a/utils/captcha/__init__.py b/utils/captcha/__init__.py index e360b72..ce687d5 100644 --- a/utils/captcha/__init__.py +++ b/utils/captcha/__init__.py @@ -17,30 +17,30 @@ from math import ceil from six import BytesIO from PIL import Image, ImageDraw, ImageFont -__version__ = '0.3.3' +__version__ = "0.3.3" current_path = os.path.normpath(os.path.dirname(__file__)) -class Captcha(object): +class Captcha(object): def __init__(self, request): """ something init """ self.django_request = request - self.session_key = '_django_captcha_key' + self.session_key = "_django_captcha_key" self.words = ["hello", "word"] # image size (pix) self.img_width = 150 self.img_height = 30 - self.type = 'number' - self.mode = 'number' + self.type = "number" + self.mode = "number" def _get_font_size(self): s1 = int(self.img_height * 0.8) - s2 = int(self.img_width/len(self.code)) - return int(min((s1, s2)) + max((s1, s2))*0.05) + s2 = int(self.img_width / len(self.code)) + return int(min((s1, s2)) + max((s1, s2)) * 0.05) def _get_words(self): """ words list @@ -51,7 +51,6 @@ class Captcha(object): if self.words: return set(self.words) - def _set_answer(self, answer): self.django_request.session[self.session_key] = str(answer) @@ -86,16 +85,16 @@ class Captcha(object): """ # font color - self.font_color = ['black', 'darkblue', 'darkred'] + self.font_color = ["black", "darkblue", "darkred"] # background color self.background = (random.randrange(230, 255), random.randrange(230, 255), random.randrange(230, 255)) # font path - self.font_path = os.path.join(current_path, 'timesbi.ttf') # or Menlo.ttc + self.font_path = os.path.join(current_path, "timesbi.ttf") # or Menlo.ttc - self.django_request.session[self.session_key] = '' - im = Image.new('RGB', (self.img_width, self.img_height), self.background) + self.django_request.session[self.session_key] = "" + im = Image.new("RGB", (self.img_width, self.img_height), self.background) self.code = self._generate() # set font size automaticly @@ -105,20 +104,20 @@ class Captcha(object): draw = ImageDraw.Draw(im) # draw noisy point/line - if self.mode == 'word': - c = int(8/len(self.code)*3) or 3 - elif self.mode == 'number': + if self.mode == "word": + c = int(8 / len(self.code) * 3) or 3 + elif self.mode == "number": c = 4 - for i in range(random.randrange(c-2, c)): + for i in range(random.randrange(c - 2, c)): line_color = (random.randrange(0, 255), random.randrange(0, 255), random.randrange(0, 255)) - xy = (random.randrange(0, int(self.img_width*0.2)), random.randrange(0, self.img_height), - random.randrange(int(3*self.img_width/4), self.img_width), random.randrange(0, self.img_height)) - draw.line(xy, fill=line_color, width=int(self.font_size*0.1)) + xy = (random.randrange(0, int(self.img_width * 0.2)), random.randrange(0, self.img_height), + random.randrange(int(3 * self.img_width / 4), self.img_width), random.randrange(0, self.img_height)) + draw.line(xy, fill=line_color, width=int(self.font_size * 0.1)) # main part - j = int(self.font_size*0.3) - k = int(self.font_size*0.5) + j = int(self.font_size * 0.3) + k = int(self.font_size * 0.5) x = random.randrange(j, k) for i in self.code: @@ -126,21 +125,21 @@ class Captcha(object): m = int(len(self.code)) y = random.randrange(1, 3) - if i in ('+', '=', '?'): + if i in ("+", "=", "?"): # 对计算符号等特殊字符放大处理 - m = ceil(self.font_size*0.8) + m = ceil(self.font_size * 0.8) else: # 字体大小变化量,字数越少,字体大小变化越多 - m = random.randrange(0, int(45 / self.font_size) + int(self.font_size/5)) + m = random.randrange(0, int(45 / self.font_size) + int(self.font_size / 5)) - self.font = ImageFont.truetype(self.font_path.replace('\\', '/'), self.font_size + int(ceil(m))) + self.font = ImageFont.truetype(self.font_path.replace("\\", "/"), self.font_size + int(ceil(m))) draw.text((x, y), i, font=self.font, fill=random.choice(self.font_color)) - x += self.font_size*0.9 + x += self.font_size * 0.9 del x del draw with BytesIO() as buf: - im.save(buf, 'gif') + im.save(buf, "gif") buf_str = buf.getvalue() return buf_str @@ -152,7 +151,6 @@ class Captcha(object): return False code = code.strip() - _code = self.django_request.session.get(self.session_key) or '' - self.django_request.session[self.session_key] = '' + _code = self.django_request.session.get(self.session_key) or "" + self.django_request.session[self.session_key] = "" return _code.lower() == str(code).lower() - diff --git a/utils/management/commands/initadmin.py b/utils/management/commands/initadmin.py index 5829178..bdd84ae 100644 --- a/utils/management/commands/initadmin.py +++ b/utils/management/commands/initadmin.py @@ -13,7 +13,7 @@ class Command(BaseCommand): "would you like to reset it's password?\n" "Input yes to confirm: ")) if input() == "yes": - # for dev + # todo remove this in product env # rand_password = rand_str(length=6) rand_password = "rootroot" admin.save()