Merge pull request #76 from QingdaoU/chiaki_dev

Chiaki dev
This commit is contained in:
稗田千秋 2017-04-19 00:47:17 +08:00 committed by GitHub
commit c41a739e1f
19 changed files with 405 additions and 25 deletions

View File

@ -44,3 +44,23 @@ class EditUserSerializer(serializers.Serializer):
open_api = serializers.BooleanField()
two_factor_auth = serializers.BooleanField()
is_disabled = serializers.BooleanField()
class ApplyResetPasswordSerializer(serializers.Serializer):
email = serializers.EmailField()
captcha = serializers.CharField(max_length=4, min_length=4)
class ResetPasswordSerializer(serializers.Serializer):
token = serializers.CharField(min_length=1, max_length=40)
password = serializers.CharField(min_length=6, max_length=30)
captcha = serializers.CharField(max_length=4, min_length=4)
class SSOSerializer(serializers.Serializer):
appkey = serializers.CharField(max_length=35)
token = serializers.CharField(max_length=40)
class TwoFactorAuthCodeSerializer(serializers.Serializer):
code = serializers.IntegerField()

10
account/tasks.py Normal file
View File

@ -0,0 +1,10 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from celery import shared_task
from utils.mail import send_email
@shared_task
def _send_email(from_name, to_email, to_name, subject, content):
send_email(from_name, to_email, to_name, subject, content)

View File

@ -0,0 +1,78 @@
<table cellpadding="0" cellspacing="0" align="center" style="text-align:left;font-family:'微软雅黑','黑体',arial;"
width="742">
<tbody>
<tr>
<td>
<table cellpadding="0" cellspacing="0"
style="text-align:left;border:1px solid #50a5e6;color:#fff;font-size:18px;" width="740">
<tbody>
<tr height="39" style="background-color:#50a5e6;">
<td style="padding-left:15px;font-family:'微软雅黑','黑体',arial;">
{{ website_name }} 登录信息找回
</td>
</tr>
</tbody>
</table>
<table cellpadding="0" cellspacing="0"
style="text-align:left;border:1px solid #f0f0f0;border-top:none;color:#585858;background-color:#fafafa;"
width="740">
<tbody>
<tr height="25">
<td></td>
</tr>
<tr height="40">
<td style="padding-left:25px;padding-right:25px;font-size:18px;font-family:'微软雅黑','黑体',arial;">
Hello, {{ username }}:
</td>
</tr>
<tr height="15">
<td></td>
</tr>
<tr height="30">
<td style="padding-left:55px;padding-right:55px;font-family:'微软雅黑','黑体',arial;font-size:14px;">
您刚刚在 {{ website_name }} 申请了找回登录信息服务。
</td>
</tr>
<tr height="30">
<td style="padding-left:55px;padding-right:55px;font-family:'微软雅黑','黑体',arial;font-size:14px;">
请在<span style="color:rgb(255,0,0)">30分钟</span>内点击下面链接设置您的新密码:
</td>
</tr>
<tr height="60">
<td style="padding-left:55px;padding-right:55px;font-family:'微软雅黑','黑体',arial;font-size:14px;">
<a href="{{ link }}" target="_blank"
style="color: rgb(255,255,255);text-decoration: none;display: block;min-height: 39px;width: 158px;line-height: 39px;background-color:rgb(80,165,230);font-size:20px;text-align:center;">重置密码</a>
</td>
</tr>
<tr height="10">
<td></td>
</tr>
<tr height="20">
<td style="padding-left:55px;padding-right:55px;font-family:'微软雅黑','黑体',arial;font-size:12px;">
如果上面的链接点击无效,请复制以下链接至浏览器的地址栏直接打开。
</td>
</tr>
<tr height="30">
<td style="padding-left:55px;padding-right:65px;font-family:'微软雅黑','黑体',arial;">
<a href="{{ link }}" target="_blank" style="color:#0c94de;font-size:12px;">
{{ link }}
</a>
</td>
</tr>
<tr height="20">
<td style="padding-left:55px;padding-right:55px;font-family:'微软雅黑','黑体',arial;font-size:12px;">
如果您没有提出过该申请,请忽略此邮件。有可能是其他用户误填了您的邮件地址,我们不会对你的帐户进行任何修改。
请不要向他人透露本邮件的内容,否则可能会导致您的账号被盗。
</td>
</tr>
<tr height="20">
<td></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>

