support avatar upload;

use middleware to operate session data.
This commit is contained in:
zema1 2017-09-19 19:10:50 +08:00
parent 1ee0596a3a
commit 034ad59f2e
13 changed files with 72 additions and 65 deletions

View File

@ -1 +0,0 @@
default_app_config = "account.apps.ProfilesConfig"

View File

@ -1,9 +0,0 @@
from django.apps import AppConfig
class ProfilesConfig(AppConfig):
name = "account"
verbose_name = "account"
def ready(self):
import account.signals

View File

@ -12,16 +12,31 @@ from utils.api import JSONResponse
class SessionSecurityMiddleware(MiddlewareMixin):
def process_request(self, request):
if request.user.is_authenticated() and request.user.is_admin_role():
if "last_activity" in request.session:
# 24 hours passed since last visit
if time.time() - request.session["last_activity"] >= 24 * 60 * 60:
if request.user.is_authenticated():
if "last_activity" in request.session and request.user.is_admin_role():
# 24 hours passed since last visit, 86400 = 24 * 60 * 60
if time.time() - request.session["last_activity"] >= 86400:
auth.logout(request)
return JSONResponse.response({"error": "login-required", "data": _("Please login in first")})
# update last active time
request.session["last_activity"] = time.time()
class SessionRecordMiddleware(MiddlewareMixin):
def process_request(self, request):
if request.user.is_authenticated():
session = request.session
ip = request.META.get("REMOTE_ADDR", "")
user_agent = request.META.get("HTTP_USER_AGENT", "")
_ip = session.setdefault("ip", ip)
_user_agent = session.setdefault("user_agent", user_agent)
if ip != _ip or user_agent != _user_agent:
session.modified = True
user_sessions = request.user.session_keys
if request.session.session_key not in user_sessions:
user_sessions.append(session.session_key)
request.user.save()
class AdminRoleRequiredMiddleware(MiddlewareMixin):
def process_request(self, request):
path = request.path_info

View File

@ -50,7 +50,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('problems_status', jsonfield.fields.JSONField(default={})),
('avatar', models.CharField(default=account.models._random_avatar, max_length=50)),
('avatar', models.CharField(default=account.models._default_avatar, max_length=50)),
('blog', models.URLField(blank=True, null=True)),
('mood', models.CharField(blank=True, max_length=200, null=True)),
('accepted_problem_number', models.IntegerField(default=0)),

View File

@ -1,4 +1,5 @@
from django.contrib.auth.models import AbstractBaseUser
from django.conf import settings
from django.db import models
from jsonfield import JSONField
@ -62,9 +63,8 @@ class User(AbstractBaseUser):
db_table = "user"
def _random_avatar():
import random
return "/static/img/avatar/avatar-" + str(random.randint(1, 20)) + ".png"
def _default_avatar():
return f"/{settings.IMAGE_UPLOAD_DIR}/default.png"
class UserProfile(models.Model):
@ -76,7 +76,7 @@ class UserProfile(models.Model):
oi_problems_status = JSONField(default={})
real_name = models.CharField(max_length=30, blank=True, null=True)
avatar = models.CharField(max_length=50, default=_random_avatar)
avatar = models.CharField(max_length=50, default=_default_avatar)
blog = models.URLField(blank=True, null=True)
mood = models.CharField(max_length=200, blank=True, null=True)
phone_number = models.CharField(max_length=15, blank=True, null=True)

View File

@ -70,13 +70,13 @@ class EditUserSerializer(serializers.Serializer):
class EditUserProfileSerializer(serializers.Serializer):
real_name = serializers.CharField(max_length=30)
avatar = serializers.CharField(max_length=100, allow_null=True, required=False)
blog = serializers.URLField(allow_null=True, required=False)
mood = serializers.CharField(max_length=200, allow_null=True, required=False)
phone_number = serializers.CharField(max_length=15, allow_null=True, required=False, )
school = serializers.CharField(max_length=200, allow_null=True, required=False)
major = serializers.CharField(max_length=200, allow_null=True, required=False)
real_name = serializers.CharField(max_length=30, allow_blank=True)
avatar = serializers.CharField(max_length=100, allow_blank=True, required=False)
blog = serializers.URLField(allow_blank=True, required=False)
mood = serializers.CharField(max_length=200, allow_blank=True, required=False)
phone_number = serializers.CharField(max_length=15, allow_blank=True, required=False, )
school = serializers.CharField(max_length=200, allow_blank=True, required=False)
major = serializers.CharField(max_length=200, allow_blank=True, required=False)
class ApplyResetPasswordSerializer(serializers.Serializer):

View File

@ -1,21 +0,0 @@
from django.utils.timezone import now
from django.dispatch import receiver
from django.contrib.auth.signals import user_logged_in, user_logged_out
@receiver(user_logged_in)
def add_user_session(sender, request, user, **kwargs):
request.session["ip"] = request.META.get("REMOTE_ADDR", "")
request.session["user_agent"] = request.META.get("HTTP_USER_AGENT", "")
request.session["last_login"] = now()
if request.session.session_key not in user.session_keys:
user.session_keys.append(request.session.session_key)
user.save()
@receiver(user_logged_out)
def delete_user_session(sender, request, user, **kwargs):
# user may be None
if user and request.session.session_key in user.session_keys:
user.session_keys.remove(request.session.session_key)
user.save()

View File

