Accept Merge Request #336 mr new-arch-dev : (new-arch -> dev)

Merge Request: mr new-arch-dev
Created By: @virusdefender
Accepted By: @virusdefender
URL: https://coding.net/u/virusdefender/p/qduoj/git/merge/336
This commit is contained in:
virusdefender
2015-12-23 00:33:20 +08:00
43 changed files with 1078 additions and 277 deletions

2
.gitignore vendored
View File

@@ -52,7 +52,7 @@ db.db
#redis dump
*.rdb
#*.out
db.sqlite3
*.sqlite3
.DS_Store
log/
static/release/css

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2015-12-11 14:30
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0015_userprofile_student_id'),
]
operations = [
migrations.AddField(
model_name='user',
name='tfa_token',
field=models.CharField(blank=True, max_length=10, null=True),
),
migrations.AddField(
model_name='user',
name='two_factor_auth',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2015-12-12 13:39
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0016_auto_20151211_2230'),
]
operations = [
migrations.AlterField(
model_name='user',
name='tfa_token',
field=models.CharField(blank=True, max_length=40, null=True),
),
]

View File

@@ -40,6 +40,9 @@ class User(AbstractBaseUser):
reset_password_token_create_time = models.DateTimeField(blank=True, null=True)
# 论坛授权token
auth_token = models.CharField(max_length=40, blank=True, null=True)
# 是否开启两步验证
two_factor_auth = models.BooleanField(default=False)
tfa_token = models.CharField(max_length=40, blank=True, null=True)
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = []

View File

@@ -7,7 +7,7 @@ from .models import User, UserProfile
class UserLoginSerializer(serializers.Serializer):
username = serializers.CharField(max_length=30)
password = serializers.CharField(max_length=30)
captcha = serializers.CharField(min_length=4, max_length=4)
tfa_code = serializers.CharField(min_length=6, max_length=6, required=False)
class UsernameCheckSerializer(serializers.Serializer):
@@ -84,3 +84,7 @@ class UserProfileSerializer(serializers.ModelSerializer):
model = UserProfile
fields = ["avatar", "blog", "mood", "hduoj_username", "bestcoder_username", "codeforces_username",
"rank", "accepted_number", "submissions_number", "problems_status", "phone_number", "school", "student_id"]
class TwoFactorAuthCodeSerializer(serializers.Serializer):
code = serializers.IntegerField()

8
account/tasks.py Normal file
View File

@@ -0,0 +1,8 @@
# 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

@@ -1,11 +1,13 @@
# coding=utf-8
import codecs
import qrcode
import StringIO
from django import http
from django.contrib import auth
from django.shortcuts import render
from django.db.models import Q
from django.conf import settings
from django.http import HttpResponseRedirect
from django.http import HttpResponse
from django.core.exceptions import MultipleObjectsReturned
from django.utils.timezone import now
@@ -14,7 +16,9 @@ from rest_framework.response import Response
from utils.shortcuts import (serializer_invalid_response, error_response,
success_response, error_page, paginate, rand_str)
from utils.captcha import Captcha
from utils.mail import send_email
from utils.otp_auth import OtpAuth
from .tasks import _send_email
from .decorators import login_required
from .models import User, UserProfile
@@ -23,7 +27,8 @@ from .serializers import (UserLoginSerializer, UserRegisterSerializer,
UserChangePasswordSerializer,
UserSerializer, EditUserSerializer,
ApplyResetPasswordSerializer, ResetPasswordSerializer,
SSOSerializer, EditUserProfileSerializer, UserProfileSerializer)
SSOSerializer, EditUserProfileSerializer,
UserProfileSerializer, TwoFactorAuthCodeSerializer)
from .decorators import super_admin_required
@@ -38,14 +43,23 @@ class UserLoginAPIView(APIView):
serializer = UserLoginSerializer(data=request.data)
if serializer.is_valid():
data = serializer.data
captcha = Captcha(request)
if not captcha.check(data["captcha"]):
return error_response(u"验证码错误")
print data
user = auth.authenticate(username=data["username"], password=data["password"])
# 用户名或密码错误的话 返回None
if user:
if not user.two_factor_auth:
auth.login(request, user)
return success_response(u"登录成功")
# 没有输入两步验证的验证码
if user.two_factor_auth and "tfa_code" not in data:
return success_response("tfa_required")
if OtpAuth(user.tfa_token).valid_totp(data["tfa_code"]):
auth.login(request, user)
return success_response(u"登录成功")
else:
return error_response(u"验证码错误")
else:
return error_response(u"用户名或密码错误")
else:
@@ -151,9 +165,9 @@ class EmailCheckAPIView(APIView):
检测邮箱是否存在,用状态码标识结果
---
"""
#这里是为了适应前端表单验证空间的要求
# 这里是为了适应前端表单验证空间的要求
reset = request.GET.get("reset", None)
#如果reset为true说明该请求是重置密码页面发出的要返回的状态码应正好相反
# 如果reset为true说明该请求是重置密码页面发出的要返回的状态码应正好相反
if reset:
existed = 200
does_not_existed = 400
@@ -287,18 +301,21 @@ class ApplyResetPasswordAPIView(APIView):
user = User.objects.get(email=data["email"])
except User.DoesNotExist:
return error_response(u"用户不存在")
if user.reset_password_token_create_time and (now() - user.reset_password_token_create_time).total_seconds() < 20 * 60:
if user.reset_password_token_create_time and (
now() - user.reset_password_token_create_time).total_seconds() < 20 * 60:
return error_response(u"20分钟内只能找回一次密码")
user.reset_password_token = rand_str()
user.reset_password_token_create_time = now()
user.save()
email_template = codecs.open(settings.TEMPLATES[0]["DIRS"][0] + "utils/reset_password_email.html", "r", "utf-8").read()
email_template = codecs.open(settings.TEMPLATES[0]["DIRS"][0] + "utils/reset_password_email.html", "r",
"utf-8").read()
email_template = email_template.replace("{{ username }}", user.username).\
replace("{{ website_name }}", settings.WEBSITE_INFO["website_name"]).\
replace("{{ link }}", request.scheme + "://" + request.META['HTTP_HOST'] + "/reset_password/t/" + user.reset_password_token)
email_template = email_template.replace("{{ username }}", user.username). \
replace("{{ website_name }}", settings.WEBSITE_INFO["website_name"]). \
replace("{{ link }}", request.scheme + "://" + request.META[
'HTTP_HOST'] + "/reset_password/t/" + user.reset_password_token)
send_email(settings.WEBSITE_INFO["website_name"],
_send_email.delay(settings.WEBSITE_INFO["website_name"],
user.email,
user.username,
settings.WEBSITE_INFO["website_name"] + u" 登录信息找回邮件",
@@ -350,7 +367,10 @@ class SSOAPIView(APIView):
if serializer.is_valid():
try:
user = User.objects.get(auth_token=serializer.data["token"])
return success_response({"username": user.username})
user.auth_token = None
user.save()
return success_response(
{"username": user.username, "admin_type": user.admin_type, "avatar": user.userprofile.avatar})
except User.DoesNotExist:
return error_response(u"用户不存在")
else:
@@ -364,7 +384,8 @@ class SSOAPIView(APIView):
token = rand_str()
request.user.auth_token = token
request.user.save()
return render(request, "oj/account/sso.html", {"redirect_url": callback + "?token=" + token, "callback": callback})
return render(request, "oj/account/sso.html",
{"redirect_url": callback + "?token=" + token, "callback": callback})
def reset_password_page(request, token):
@@ -375,3 +396,55 @@ def reset_password_page(request, token):
if (now() - user.reset_password_token_create_time).total_seconds() > 30 * 60:
return error_page(request, u"链接已过期")
return render(request, "oj/account/reset_password.html", {"user": user})
class TwoFactorAuthAPIView(APIView):
@login_required
def get(self, request):
"""
获取绑定二维码
"""
user = request.user
if user.two_factor_auth:
return error_response(u"已经开启两步验证了")
token = rand_str()
user.tfa_token = token
user.save()
image = qrcode.make(OtpAuth(token).to_uri("totp", settings.WEBSITE_INFO["url"], "OnlineJudgeAdmin"))
buf = StringIO.StringIO()
image.save(buf, 'gif')
return HttpResponse(buf.getvalue(), 'image/gif')
@login_required
def post(self, request):
"""
开启两步验证
"""
serializer = TwoFactorAuthCodeSerializer(data=request.data)
if serializer.is_valid():
code = serializer.data["code"]
user = request.user
if OtpAuth(user.tfa_token).valid_totp(code):
user.two_factor_auth = True
user.save()
return success_response(u"开启两步验证成功")
else:
return error_response(u"验证码错误")
else:
return serializer_invalid_response(serializer)
@login_required
def put(self, request):
serializer = TwoFactorAuthCodeSerializer(data=request.data)
if serializer.is_valid():
user = request.user
code = serializer.data["code"]
if OtpAuth(user.tfa_token).valid_totp(code):
user.two_factor_auth = False
user.save()
else:
return error_response(u"验证码错误")
else:
return serializer_invalid_response(serializer)

View File

@@ -110,7 +110,8 @@ class ContestRank(models.Model):
# 之前已经提交过,但是是错误的,这次提交是正确的。错误的题目不计入罚时
self.total_time += (info["ac_time"] + info["error_number"] * 20 * 60)
problem = ContestProblem.objects.get(id=submission.problem_id)
if problem.total_accepted_number == 0:
# 更新题目计数器在前 所以是1
if problem.total_accepted_number == 1:
info["is_first_ac"] = True
else:
@@ -128,7 +129,7 @@ class ContestRank(models.Model):
self.total_time += info["ac_time"]
problem = ContestProblem.objects.get(id=submission.problem_id)
if problem.total_accepted_number == 0:
if problem.total_accepted_number == 1:
info["is_first_ac"] = True
else:

View File

View File

@@ -3,6 +3,6 @@ ENV PYTHONBUFFERED 1
RUN mkdir -p /code/log /code/test_case /code/upload
WORKDIR /code
ADD requirements.txt /code/
RUN pip install -r requirements.txt
RUN pip install -i http://pypi.douban.com/simple -r requirements.txt --trusted-host pypi.douban.com
EXPOSE 8010
CMD supervisord

View File

@@ -11,4 +11,6 @@ supervisor
pillow
jsonfield
Envelopes
huey
celery
django-celery
qrcode

View File

@@ -1,6 +1,6 @@
[program:mq]
[program:task_queue]
command=python manage.py run_huey
command=python manage.py celeryd -B -l DEBUG
directory=/code/
user=root

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('judge_dispatcher', '0002_auto_20151207_2310'),
]
operations = [
migrations.AddField(
model_name='judgeserver',
name='create_time',
field=models.DateTimeField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='judgeserver',
name='name',
field=models.CharField(default='judger', max_length=30),
preserve_default=False,
),
]

View File

@@ -3,6 +3,7 @@ from django.db import models
class JudgeServer(models.Model):
name = models.CharField(max_length=30)
ip = models.GenericIPAddressField()
port = models.IntegerField()
# 这个服务器最大可能运行的判题实例数量
@@ -14,6 +15,7 @@ class JudgeServer(models.Model):
lock = models.BooleanField(default=False)
# status 为 false 的时候代表不使用这个服务器
status = models.BooleanField(default=True)
create_time = models.DateTimeField(auto_now_add=True, blank=True, null=True)
def use_judge_instance(self):
# 因为use 和 release 中间是判题时间,可能这个 model 的数据已经被修改了所以不能直接使用self.xxx否则取到的是旧数据

View File

@@ -0,0 +1,29 @@
# coding=utf-8
import json
from rest_framework import serializers
from .models import JudgeServer
class CreateJudgesSerializer(serializers.Serializer):
name = serializers.CharField(max_length=30)
ip = serializers.IPAddressField()
port = serializers.IntegerField()
# 这个服务器最大可能运行的判题实例数量
max_instance_number = serializers.IntegerField()
token = serializers.CharField(max_length=30)
class EditJudgesSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField(max_length=30)
ip = serializers.IPAddressField()
port = serializers.IntegerField()
# 这个服务器最大可能运行的判题实例数量
max_instance_number = serializers.IntegerField()
token = serializers.CharField(max_length=30)
status = serializers.BooleanField()
class JudgesSerializer(serializers.ModelSerializer):
class Meta:
model = JudgeServer

View File

@@ -32,7 +32,7 @@ class JudgeDispatcher(object):
if servers.exists():
return servers.first()
def judge(self, is_waiting_task=False):
def judge(self):
self.submission.judge_start_time = int(time.time() * 1000)
with transaction.atomic():
@@ -89,7 +89,7 @@ class JudgeDispatcher(object):
submission = Submission.objects.get(id=waiting_submission.submission_id)
waiting_submission.delete()
_judge(submission, time_limit=waiting_submission.time_limit,
_judge.delay(submission, time_limit=waiting_submission.time_limit,
memory_limit=waiting_submission.memory_limit, test_case_id=waiting_submission.test_case_id,
is_waiting_task=True)
@@ -113,13 +113,14 @@ class JudgeDispatcher(object):
# 普通题目的话,到这里就结束了
def update_contest_problem_status(self):
# 能运行到这里的都是比赛题目
# 能运行到这里的都是比赛题目a
contest = Contest.objects.get(id=self.submission.contest_id)
if contest.status != CONTEST_UNDERWAY:
logger.info("Contest debug mode, id: " + str(contest.id) + ", submission id: " + self.submission.id)
return
with transaction.atomic():
contest_problem = ContestProblem.objects.select_for_update().get(contest=contest, id=self.submission.problem_id)
contest_problem = ContestProblem.objects.select_for_update().get(contest=contest,
id=self.submission.problem_id)
contest_problem.add_submission_number()

71
judge_dispatcher/views.py Normal file
View File

@@ -0,0 +1,71 @@
# coding=utf-8
from rest_framework.views import APIView
from account.decorators import super_admin_required
from utils.shortcuts import success_response, serializer_invalid_response, error_response, paginate
from .serializers import CreateJudgesSerializer, JudgesSerializer, EditJudgesSerializer
from .models import JudgeServer
class AdminJudgeServerAPIView(APIView):
@super_admin_required
def post(self, request):
"""
添加判题服务器 json api接口
---
request_serializer: CreateJudgesSerializer
response_serializer: JudgesSerializer
"""
serializer = CreateJudgesSerializer(data=request.data)
if serializer.is_valid():
data = serializer.data
judge_server = JudgeServer.objects.create(name=data["name"], ip=data["ip"], port=data["port"],
max_instance_number=data["max_instance_number"],
token=data["token"],
created_by=request.user)
return success_response(JudgesSerializer(judge_server).data)
else:
return serializer_invalid_response(serializer)
@super_admin_required
def put(self, request):
"""
修改判题服务器信息 json api接口
---
request_serializer: EditJudgesSerializer
response_serializer: JudgesSerializer
"""
serializer = EditJudgesSerializer(data=request.data)
if serializer.is_valid():
data = serializer.data
try:
judge_server = JudgeServer.objects.get(pk=data["id"])
except JudgeServer.DoesNotExist:
return error_response(u"此判题服务器不存在!")
judge_server.name = data["name"]
judge_server.ip = data["ip"]
judge_server.port = data["port"]
judge_server.max_instance_number = data["max_instance_number"]
judge_server.token = data["token"]
judge_server.status = data["status"]
judge_server.save()
return success_response(JudgesSerializer(judge_server).data)
else:
return serializer_invalid_response(serializer)
@super_admin_required
def get(self, request):
"""
获取全部判题服务器
"""
judge_server_id = request.GET.get("judge_server_id", None)
if judge_server_id:
try:
judge_server = JudgeServer.objects.get(id=judge_server_id)
except JudgeServer.DoesNotExist:
return error_response(u"判题服务器不存在")
return success_response(JudgesSerializer(judge_server).data)
judge_server = JudgeServer.objects.all()
return paginate(request, judge_server, JudgesSerializer)

View File

View File

@@ -1,19 +0,0 @@
# coding=utf-8
import redis
import datetime
from rest_framework.views import APIView
from judge.result import result
from django.conf import settings
from utils.shortcuts import success_response
from submission.models import Submission
class QueueLengthMonitorAPIView(APIView):
def get(self, request):
r = redis.Redis(host=settings.redis_config["host"], port=settings.redis_config["port"], db=settings.redis_config["db"])
waiting_number = r.get("judge_queue_length")
if waiting_number is None:
waiting_number = 0
now = datetime.datetime.now()
return success_response({"time": ":".join([str(now.hour), str(now.minute), str(now.second)]),
"count": waiting_number})

View File

@@ -7,3 +7,8 @@
|___/ |___/ |_|
https://github.com/QingdaoU/OnlineJudge
"""
from __future__ import absolute_import
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app