View File

@ -3,5 +3,5 @@ from django.conf.urls import url
from ..views.admin import UserAdminAPI
urlpatterns = [
url(r"^user$", UserAdminAPI.as_view(), name="user_admin_api"),
url(r"^user/?$", UserAdminAPI.as_view(), name="user_admin_api"),
]

View File

@ -1,9 +1,15 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from django.conf.urls import url
from ..views.oj import UserChangePasswordAPI, UserLoginAPI, UserRegisterAPI
from ..views.oj import (UserChangePasswordAPI, UserLoginAPI, UserRegisterAPI,
ApplyResetPasswordAPI, ResetPasswordAPI)
urlpatterns = [
url(r"^login$", UserLoginAPI.as_view(), name="user_login_api"),
url(r"^register$", UserRegisterAPI.as_view(), name="user_register_api"),
url(r"^change_password$", UserChangePasswordAPI.as_view(), name="user_change_password_api")
url(r"^login/?$", UserLoginAPI.as_view(), name="user_login_api"),
url(r"^register/?$", UserRegisterAPI.as_view(), name="user_register_api"),
url(r"^change_password/?$", UserChangePasswordAPI.as_view(), name="user_change_password_api"),
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")
]

15
account/urls/user.py Normal file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from django.conf.urls import url
from ..views.user import (UserInfoAPI, UserProfileAPI, AvatarUploadAPI,
SSOAPI, TwoFactorAuthAPI)
urlpatterns = [
url(r"^user/?$", UserInfoAPI.as_view(), name="user_info_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")
]

View File

@ -1,15 +1,26 @@
from django.contrib import auth
from django.core.exceptions import MultipleObjectsReturned
from django.utils.translation import ugettext as _
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import timedelta
from otpauth import OtpAuth
from django.contrib import auth
from django.conf import settings
from django.core.exceptions import MultipleObjectsReturned
from django.utils.translation import ugettext as _
from django.utils.timezone import now
from conf.models import WebsiteConfig
from utils.api import APIView, validate_serializer
from utils.captcha import Captcha
from utils.shortcuts import rand_str
from ..decorators import login_required
from ..models import User, UserProfile
from ..serializers import (UserChangePasswordSerializer, UserLoginSerializer,
UserRegisterSerializer)
UserRegisterSerializer,
ApplyResetPasswordSerializer, ResetPasswordSerializer)
from ..tasks import _send_email
class UserLoginAPI(APIView):
@ -92,3 +103,55 @@ class UserChangePasswordAPI(APIView):
return self.success(_("Succeeded"))
else:
return self.error(_("Invalid old password"))
class ApplyResetPasswordAPI(APIView):
@validate_serializer(ApplyResetPasswordSerializer)
def post(self, request):
data = request.data
captcha = Captcha(request)
config = WebsiteConfig.objects.first()
if not captcha.check(data["captcha"]):
return self.error(_("Invalid captcha"))
try:
user = User.objects.get(email=data["email"])
except User.DoesNotExist:
return self.error(_("User does not exist"))
if user.reset_password_token_expire_time and 0 < (
user.reset_password_token_expire_time - now()).total_seconds() < 20 * 60:
return self.error(_("You can only reset password once per 20 minutes"))
user.reset_password_token = rand_str()
user.reset_password_token_expire_time = now() + timedelta(minutes=20)
user.save()
email_template = open("reset_password_email.html", "w",
encoding="utf-8").read()
email_template = email_template.replace("{{ username }}", user.username). \
replace("{{ website_name }}", settings.WEBSITE_INFO["website_name"]). \
replace("{{ link }}", settings.WEBSITE_INFO["url"] + "/reset_password/t/" +
user.reset_password_token)
_send_email.delay(config.name,
user.email,
user.username,
config.name + " 登录信息找回邮件",
email_template)
return self.success(_("Succeeded"))
class ResetPasswordAPI(APIView):
@validate_serializer(ResetPasswordSerializer)
def post(self, request):
data = request.data
captcha = Captcha(request)
if not captcha.check(data["captcha"]):
return self.error(_("Invalid captcha"))
try:
user = User.objects.get(reset_password_token=data["token"])
except User.DoesNotExist:
return self.error(_("Token dose not exist"))
if 0 < (user.reset_password_token_expire_time - now()).total_seconds() < 30 * 60:
return self.error(_("Token expired"))
user.reset_password_token = None
user.set_password(data["password"])
user.save()
return self.success(_("Succeeded"))