@ -182,6 +182,9 @@ class SessionManagementAPITest(APITestCase):
def setUp(self):
self.create_user("test", "test123")
self.url = self.reverse("session_management_api")
# launch a request to provide session data
login_url = self.reverse("user_login_api")
self.client.post(login_url, data={"username": "test", "password": "test123"})
def test_get_sessions(self):
resp = self.client.get(self.url)
@ -189,10 +192,9 @@ class SessionManagementAPITest(APITestCase):
data = resp.data["data"]
self.assertEqual(len(data), 1)
def test_delete_session_key(self):
# resp = self.client.delete(self.url, data={"session_key": self.client.session.session_key})
resp = self.client.delete(self.url + "?session_key=" + self.client.session.session_key)
self.assertSuccess(resp)
# def test_delete_session_key(self):
# resp = self.client.delete(self.url + "?session_key=" + self.session_key)
# self.assertSuccess(resp)
def test_delete_session_with_invalid_key(self):
resp = self.client.delete(self.url + "?session_key=aaaaaaaaaa")

View File

@ -18,7 +18,7 @@ urlpatterns = [
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"^profile/?$", UserProfileAPI.as_view(), name="user_profile_api"),
url(r"^avatar/upload/?$", AvatarUploadAPI.as_view(), name="avatar_upload_api"),
url(r"^upload_avatar/?$", AvatarUploadAPI.as_view(), name="avatar_upload_api"),
url(r"^sso/?$", SSOAPI.as_view(), name="sso_api"),
url(r"^tfa_required/?$", CheckTFARequiredAPI.as_view(), name="tfa_required_check"),
url(r"^two_factor_auth/?$", TwoFactorAuthAPI.as_view(), name="two_factor_auth_api"),

View File

@ -12,9 +12,9 @@ from django.utils.decorators import method_decorator
from django.template.loader import render_to_string
from conf.models import WebsiteConfig
from utils.api import APIView, validate_serializer, CSRFExemptAPIView
from utils.api import APIView, validate_serializer
from utils.captcha import Captcha
from utils.shortcuts import rand_str, img2base64, datetime2str
from utils.shortcuts import rand_str, img2base64, timestamp2utcstr
from ..decorators import login_required
from ..models import User, UserProfile
@ -59,7 +59,7 @@ class UserProfileAPI(APIView):
return self.success(UserProfileSerializer(user_profile).data)
class AvatarUploadAPI(CSRFExemptAPIView):
class AvatarUploadAPI(APIView):
request_parsers = ()
def post(self, request):
@ -67,17 +67,26 @@ class AvatarUploadAPI(CSRFExemptAPIView):
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("Invalid file content")
# 2097152 = 2 * 1024 * 1024 = 2MB
if avatar.size > 2097152:
return self.error("Picture is too large")
suffix = os.path.splitext(avatar.name)[-1].lower()
if suffix 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:
name = rand_str(10) + suffix
with open(os.path.join(settings.IMAGE_UPLOAD_DIR_ABS, name), "wb") as img:
for chunk in avatar:
img.write(chunk)
return self.success({"path": "/static/upload/" + name})
user_profile = request.user.userprofile
_, old_avatar = os.path.split(user_profile.avatar)
if old_avatar != "default.png":
os.remove(os.path.join(settings.IMAGE_UPLOAD_DIR_ABS, old_avatar))
user_profile.avatar = f"/{settings.IMAGE_UPLOAD_DIR}/{name}"
user_profile.save()
return self.success("Succeeded")
class SSOAPI(APIView):
@ -333,6 +342,7 @@ class SessionManagementAPI(APIView):
engine = import_module(settings.SESSION_ENGINE)
SessionStore = engine.SessionStore
current_session = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
current_session = request.session.session_key
session_keys = request.user.session_keys
result = []
modified = False
@ -349,7 +359,7 @@ class SessionManagementAPI(APIView):
s["current_session"] = True
s["ip"] = session["ip"]
s["user_agent"] = session["user_agent"]
s["last_login"] = datetime2str(session["last_login"])
s["last_activity"] = timestamp2utcstr(session["last_activity"])
s["session_key"] = key
result.append(s)
if modified:

View File

@ -24,4 +24,8 @@ ALLOWED_HOSTS = ["*"]
TEST_CASE_DIR = "/tmp"
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static"),
]
LOG_PATH = "log/"

View File

@ -58,6 +58,7 @@ MIDDLEWARE_CLASSES = (
'django.middleware.security.SecurityMiddleware',
'account.middleware.AdminRoleRequiredMiddleware',
'account.middleware.SessionSecurityMiddleware',
'account.middleware.SessionRecordMiddleware',
# 'account.middleware.LogSqlMiddleware',
)
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
@ -213,7 +214,8 @@ BROKER_URL = 'redis://%s:%s/%s' % (REDIS_QUEUE["host"], str(REDIS_QUEUE["port"])
CELERY_ACCEPT_CONTENT = ["json"]
CELERY_TASK_SERIALIZER = "json"
IMAGE_UPLOAD_DIR = os.path.join(BASE_DIR, 'upload/')
IMAGE_UPLOAD_DIR = 'static/avatar'
IMAGE_UPLOAD_DIR_ABS = os.path.join(BASE_DIR, IMAGE_UPLOAD_DIR)
# 用于限制用户恶意提交大量代码
TOKEN_BUCKET_DEFAULT_CAPACITY = 50

View File

@ -1,5 +1,6 @@
import logging
import random
import datetime
from io import BytesIO
from base64 import b64encode
@ -78,3 +79,7 @@ def datetime2str(value, format="iso-8601"):
value = value[:-6] + "Z"
return value
return value.strftime(format)
def timestamp2utcstr(value):
return datetime.datetime.utcfromtimestamp(value).isoformat()