19
oj/celery.py Normal file
View File

@@ -0,0 +1,19 @@
from __future__ import absolute_import
import os
from celery import Celery
# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oj.settings')
from django.conf import settings
app = Celery('oj')
# Using a string here means the worker will not have to
# pickle the object when using Windows.
app.config_from_object('django.conf:settings')
# load task modules from all registered Django app configs.
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)

View File

@@ -28,6 +28,12 @@ REDIS_QUEUE = {
"db": 2
}
# for celery
BROKER_URL = 'redis://%s:%s/%s' % (REDIS_QUEUE["host"], str(REDIS_QUEUE["port"]), str(REDIS_QUEUE["db"]))
ACCEPT_CONTENT = ['json']
DEBUG = True
ALLOWED_HOSTS = []

View File

@@ -37,6 +37,12 @@ REDIS_QUEUE = {
"db": 2
}
# for celery
BROKER_URL = 'redis://%s:%s/%s' % (REDIS_QUEUE["host"], str(REDIS_QUEUE["port"]), str(REDIS_QUEUE["db"]))
ACCEPT_CONTENT = ['json']
DEBUG = False
ALLOWED_HOSTS = ['*']

View File

@@ -10,7 +10,7 @@ https://docs.djangoproject.com/en/1.8/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.8/ref/settings/
"""
from __future__ import absolute_import
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
@@ -22,6 +22,9 @@ if ENV == "local":
elif ENV == "server":
from .server_settings import *
import djcelery
djcelery.setup_loader()
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -53,7 +56,8 @@ INSTALLED_APPS = (
'judge_dispatcher',
'rest_framework',
'huey.djhuey',
'djcelery',
)
if DEBUG:
@@ -186,14 +190,6 @@ WEBSITE_INFO = {"website_name": "qduoj",
"website_footer": u"青岛大学信息工程学院 创新实验室 <a href=\"http://www.miibeian.gov.cn/\">京ICP备15062075号-1</a>",
"url": "https://qduoj.com"}
HUEY = {
'backend': 'huey.backends.redis_backend',
'name': 'task_queue',
'connection': {'host': REDIS_QUEUE["host"], 'port': REDIS_QUEUE["port"], 'db': REDIS_QUEUE["db"]},
'always_eager': False, # Defaults to False when running via manage.py run_huey
# Options to pass into the consumer when running ``manage.py run_huey``
'consumer_options': {'workers': 50},
}
SMTP_CONFIG = {"smtp_server": "smtp.mxhichina.com",
"email": "noreply@qduoj.com",

View File

@@ -6,7 +6,8 @@ from django.views.generic import TemplateView
from account.views import (UserLoginAPIView, UsernameCheckAPIView, UserRegisterAPIView,
UserChangePasswordAPIView, EmailCheckAPIView,
UserAdminAPIView, UserInfoAPIView, ResetPasswordAPIView,
ApplyResetPasswordAPIView, SSOAPIView, UserProfileAPIView)
ApplyResetPasswordAPIView, SSOAPIView, UserProfileAPIView,
TwoFactorAuthAPIView)
from announcement.views import AnnouncementAdminAPIView
@@ -22,11 +23,9 @@ from admin.views import AdminTemplateView
from problem.views import TestCaseUploadAPIView, ProblemTagAdminAPIView, ProblemAdminAPIView
from submission.views import (SubmissionAPIView, SubmissionAdminAPIView, ContestSubmissionAPIView,
SubmissionShareAPIView, SubmissionRejudgeAdminAPIView)
from monitor.views import QueueLengthMonitorAPIView
from judge_dispatcher.views import AdminJudgeServerAPIView
from utils.views import SimditorImageUploadAPIView
urlpatterns = [
url("^$", "account.views.index_page", name="index_page"),
@@ -56,7 +55,6 @@ urlpatterns = [
url(r'^api/submission/$', SubmissionAPIView.as_view(), name="submission_api"),
url(r'^api/group_join/$', JoinGroupAPIView.as_view(), name="group_join_api"),
url(r'^api/admin/upload_image/$', SimditorImageUploadAPIView.as_view(), name="simditor_upload_image"),
url(r'^api/admin/announcement/$', AnnouncementAdminAPIView.as_view(), name="announcement_admin_api"),
url(r'^api/admin/contest/$', ContestAdminAPIView.as_view(), name="contest_admin_api"),
@@ -65,16 +63,17 @@ urlpatterns = [
url(r'^api/admin/group_member/$', GroupMemberAdminAPIView.as_view(), name="group_member_admin_api"),
url(r'^api/admin/group/promot_as_admin/$', GroupPrometAdminAPIView.as_view(), name="group_promote_admin_api"),
url(r'^api/admin/problem/$', ProblemAdminAPIView.as_view(), name="problem_admin_api"),
url(r'^api/admin/contest_problem/$', ContestProblemAdminAPIView.as_view(), name="contest_problem_admin_api"),
url(r'^api/admin/contest_problem/public/', MakeContestProblemPublicAPIView.as_view(), name="make_contest_problem_public"),
url(r'^api/admin/contest_problem/public/', MakeContestProblemPublicAPIView.as_view(),
name="make_contest_problem_public"),
url(r'^api/admin/test_case_upload/$', TestCaseUploadAPIView.as_view(), name="test_case_upload_api"),
url(r'^api/admin/tag/$', ProblemTagAdminAPIView.as_view(), name="problem_tag_admin_api"),
url(r'^api/admin/join_group_request/$', JoinGroupRequestAdminAPIView.as_view(),
name="join_group_request_admin_api"),
url(r'^api/admin/submission/$', SubmissionAdminAPIView.as_view(), name="submission_admin_api_view"),
url(r'^api/admin/monitor/$', QueueLengthMonitorAPIView.as_view(), name="queue_length_monitor_api"),
url(r'^api/admin/judges/$', AdminJudgeServerAPIView.as_view(), name="judges_admin_api"),
url(r'^contest/(?P<contest_id>\d+)/problem/(?P<contest_problem_id>\d+)/$', "contest.views.contest_problem_page",
name="contest_problem_page"),
@@ -93,14 +92,12 @@ urlpatterns = [
url(r'^contests/$', "contest.views.contest_list_page", name="contest_list_page"),
url(r'^contests/(?P<page>\d+)/$', "contest.views.contest_list_page", name="contest_list_page"),
url(r'^problem/(?P<problem_id>\d+)/$', "problem.views.problem_page", name="problem_page"),
url(r'^problems/$', "problem.views.problem_list_page", name="problem_list_page"),
url(r'^problems/(?P<page>\d+)/$', "problem.views.problem_list_page", name="problem_list_page"),
url(r'^problem/(?P<problem_id>\d+)/submissions/$', "submission.views.problem_my_submissions_list_page",
name="problem_my_submissions_page"),
url(r'^submission/(?P<submission_id>\w+)/$', "submission.views.my_submission", name="my_submission_page"),
url(r'^submissions/$', "submission.views.my_submission_list_page", name="my_submission_list_page"),
url(r'^submissions/(?P<page>\d+)/$', "submission.views.my_submission_list_page", name="my_submission_list_page"),
@@ -127,12 +124,16 @@ urlpatterns = [
url(r'^api/apply_reset_password/$', ApplyResetPasswordAPIView.as_view(), name="apply_reset_password_api"),
url(r'^api/reset_password/$', ResetPasswordAPIView.as_view(), name="apply_reset_password_api"),
url(r'^account/settings/$', TemplateView.as_view(template_name="oj/account/settings.html"), name="account_setting_page"),
url(r'^account/settings/avatar/$', TemplateView.as_view(template_name="oj/account/avatar.html"), name="avatar_settings_page"),
url(r'^account/settings/$', TemplateView.as_view(template_name="oj/account/settings.html"),
name="account_setting_page"),
url(r'^account/settings/avatar/$', TemplateView.as_view(template_name="oj/account/avatar.html"),
name="avatar_settings_page"),
url(r'^account/sso/$', SSOAPIView.as_view(), name="sso_api"),
url(r'^api/account/userprofile/$', UserProfileAPIView.as_view(), name="userprofile_api"),
url(r'^reset_password/$', TemplateView.as_view(template_name="oj/account/apply_reset_password.html"), name="apply_reset_password_page"),
url(r'^reset_password/t/(?P<token>\w+)/$', "account.views.reset_password_page", name="reset_password_page")
url(r'^reset_password/t/(?P<token>\w+)/$', "account.views.reset_password_page", name="reset_password_page"),
url(r'^api/two_factor_auth/$', TwoFactorAuthAPIView.as_view(), name="two_factor_auth_api"),
url(r'^two_factor_auth/$', TemplateView.as_view(template_name="oj/account/two_factor_auth.html"), name="two_factor_auth_page"),
]

View File

@@ -118,3 +118,13 @@ li.problem-tag {
padding-top: 7.5px;
padding-bottom: 7.5px;
}
#tfa-qrcode{
height: 40%;
width: 40%;
}
#tfa-area{
display: none;
}

View File

@@ -22,9 +22,10 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "bootstrap"], function ($,
}
var superAdminNav = [
{ name: "首页",
{
name: "首页",
children: [{name: "主页", hash: "#index/index"},
{name: "监控", hash: "#monitor/monitor"}]
{name: "判题服务器", hash: "#judges/judges"}]
},
{
name: "通用",
@@ -49,7 +50,8 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "bootstrap"], function ($,
];
var adminNav = [
{ name: "首页",
{
name: "首页",
children: [{name: "主页", hash: "#index/index"}]
},
{
@@ -79,7 +81,7 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "bootstrap"], function ($,
hide_loading: function () {
$("#loading-gif").hide();
},
getLiId: function(hash){
getLiId: function (hash) {
return hash.replace("#", "li-").replace("/", "-");
}
});
@@ -89,14 +91,14 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "bootstrap"], function ($,
url: "/api/user/",
method: "get",
dataType: "json",
success: function(data){
if(!data.code){
success: function (data) {
if (!data.code) {
vm.username = data.data.username;
vm.adminType = data.data.admin_type;
if (data.data.admin_type == 2){
if (data.data.admin_type == 2) {
vm.adminNavList = superAdminNav;
}
else{
else {
vm.adminNavList = adminNav;
}
}
@@ -104,7 +106,6 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "bootstrap"], function ($,
});
avalon.scan();
window.onhashchange = function () {
@@ -115,12 +116,14 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "bootstrap"], function ($,
show_template("template/" + hash + ".html");
}
};
setTimeout(function(){li_active("#li-" + hash.replace("/", "-"));}, 500);
setTimeout(function () {
li_active("#li-" + hash.replace("/", "-"));
}, 500);
$.ajaxSetup({
beforeSend: csrfTokenHeader,
dataType: "json",
error: function(){
error: function () {
bsAlert("请求失败");
}
});

View File

@@ -0,0 +1,143 @@
require(["jquery", "avalon", "csrfToken", "bsAlert", "validator", "pager"],
function ($, avalon, csrfTokenHeader, bsAlert, editor) {
avalon.ready(function () {
if (avalon.vmodels.judges) {
var vm = avalon.vmodels.judges;
}
else {
var vm = avalon.define({
$id: "judges",
judgesList: [],
isEditing: false,
showEnableOnly: false,
//编辑器同步变量
max_instance_number: 0,
ipAddress: "",
port: 0,
status: true,
judgesId: -1,
name: "",
token: "",
id: 0,
pager: {
getPage: function (page) {
getPage(page);
}
},
editJudges: function (judges) {
vm.id = judges.id;
vm.name = judges.name;
vm.judgesId = judges.id;
vm.status = judges.status;
vm.port = judges.port;
vm.ipAddress = judges.ip;
vm.max_instance_number = judges.max_instance_number;
vm.token = judges.token;
vm.isEditing = true;
},
cancelEdit: function () {
vm.isEditing = false;
}
});
vm.$watch("showEnableOnly", function () {
getPage(1);
avalon.vmodels.judgesPager.currentPage = 1;
});
}
function getPage(page) {
var url = "/api/admin/judges/?paging=true&page=" + page + "&page_size=20";
if (vm.showEnableNnly)
url += "&status=true";
$.ajax({
url: url,
method: "get",
success: function (data) {
if (!data.code) {
vm.judgesList = data.data.results;
avalon.vmodels.judgesPager.totalPage = data.data.total_page;
}
else {
bsAlert(data.data);
}
}
});
}
$("#judges-form").validator().on('submit', function (e) {
if (!e.isDefaultPrevented()) {
var name = $("#name").val();
var max_instance_number = $("#max_instance_number").val();
var ip = $("#ipAddress").val();
var port = $("#port").val();
var token = $("#token").val();
$.ajax({
url: "/api/admin/judges/",
contentType: "application/json",
data: JSON.stringify({
name: name,
ip: ip,
port: port,
token: token,
max_instance_number: max_instance_number
}),
dataType: "json",
method: "post",
success: function (data) {
if (!data.code) {
bsAlert("提交成功!");
$("#name").val("");
$("#max_instance_number").val("");
$("#ipAddress").val("");
$("#port").val("");
$("#token").val("");
getPage(1);
} else {
bsAlert(data.data);
}
}
});
return false;
}
});
$("#edit-judges-form").validator().on('submit', function (e) {
if (!e.isDefaultPrevented()) {
var name = vm.name;
var max_instance_number = vm.max_instance_number;
var ip = vm.ipAddress;
var port = vm.port;
var token = vm.token;
var status = vm.status;
var id = vm.id;
$.ajax({
url: "/api/admin/judges/",
contentType: "application/json",
data: JSON.stringify({
id: id,
name: name,
ip: ip,
port: port,
token: token,
max_instance_number: max_instance_number,
status: status
}),
dataType: "json",
method: "put",
success: function (data) {
if (!data.code) {
bsAlert("提交成功!");
getPage(1);
} else {
bsAlert(data.data);
}
}
});
return false;
}
});
});
avalon.scan();
});

View File

@@ -1,48 +0,0 @@
require(["jquery", "chart"], function ($, Chart) {
var data = {
labels: ["初始化"],
datasets: [
{
label: "队列长度",
fillColor: "rgba(255,255,255,0.2)",
strokeColor: "rgba(151,187,205,1)",
pointColor: "rgba(151,187,205,1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(151,187,205,1)",
data: [0]
}
]
};
var chart = new Chart($("#waiting-queue-chart").get(0).getContext("2d")).Line(data);
var dataCounter = 0;
function getMonitorData(){
var hash = location.hash;
if (hash != "#monitor/monitor"){
clearInterval(intervalId);
}
$.ajax({
url: "/api/admin/monitor/",
method: "get",
dataType: "json",
success: function(data){
if(!data.code){
chart.addData([data.data["count"]], data.data["time"])
dataCounter ++;
}
}
})
}
$("#clear-chart-data").click(function(){
for(var i = 0;i < dataCounter;i++) {
chart.removeData();
}
dataCounter = 0;
});
var intervalId = setInterval(getMonitorData, 3000);
});

View File

@@ -4,23 +4,31 @@ require(["jquery", "bsAlert", "csrfToken", "validator"], function ($, bsAlert, c
if (!e.isDefaultPrevented()) {
var username = $("#username").val();
var password = $("#password").val();
var captcha = $("#captcha").val();
var tfaCode = $("#tfa-code").val();
console.log(tfaCode);
if(tfaCode.length && tfaCode.length != 6){
bsAlert("验证码为六位数字");
return false;
}
$.ajax({
beforeSend: csrfTokenHeader,
url: "/api/login/",
data: {username: username, password: password, captcha: captcha},
data: {username: username, password: password, tfa_code: tfaCode},
dataType: "json",
method: "post",
success: function (data) {
if (!data.code) {
if(data.data == "tfa_required"){
$("#tfa-area").show();
return false;
}
function getLocationVal(id){
var temp = unescape(location.search).split(id+"=")[1] || "";
return temp.indexOf("&")>=0 ? temp.split("&")[0] : temp;
}
var from = getLocationVal("__from");
if(from != ""){
console.log(from);
window.location.href = from;
}
else{
@@ -28,7 +36,6 @@ require(["jquery", "bsAlert", "csrfToken", "validator"], function ($, bsAlert, c
}
}
else {
refresh_captcha();
bsAlert(data.data);
}
},
@@ -40,11 +47,5 @@ require(["jquery", "bsAlert", "csrfToken", "validator"], function ($, bsAlert, c
return false;
}
});
function refresh_captcha(){
$("#captcha-img")[0].src = "/captcha/?" + Math.random();
$("#captcha")[0].value = "";
}
$("#captcha-img").click(function(){
refresh_captcha();
});
});

View File

@@ -0,0 +1,27 @@
require(["jquery", "bsAlert", "csrfToken"], function ($, bsAlert, csrfTokenHeader) {
$("#tfa_submit").click(function(){
var code = $("#tfa_code").val();
if (code.length != 6){
bsAlert("验证码是6位数字");
return;
}
$.ajax({
beforeSend: csrfTokenHeader,
url: "/api/two_factor_auth/",
data: {code: code},
dataType: "json",
method: "post",
success: function(data){
if(data.code){
bsAlert(data.data);
}
else{
bsAlert("两步验证开启成功");
location.reload();
}
}
})
})
});

View File

@@ -54,31 +54,31 @@
//以下都是页面 script 标签引用的js
announcement_0_pack: "app/admin/announcement/announcement",
userList_1_pack: "app/admin/user/userList",
problem_2_pack: "app/oj/problem/problem",
submissionList_3_pack: "app/admin/problem/submissionList",
contestCountdown_4_pack: "app/oj/contest/contestCountdown",
avatar_5_pack: "app/oj/account/avatar",
addProblem_6_pack: "app/admin/problem/addProblem",
problem_7_pack: "app/admin/problem/problem",
contestList_8_pack: "app/admin/contest/contestList",
admin_9_pack: "app/admin/admin",
login_10_pack: "app/oj/account/login",
applyResetPassword_11_pack: "app/oj/account/applyResetPassword",
addContest_12_pack: "app/admin/contest/addContest",
contestPassword_13_pack: "app/oj/contest/contestPassword",
changePassword_14_pack: "app/oj/account/changePassword",
monitor_15_pack: "app/admin/monitor/monitor",
editProblem_16_pack: "app/admin/contest/editProblem",
joinGroupRequestList_17_pack: "app/admin/group/joinGroupRequestList",
group_18_pack: "app/oj/group/group",
contestProblemList_19_pack: "app/admin/contest/contestProblemList",
editProblem_20_pack: "app/admin/problem/editProblem",
register_21_pack: "app/oj/account/register",
groupDetail_22_pack: "app/admin/group/groupDetail",
editContest_23_pack: "app/admin/contest/editContest",
resetPassword_24_pack: "app/oj/account/resetPassword",
group_25_pack: "app/admin/group/group",
settings_26_pack: "app/oj/account/settings"
twoFactorAuth_2_pack: "app/oj/account/twoFactorAuth",
problem_3_pack: "app/oj/problem/problem",
submissionList_4_pack: "app/admin/problem/submissionList",
contestCountdown_5_pack: "app/oj/contest/contestCountdown",
avatar_6_pack: "app/oj/account/avatar",
addProblem_7_pack: "app/admin/problem/addProblem",
problem_8_pack: "app/admin/problem/problem",
contestList_9_pack: "app/admin/contest/contestList",
admin_10_pack: "app/admin/admin",
login_11_pack: "app/oj/account/login",
applyResetPassword_12_pack: "app/oj/account/applyResetPassword",
addContest_13_pack: "app/admin/contest/addContest",
contestPassword_14_pack: "app/oj/contest/contestPassword",
changePassword_15_pack: "app/oj/account/changePassword",
editProblem_17_pack: "app/admin/contest/editProblem",
joinGroupRequestList_18_pack: "app/admin/group/joinGroupRequestList",
group_19_pack: "app/oj/group/group",
contestProblemList_20_pack: "app/admin/contest/contestProblemList",
editProblem_21_pack: "app/admin/problem/editProblem",
register_22_pack: "app/oj/account/register",
groupDetail_23_pack: "app/admin/group/groupDetail",
editContest_24_pack: "app/admin/contest/editContest",
resetPassword_25_pack: "app/oj/account/resetPassword",
group_26_pack: "app/admin/group/group",
settings_27_pack: "app/oj/account/settings"
},
shim: {
avalon: {
@@ -96,79 +96,79 @@
name: "userList_1_pack"
},
{
name: "problem_2_pack"
name: "twoFactorAuth_2_pack"
},
{
name: "submissionList_3_pack"
name: "problem_3_pack"
},
{
name: "contestCountdown_4_pack"
name: "submissionList_4_pack"
},
{
name: "avatar_5_pack"
name: "contestCountdown_5_pack"
},
{
name: "addProblem_6_pack"
name: "avatar_6_pack"
},
{
name: "problem_7_pack"
name: "addProblem_7_pack"
},
{
name: "contestList_8_pack"
name: "problem_8_pack"
},
{
name: "admin_9_pack"
name: "contestList_9_pack"
},
{
name: "login_10_pack"
name: "admin_10_pack"
},
{
name: "applyResetPassword_11_pack"
name: "login_11_pack"
},
{
name: "addContest_12_pack"
name: "applyResetPassword_12_pack"
},
{
name: "contestPassword_13_pack"
name: "addContest_13_pack"
},
{
name: "changePassword_14_pack"
name: "contestPassword_14_pack"
},
{
name: "monitor_15_pack"
name: "changePassword_15_pack"
},
{
name: "editProblem_16_pack"
name: "editProblem_17_pack"
},
{
name: "joinGroupRequestList_17_pack"
name: "joinGroupRequestList_18_pack"
},
{
name: "group_18_pack"
name: "group_19_pack"
},
{
name: "contestProblemList_19_pack"
name: "contestProblemList_20_pack"
},
{
name: "editProblem_20_pack"
name: "editProblem_21_pack"
},
{
name: "register_21_pack"
name: "register_22_pack"
},
{
name: "groupDetail_22_pack"
name: "groupDetail_23_pack"
},
{
name: "editContest_23_pack"
name: "editContest_24_pack"
},
{
name: "resetPassword_24_pack"
name: "resetPassword_25_pack"
},
{
name: "group_25_pack"
name: "group_26_pack"
},
{
name: "settings_26_pack"
name: "settings_27_pack"
}
],
optimizeCss: "standard",

View File

@@ -56,31 +56,31 @@ var require = {
//以下都是页面 script 标签引用的js
announcement_0_pack: "app/admin/announcement/announcement",
userList_1_pack: "app/admin/user/userList",
problem_2_pack: "app/oj/problem/problem",
submissionList_3_pack: "app/admin/problem/submissionList",
contestCountdown_4_pack: "app/oj/contest/contestCountdown",
avatar_5_pack: "app/oj/account/avatar",
addProblem_6_pack: "app/admin/problem/addProblem",
problem_7_pack: "app/admin/problem/problem",
contestList_8_pack: "app/admin/contest/contestList",
admin_9_pack: "app/admin/admin",
login_10_pack: "app/oj/account/login",
applyResetPassword_11_pack: "app/oj/account/applyResetPassword",
addContest_12_pack: "app/admin/contest/addContest",
contestPassword_13_pack: "app/oj/contest/contestPassword",
changePassword_14_pack: "app/oj/account/changePassword",
monitor_15_pack: "app/admin/monitor/monitor",
editProblem_16_pack: "app/admin/contest/editProblem",
joinGroupRequestList_17_pack: "app/admin/group/joinGroupRequestList",
group_18_pack: "app/oj/group/group",
contestProblemList_19_pack: "app/admin/contest/contestProblemList",
editProblem_20_pack: "app/admin/problem/editProblem",
register_21_pack: "app/oj/account/register",
groupDetail_22_pack: "app/admin/group/groupDetail",
editContest_23_pack: "app/admin/contest/editContest",
resetPassword_24_pack: "app/oj/account/resetPassword",
group_25_pack: "app/admin/group/group",
settings_26_pack: "app/oj/account/settings",
twoFactorAuth_2_pack: "app/oj/account/twoFactorAuth",
problem_3_pack: "app/oj/problem/problem",
submissionList_4_pack: "app/admin/problem/submissionList",
contestCountdown_5_pack: "app/oj/contest/contestCountdown",
avatar_6_pack: "app/oj/account/avatar",
addProblem_7_pack: "app/admin/problem/addProblem",
problem_8_pack: "app/admin/problem/problem",
contestList_9_pack: "app/admin/contest/contestList",
admin_10_pack: "app/admin/admin",
login_11_pack: "app/oj/account/login",
applyResetPassword_12_pack: "app/oj/account/applyResetPassword",
addContest_13_pack: "app/admin/contest/addContest",
contestPassword_14_pack: "app/oj/contest/contestPassword",
changePassword_15_pack: "app/oj/account/changePassword",
editProblem_17_pack: "app/admin/contest/editProblem",
joinGroupRequestList_18_pack: "app/admin/group/joinGroupRequestList",
group_19_pack: "app/oj/group/group",
contestProblemList_20_pack: "app/admin/contest/contestProblemList",
editProblem_21_pack: "app/admin/problem/editProblem",
register_22_pack: "app/oj/account/register",
groupDetail_23_pack: "app/admin/group/groupDetail",
editContest_24_pack: "app/admin/contest/editContest",
resetPassword_25_pack: "app/oj/account/resetPassword",
group_26_pack: "app/admin/group/group",
settings_27_pack: "app/oj/account/settings"
},
shim: {
avalon: {

View File

@@ -1,9 +1,9 @@
# coding=utf-8
from huey.djhuey import task
from __future__ import absolute_import
from celery import shared_task
from judge_dispatcher.tasks import JudgeDispatcher
@task()
def _judge(submission, time_limit, memory_limit, test_case_id, is_waiting_task=False):
JudgeDispatcher(submission, time_limit, memory_limit, test_case_id).judge(is_waiting_task)
@shared_task
def _judge(submission, time_limit, memory_limit, test_case_id):
JudgeDispatcher(submission, time_limit, memory_limit, test_case_id).judge()

View File

@@ -43,7 +43,7 @@ class SubmissionAPIView(APIView):
problem_id=problem.id)
try:
_judge(submission, problem.time_limit, problem.memory_limit, problem.test_case_id)
_judge.delay(submission, problem.time_limit, problem.memory_limit, problem.test_case_id)
except Exception as e:
logger.error(e)
return error_response(u"提交判题任务失败")
@@ -88,7 +88,7 @@ class ContestSubmissionAPIView(APIView):
code=data["code"],
problem_id=problem.id)
try:
_judge(submission, problem.time_limit, problem.memory_limit, problem.test_case_id)
_judge.delay(submission, problem.time_limit, problem.memory_limit, problem.test_case_id)
except Exception as e:
logger.error(e)
return error_response(u"提交判题任务失败")
@@ -273,7 +273,7 @@ class SubmissionRejudgeAdminAPIView(APIView):
except Problem.DoesNotExist:
return error_response(u"题目不存在")
try:
_judge(submission, problem.time_limit, problem.memory_limit, problem.test_case_id)
_judge.delay(submission, problem.time_limit, problem.memory_limit, problem.test_case_id)
except Exception as e:
logger.error(e)
return error_response(u"提交判题任务失败")

View File

@@ -0,0 +1,131 @@
<div ms-controller="judges" class="col-md-9">
<h1>判题服务器管理</h1>
<table class="table table-striped">
<tr>
<th>编号</th>
<th>名字</th>
<th>最大实例数量</th>
<th>负载</th>
<th>创建时间</th>
<th>状态</th>
<th></th>
</tr>
<tr ms-repeat="judgesList">
<td>{{ el.id }}</td>
<td>{{ el.name }}</td>
<td>{{ el.max_instance_number }}</td>
<td>{{ el.workload }}</td>
<td>{{ el.create_time|date("yyyy-MM-dd HH:mm:ss")}}</td>
<td ms-text="el.status?'启用':'停用'"></td>
<td>
<button class="btn-sm btn-info" ms-click="editJudges(el)">编辑</button>
</td>
</tr>
</table>
<div class="form-group">
<label>仅显示启用 <input ms-duplex-checked="showEnableOnly" type="checkbox"/></label>
</div>
<div class="right">
<ms:pager $id="judgesPager" config="pager"></ms:pager>
</div>
<div ms-visible="isEditing">
<h3>编辑判题服务器</h3>
<form id="edit-judges-form">
<div class="col-md-12">
<div class="col-md-6">
<div class="form-group">
<label>名字</label>
<input name="title" type="text" class="form-control" placeholder="名字" maxlength="30" ms-duplex="name" required data-error="请填写合法的服务器名称">
<div class="help-block with-errors"></div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label>口令</label>
<input name="title" type="text" class="form-control" ms-duplex="token" placeholder="口令" maxlength="30" required data-error="请填写合法的口令">
<div class="help-block with-errors"></div>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label>IP</label>
<input name="ip" type="text" class="form-control" ms-duplex="ipAddress" placeholder="IP" required data-error="请填写合法的IP地址">
<div class="help-block with-errors"></div>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label>端口</label>
<input name="port" type="number" class="form-control" ms-duplex="port" placeholder="端口" required data-error="请填写合法的端口号">
<div class="help-block with-errors"></div>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label>最大实例数量</label>
<input type="number" class="form-control" placeholder="最大实例数量" ms-duplex="max_instance_number" required data-error="请填写合法的最大实例数量">
<div class="help-block with-errors"></div>
</div>
</div>
<div class="form-group">
<label>启用 <input ms-duplex-checked="status" type="checkbox"/></label>
</div>
<div class="form-group">
<button class="btn btn-success">保存修改</button>
&nbsp;&nbsp;
<a ms-click="cancelEdit()" class="btn btn-danger">取消</a>
</div>
</div>
</form>
</div>
<h3>添加判题服务器</h3>
<form id="judges-form">
<div class="form-group">
<div class="col-md-12">
<div class="col-md-6">
<div class="form-group">
<label>名字</label>
<input name="title" type="text" class="form-control" id="name" placeholder="名字" maxlength="30" required data-error="请填写合法的服务器名称">
<div class="help-block with-errors"></div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label>口令</label>
<input name="title" type="text" class="form-control" id="token" placeholder="口令" maxlength="30" required data-error="请填写合法的口令">
<div class="help-block with-errors"></div>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label>IP</label>
<input name="ip" type="text" class="form-control" id="ipAddress" placeholder="IP" required data-error="请填写合法的IP地址">
<div class="help-block with-errors"></div>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label>端口</label>
<input name="port" type="number" class="form-control" id="port" placeholder="端口" required data-error="请填写合法的端口号">
<div class="help-block with-errors"></div>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label>最大实例数量</label>
<input type="number" class="form-control" placeholder="最大实例数量" id="max_instance_number" required data-error="请填写合法的最大实例数量">
<div class="help-block with-errors"></div>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success">添加</button>
</div>
</div>
</form>
</div>
<script src="/static/js/app/admin/judges/judges.js"></script>

View File

@@ -10,6 +10,7 @@
<li class="list-group-header">通用设置</li>
<li class="list-group-item"><a href="/account/settings/">个人信息</a></li>
<li class="list-group-item active"><a href="/account/settings/avatar/">更换头像</a></li>
<li class="list-group-item"><a href="/two_factor_auth/">两步验证</a></li>
<li class="list-group-item"><a href="/change_password/">修改密码</a></li>
</ul>
</div>

View File

@@ -10,6 +10,7 @@
<li class="list-group-header">通用设置</li>
<li class="list-group-item"><a href="/account/settings/">个人信息</a></li>
<li class="list-group-item"><a href="/account/settings/avatar/">更换头像</a></li>
<li class="list-group-item"><a href="/two_factor_auth/">两步验证</a></li>
<li class="list-group-item active"><a href="/change_password/">修改密码</a></li>
</ul>
</div>

View File

@@ -11,22 +11,21 @@
<div class="form-group">
<label for="username">用户名</label>
<input type="text" class="form-control input-lg" id="username" name="username" maxlength="30"
data-error="请填写用户名" placeholder="用户名" autofocus required>
data-error="请填写用户名" placeholder="用户名" autofocus required autocomplete="off">
<div class="help-block with-errors"></div>
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" class="form-control input-lg" id="password" name="password" maxlength="30"
data-error="请填写密码" placeholder="密码" required>
data-error="请填写密码" placeholder="密码" required autocomplete="off">
<div class="help-block with-errors"></div>
</div>
<div class="form-group" id="captcha-area">
<label for="captcha">验证</label>&nbsp;&nbsp;<img src="/captcha/" id="captcha-img"><small>
<p></p></small>
<input type="text" class="form-control input-lg" id="captcha" name="captcha"
placeholder="验证码" maxlength="4" data-error="请填写验证码" required>
<div class="form-group" id="tfa-area">
<label for="captcha">两步验证</label>
<input type="text" class="form-control input-lg" id="tfa-code" name="tfa-code"
placeholder="两步验证验证码" maxlength="6" data-error="请填写两步验证验证码" required autocomplete="off">
<div class="help-block with-errors"></div>
</div>
<div class="form-group">

View File

@@ -10,6 +10,7 @@
<li class="list-group-header">通用设置</li>
<li class="list-group-item active"><a href="/account/settings/">个人信息</a></li>
<li class="list-group-item"><a href="/account/settings/avatar/">更换头像</a></li>
<li class="list-group-item"><a href="/two_factor_auth/">两步验证</a></li>
<li class="list-group-item"><a href="/change_password/">修改密码</a></li>
</ul>
</div>

View File

@@ -0,0 +1,41 @@
{% extends "oj_base.html" %}
{% block title %}
两步验证
{% endblock %}
{% block body %}
<div class="container main">
<div class="col-lg-2">
<ul class="list-group">
<li class="list-group-header">通用设置</li>
<li class="list-group-item"><a href="/account/settings/">个人信息</a></li>
<li class="list-group-item"><a href="/account/settings/avatar/">更换头像</a></li>
<li class="list-group-item active"><a href="/two_factor_auth/">两步验证</a></li>
<li class="list-group-item"><a href="/change_password/">修改密码</a></li>
</ul>
</div>
<div class="col-lg-8">
{% if not request.user.two_factor_auth %}
<h3>扫描二维码开启两步验证</h3>
<img src="/api/two_factor_auth/" id="tfa-qrcode">
<div class="form-inline">
<div class="form-group">
<label for="tfa_code">验证码</label>
<input type="text" maxlength="6" class="form-control" id="tfa_code"
placeholder="输入 app 上显示的验证码">
</div>
<button type="button" class="btn btn-primary" id="tfa_submit">确定</button>
</div>
{% else %}
<div class="alert alert-success" role="alert">两步验证已经开启</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block js_block %}
<script src="/static/js/app/oj/account/twoFactorAuth.js"></script>
{% endblock %}

208
utils/otp_auth.py Normal file
View File

@@ -0,0 +1,208 @@
# -*- coding: utf-8 -*-
"""
otpauth
~~~~~~~
Implements two-step verification of HOTP/TOTP.
:copyright: (c) 2013 - 2015 by Hsiaoming Yang.
:license: BSD, see LICENSE for more details.
"""
import sys
import time
import hmac
import base64
import struct
import hashlib
import warnings
if sys.version_info[0] == 3:
PY2 = False
string_type = str
else:
PY2 = True
string_type = unicode
range = xrange
__author__ = 'Hsiaoming Yang <me@lepture.com>'
__homepage__ = 'https://github.com/lepture/otpauth'
__version__ = '1.0.1'
__all__ = ['OtpAuth', 'HOTP', 'TOTP', 'generate_hotp', 'generate_totp']
HOTP = 'hotp'
TOTP = 'totp'
class OtpAuth(object):
"""One Time Password Authentication.
:param secret: A secret token for the authentication.
"""
def __init__(self, secret):
self.secret = secret
def hotp(self, counter=4):
"""Generate a HOTP code.
:param counter: HOTP is a counter based algorithm.
"""
return generate_hotp(self.secret, counter)
def totp(self, period=30, timestamp=None):
"""Generate a TOTP code.
A TOTP code is an extension of HOTP algorithm.
:param period: A period that a TOTP code is valid in seconds
:param timestamp: Create TOTP at this given timestamp
"""
return generate_totp(self.secret, period, timestamp)
def valid_hotp(self, code, last=0, trials=100):
"""Valid a HOTP code.
:param code: A number that is less than 6 characters.
:param last: Guess HOTP code from last + 1 range.
:param trials: Guest HOTP code end at last + trials + 1.
"""
if not valid_code(code):
return False
code = bytes(int(code))
for i in range(last + 1, last + trials + 1):
if compare_digest(bytes(self.hotp(counter=i)), code):
return i
return False
def valid_totp(self, code, period=30, timestamp=None):
"""Valid a TOTP code.
:param code: A number that is less than 6 characters.
:param period: A period that a TOTP code is valid in seconds
:param timestamp: Validate TOTP at this given timestamp
"""
if not valid_code(code):
return False
return compare_digest(
bytes(self.totp(period, timestamp)),
bytes(int(code))
)
@property
def encoded_secret(self):
secret = base64.b32encode(to_bytes(self.secret))
# bytes to string
secret = secret.decode('utf-8')
# remove pad string
return secret.strip('=')
def to_uri(self, type, label, issuer, counter=None):
"""Generate the otpauth protocal string.
:param type: Algorithm type, hotp or totp.
:param label: Label of the identifier.
:param issuer: The company, the organization or something else.
:param counter: Counter of the HOTP algorithm.
"""
type = type.lower()
if type not in ('hotp', 'totp'):
raise ValueError('type must be hotp or totp')
if type == 'hotp' and not counter:
raise ValueError('HOTP type authentication need counter')
# https://code.google.com/p/google-authenticator/wiki/KeyUriFormat
url = ('otpauth://%(type)s/%(label)s?secret=%(secret)s'
'&issuer=%(issuer)s')
dct = dict(
type=type, label=label, issuer=issuer,
secret=self.encoded_secret, counter=counter
)
ret = url % dct
if type == 'hotp':
ret = '%s&counter=%s' % (ret, counter)
return ret
def to_google(self, type, label, issuer, counter=None):
"""Generate the otpauth protocal string for Google Authenticator.
.. deprecated:: 0.2.0
Use :func:`to_uri` instead.
"""
warnings.warn('deprecated, use to_uri instead', DeprecationWarning)
return self.to_uri(type, label, issuer, counter)
def generate_hotp(secret, counter=4):
"""Generate a HOTP code.
:param secret: A secret token for the authentication.
:param counter: HOTP is a counter based algorithm.
"""
# https://tools.ietf.org/html/rfc4226
msg = struct.pack('>Q', counter)
digest = hmac.new(to_bytes(secret), msg, hashlib.sha1).digest()
ob = digest[19]
if PY2:
ob = ord(ob)
pos = ob & 15
base = struct.unpack('>I', digest[pos:pos + 4])[0] & 0x7fffffff
token = base % 1000000
return token
def generate_totp(secret, period=30, timestamp=None):
"""Generate a TOTP code.
A TOTP code is an extension of HOTP algorithm.
:param secret: A secret token for the authentication.
:param period: A period that a TOTP code is valid in seconds
:param timestamp: Current time stamp.
"""
if timestamp is None:
timestamp = time.time()
counter = int(timestamp) // period
return generate_hotp(secret, counter)
def to_bytes(text):
if isinstance(text, string_type):
# Python3 str -> bytes
# Python2 unicode -> str
text = text.encode('utf-8')
return text
def valid_code(code):
code = string_type(code)
return code.isdigit() and len(code) <= 6
def compare_digest(a, b):
func = getattr(hmac, 'compare_digest', None)
if func:
return func(a, b)
# fallback
if len(a) != len(b):
return False
rv = 0
if PY2:
from itertools import izip
for x, y in izip(a, b):
rv |= ord(x) ^ ord(y)
else:
for x, y in zip(a, b):
rv |= x ^ y
return rv == 0

View File

@@ -2,6 +2,7 @@
import hashlib
import time
import random
import logging
from django.shortcuts import render
from django.core.paginator import Paginator
@@ -9,6 +10,9 @@ from django.core.paginator import Paginator
from rest_framework.response import Response
logger = logging.getLogger("app_info")
def error_page(request, error_reason):
return render(request, "utils/error.html", {"error": error_reason})
@@ -96,7 +100,8 @@ def paginate_data(request, query_set, object_serializer):
def paginate(request, query_set, object_serializer=None):
try:
data= paginate_data(request, query_set, object_serializer)
except Exception:
except Exception as e:
logger.error(str(e))
return error_response(u"参数错误")
return success_response(data)