153
account/views/user.py Normal file
View File

@ -0,0 +1,153 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import qrcode
from io import StringIO
from otpauth import OtpAuth
from django.conf import settings
from django.http import HttpResponse
from django.utils.translation import ugettext as _
from conf.models import WebsiteConfig
from utils.api import APIView, validate_serializer
from utils.shortcuts import rand_str
from ..decorators import login_required
from ..models import User
from ..serializers import (EditUserSerializer, UserSerializer,
SSOSerializer, TwoFactorAuthCodeSerializer)
class UserInfoAPI(APIView):
@login_required
def get(self, request):
"""
Return user info api
"""
return self.success(UserSerializer(request.user).data)
class UserProfileAPI(APIView):
@login_required
def get(self, request):
"""
Return user info api
"""
return self.success(UserSerializer(request.user).data)
@validate_serializer(EditUserSerializer)
@login_required
def put(self, request):
data = request.data
user_profile = request.user.userprofile
if data["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(APIView):
def post(self, request):
if "file" not in request.FILES:
return self.error(_("Upload failed"))
f = request.FILES["file"]
if f.size > 1024 * 1024:
return self.error(_("Picture too large"))
if os.path.splitext(f.name)[-1].lower() not in [".gif", ".jpg", ".jpeg", ".bmp", ".png"]:
return self.error(_("Unsupported file format"))
name = "avatar_" + rand_str(5) + os.path.splitext(f.name)[-1]
with open(os.path.join(settings.IMAGE_UPLOAD_DIR, name), "wb") as img:
for chunk in request.FILES["file"]:
img.write(chunk)
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 = StringIO()
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"))

View File

@ -3,5 +3,5 @@ from django.conf.urls import url
from ..views import AnnouncementAdminAPI
urlpatterns = [
url(r"^announcement$", AnnouncementAdminAPI.as_view(), name="announcement_admin_api"),
url(r"^announcement/?$", AnnouncementAdminAPI.as_view(), name="announcement_admin_api"),
]

View File

@ -3,7 +3,7 @@ from django.conf.urls import url
from ..views import SMTPAPI, JudgeServerAPI, WebsiteConfigAPI
urlpatterns = [
url(r"^smtp$", SMTPAPI.as_view(), name="smtp_admin_api"),
url(r"^website$", WebsiteConfigAPI.as_view(), name="website_config_api"),
url(r"^judge_server", JudgeServerAPI.as_view(), name="judge_server_api")
url(r"^smtp/?$", SMTPAPI.as_view(), name="smtp_admin_api"),
url(r"^website/?$", WebsiteConfigAPI.as_view(), name="website_config_api"),
url(r"^judge_server/?$", JudgeServerAPI.as_view(), name="judge_server_api")
]

View File

@ -3,7 +3,7 @@ from django.conf.urls import url
from ..views import JudgeServerHeartbeatAPI, LanguagesAPI, WebsiteConfigAPI
urlpatterns = [
url(r"^website$", WebsiteConfigAPI.as_view(), name="website_info_api"),
url(r"^judge_server_heartbeat$", JudgeServerHeartbeatAPI.as_view(), name="judge_server_heartbeat_api"),
url(r"^languages$", LanguagesAPI.as_view(), name="language_list_api")
url(r"^website/?$", WebsiteConfigAPI.as_view(), name="website_info_api"),
url(r"^judge_server_heartbeat/?$", JudgeServerHeartbeatAPI.as_view(), name="judge_server_heartbeat_api"),
url(r"^languages/?$", LanguagesAPI.as_view(), name="language_list_api")
]

View File

@ -3,6 +3,6 @@ from django.conf.urls import url
from ..views.admin import ContestAnnouncementAPI, ContestAPI
urlpatterns = [
url(r"^contest$", ContestAPI.as_view(), name="contest_api"),
url(r"^contest/announcement$", ContestAnnouncementAPI.as_view(), name="contest_announcement_admin_api")
url(r"^contest/?$", ContestAPI.as_view(), name="contest_api"),
url(r"^contest/announcement/?$", ContestAnnouncementAPI.as_view(), name="contest_announcement_admin_api")
]

View File

@ -3,5 +3,5 @@ from django.conf.urls import url
from ..views.oj import ContestAnnouncementListAPI
urlpatterns = [
url(r"^contest$", ContestAnnouncementListAPI.as_view(), name="contest_list_api"),
url(r"^contest/?$", ContestAnnouncementListAPI.as_view(), name="contest_list_api"),
]

View File

@ -1,5 +1,5 @@
Django<1.10
djangorestframework==3.3.3
django==1.9.6
djangorestframework==3.4.0
pillow
jsonfield
otpauth
@ -7,3 +7,6 @@ flake8-quotes
pytz
coverage
python-dateutil
celery
Envelopes
qrcode

View File

@ -3,6 +3,7 @@ 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")),

View File

@ -3,7 +3,7 @@ from django.conf.urls import url
from ..views.admin import ProblemAPI, TestCaseUploadAPI, ContestProblemAPI
urlpatterns = [
url(r"^test_case/upload$", TestCaseUploadAPI.as_view(), name="test_case_upload_api"),
url(r"^problem$", ProblemAPI.as_view(), name="problem_api"),
url(r"^contest/problem$", ContestProblemAPI.as_view(), name="contest_problem_api")
url(r"^test_case/upload/?$", TestCaseUploadAPI.as_view(), name="test_case_upload_api"),
url(r"^problem/?$", ProblemAPI.as_view(), name="problem_api"),
url(r"^contest/problem/?$", ContestProblemAPI.as_view(), name="contest_problem_api")
]

View File

@ -3,5 +3,5 @@ from django.conf.urls import url
from ..views.oj import ProblemTagAPI
urlpatterns = [
url(r"^problem/tags$", ProblemTagAPI.as_view(), name="problem_tag_list_api")
url(r"^problem/tags/?$", ProblemTagAPI.as_view(), name="problem_tag_list_api")
]

10
requirements.txt Normal file
View File

@ -0,0 +1,10 @@
django==1.9.6
djangorestframework==3.4.0
otpauth
pillow
python-dateutil
celery
Envelopes
pytz
jsonfield
qrcode

21
utils/mail.py Normal file
View File

@ -0,0 +1,21 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from envelopes import Envelope
from conf.models import SMTPConfig
def send_email(from_name, to_email, to_name, subject, content):
smtp = SMTPConfig.objects.first()
if not smtp:
return
envlope = Envelope(from_addr=(smtp.email, from_name),
to_addr=(to_email, to_name),
subject=subject,
html_body=content)
envlope.send(smtp.server,
login=smtp.email,
password=smtp.password,
port=smtp.port,
tls=smtp.tls)