ignalxy 4 éve
szülő
commit
273bf9c105

+ 12 - 0
.idea/dataSources.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="DataSourceManagerImpl" format="xml" multifile-model="true">
+    <data-source source="LOCAL" name="db" uuid="1407b81f-6eb8-4a13-bf4d-8e18ebbdb340">
+      <driver-ref>sqlite.xerial</driver-ref>
+      <synchronize>true</synchronize>
+      <jdbc-driver>org.sqlite.JDBC</jdbc-driver>
+      <jdbc-url>jdbc:sqlite:C:\Users\k\PycharmProjects\st-cloud\db.sqlite3</jdbc-url>
+      <working-dir>$ProjectFileDir$</working-dir>
+    </data-source>
+  </component>
+</project>

+ 1 - 1
.idea/misc.xml

@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
-  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7 (hy_env)" project-jdk-type="Python SDK" />
+  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9" project-jdk-type="Python SDK" />
 </project>

+ 15 - 0
_user/admin.py

@@ -0,0 +1,15 @@
+from django.contrib import admin
+from .models import User, LoginToken
+
+
+# Register your models here.
+class UserAdmin(admin.ModelAdmin):
+    list_display = ["username", "password", "email"]
+
+
+class LoginTokenAdmin(admin.ModelAdmin):
+    list_display = ["_user", "token"]
+
+
+admin.site.register(User, UserAdmin)
+admin.site.register(LoginToken, LoginTokenAdmin)

+ 5 - 0
_user/apps.py

@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class UserConfig(AppConfig):
+    name = 'user'

+ 45 - 0
_user/decorators.py

@@ -0,0 +1,45 @@
+from functools import wraps
+from urllib.parse import urlparse
+
+from django.conf import settings
+from django.shortcuts import resolve_url
+from django.http import JsonResponse
+
+from .models import User, LoginToken
+
+
+def user_passes_test(test_func, error):
+    def decorator(view_func):
+        @wraps(view_func)
+        def _wrapped_view(request, *args, **kwargs):
+            if test_func(request):
+                return view_func(request, *args, **kwargs)
+            return JsonResponse({'code': 401, 'error': error}, status=401)
+
+        return _wrapped_view
+
+    return decorator
+
+
+def login_required(function=None, error='error'):
+    """
+    Decorator for views that checks that the _user is logged in, redirecting
+    to the log-in page if necessary.
+    """
+
+    def is_login(request):
+        username = request.data.get('username', '')
+        token = request.data.get('token', '')
+        try:
+            user = User.objects.get(username='username')
+            if user.check_token(token):
+                user.tokens.get(token=token)
+                return True
+        except:
+            return False
+        return False
+
+    actual_decorator = user_passes_test(is_login, '请登录')
+    if function:
+        return actual_decorator(function)
+    return actual_decorator

+ 86 - 0
_user/models.py

@@ -0,0 +1,86 @@
+import unicodedata
+
+from datetime import datetime, time
+from django.core.mail import send_mail
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+from django.utils.http import base36_to_int, int_to_base36
+from django.conf import settings
+from django.utils.crypto import constant_time_compare, salted_hmac
+from .validators import ASCIIUsernameValidator
+
+
+class User(models.Model):
+    username = models.CharField(
+        _('username'),
+        max_length=25,
+        unique=True,
+        help_text=_('Required. 25 characters or fewer. Letters, digits and _ only.'),
+        validators=[ASCIIUsernameValidator()],
+        error_messages={
+            'unique': _("A _user with that username already exists."),
+        },
+    )
+    password = models.CharField(_('password'), max_length=128)
+    last_login = models.DateTimeField(_('last login'), blank=True, null=True)
+    email = models.EmailField(_('email address'), unique=True)
+
+    class Meta:
+        db_table = '_user'
+        verbose_name = verbose_name_plural = '用户信息表'
+
+    def set_password(self, password):
+        # TODO: 密码强度检验,密码hash存储
+        self.password = password
+
+    def send_email(self, subject, message, from_email=None, **kwargs):
+        send_mail(subject, message, from_email, [self.email], **kwargs)
+
+    def make_token(self):
+        return self._make_token(_timestamp())
+
+    def check_token(self, token):
+        if not token:
+            return False
+        try:
+            ts_b36, hash_str = token.split('-')
+        except ValueError:
+            return False
+
+        try:
+            ts = base36_to_int(ts_b36)
+        except ValueError:
+            return False
+
+        if self._make_token(ts) != token:
+            return False
+
+        timestamp = _timestamp()
+        if (timestamp - ts) > settings.PASSWORD_RESET_TIMEOUT:
+            return False
+
+        return True
+
+    def _make_token(self, timestamp):
+        ts_b36 = int_to_base36(timestamp)
+        salt = settings.SALT
+        value = self._make_hash_value(timestamp)
+        secret = settings.SECRET_KEY
+        hash_str = salted_hmac(
+            salt, value, secret=secret, algorithm='sha256'
+        ).hexdigest()[::2]
+        token = "%s-%s" % (ts_b36, hash_str)
+        return token
+
+    def _make_hash_value(self, timestamp):
+        return f'{self.pk}{self.password}{timestamp}{self.email}'
+
+
+def _timestamp():
+    dt = datetime.now()
+    return int((dt - datetime(2001, 1, 1)).total_seconds())
+
+
+class LoginToken(models.Model):
+    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='tokens')
+    token = models.CharField(max_length=256)

+ 203 - 0
_user/password_validation.py

@@ -0,0 +1,203 @@
+import functools
+import gzip
+import re
+from difflib import SequenceMatcher
+from pathlib import Path
+
+from django.conf import settings
+from django.core.exceptions import (
+    FieldDoesNotExist, ImproperlyConfigured, ValidationError,
+)
+from django.utils.functional import lazy
+from django.utils.html import format_html, format_html_join
+from django.utils.module_loading import import_string
+from django.utils.translation import gettext as _, ngettext
+
+
+@functools.lru_cache(maxsize=None)
+def get_default_password_validators():
+    return get_password_validators(settings.AUTH_PASSWORD_VALIDATORS)
+
+
+def get_password_validators(validator_config):
+    validators = []
+    for validator in validator_config:
+        try:
+            klass = import_string(validator['NAME'])
+        except ImportError:
+            msg = "The module in NAME could not be imported: %s. Check your AUTH_PASSWORD_VALIDATORS setting."
+            raise ImproperlyConfigured(msg % validator['NAME'])
+        validators.append(klass(**validator.get('OPTIONS', {})))
+
+    return validators
+
+
+def validate_password(password, user=None, password_validators=None):
+    """
+    Validate whether the password meets all validator requirements.
+
+    If the password is valid, return ``None``.
+    If the password is invalid, raise ValidationError with all error messages.
+    """
+    errors = []
+    if password_validators is None:
+        password_validators = get_default_password_validators()
+    for validator in password_validators:
+        try:
+            validator.validate(password, user)
+        except ValidationError as error:
+            errors.append(error)
+    if errors:
+        raise ValidationError(errors)
+
+
+def password_changed(password, user=None, password_validators=None):
+    """
+    Inform all validators that have implemented a password_changed() method
+    that the password has been changed.
+    """
+    if password_validators is None:
+        password_validators = get_default_password_validators()
+    for validator in password_validators:
+        password_changed = getattr(validator, 'password_changed', lambda *a: None)
+        password_changed(password, user)
+
+
+def password_validators_help_texts(password_validators=None):
+    """
+    Return a list of all help texts of all configured validators.
+    """
+    help_texts = []
+    if password_validators is None:
+        password_validators = get_default_password_validators()
+    for validator in password_validators:
+        help_texts.append(validator.get_help_text())
+    return help_texts
+
+
+def _password_validators_help_text_html(password_validators=None):
+    """
+    Return an HTML string with all help texts of all configured validators
+    in an <ul>.
+    """
+    help_texts = password_validators_help_texts(password_validators)
+    help_items = format_html_join('', '<li>{}</li>', ((help_text,) for help_text in help_texts))
+    return format_html('<ul>{}</ul>', help_items) if help_items else ''
+
+
+password_validators_help_text_html = lazy(_password_validators_help_text_html, str)
+
+
+class MinimumLengthValidator:
+    """
+    Validate whether the password is of a minimum length.
+    """
+    def __init__(self, min_length=8):
+        self.min_length = min_length
+
+    def validate(self, password, user=None):
+        if len(password) < self.min_length:
+            raise ValidationError(
+                ngettext(
+                    "This password is too short. It must contain at least %(min_length)d character.",
+                    "This password is too short. It must contain at least %(min_length)d characters.",
+                    self.min_length
+                ),
+                code='password_too_short',
+                params={'min_length': self.min_length},
+            )
+
+    def get_help_text(self):
+        return ngettext(
+            "Your password must contain at least %(min_length)d character.",
+            "Your password must contain at least %(min_length)d characters.",
+            self.min_length
+        ) % {'min_length': self.min_length}
+
+
+class UserAttributeSimilarityValidator:
+    """
+    Validate whether the password is sufficiently different from the _user's
+    attributes.
+
+    If no specific attributes are provided, look at a sensible list of
+    defaults. Attributes that don't exist are ignored. Comparison is made to
+    not only the full attribute value, but also its components, so that, for
+    example, a password is validated against either part of an email address,
+    as well as the full address.
+    """
+    DEFAULT_USER_ATTRIBUTES = ('username', 'first_name', 'last_name', 'email')
+
+    def __init__(self, user_attributes=DEFAULT_USER_ATTRIBUTES, max_similarity=0.7):
+        self.user_attributes = user_attributes
+        self.max_similarity = max_similarity
+
+    def validate(self, password, user=None):
+        if not user:
+            return
+
+        for attribute_name in self.user_attributes:
+            value = getattr(user, attribute_name, None)
+            if not value or not isinstance(value, str):
+                continue
+            value_parts = re.split(r'\W+', value) + [value]
+            for value_part in value_parts:
+                if SequenceMatcher(a=password.lower(), b=value_part.lower()).quick_ratio() >= self.max_similarity:
+                    try:
+                        verbose_name = str(user._meta.get_field(attribute_name).verbose_name)
+                    except FieldDoesNotExist:
+                        verbose_name = attribute_name
+                    raise ValidationError(
+                        _("The password is too similar to the %(verbose_name)s."),
+                        code='password_too_similar',
+                        params={'verbose_name': verbose_name},
+                    )
+
+    def get_help_text(self):
+        return _('Your password can’t be too similar to your other personal information.')
+
+
+class CommonPasswordValidator:
+    """
+    Validate whether the password is a common password.
+
+    The password is rejected if it occurs in a provided list of passwords,
+    which may be gzipped. The list Django ships with contains 20000 common
+    passwords (lowercased and deduplicated), created by Royce Williams:
+    https://gist.github.com/roycewilliams/281ce539915a947a23db17137d91aeb7
+    The password list must be lowercased to match the comparison in validate().
+    """
+    DEFAULT_PASSWORD_LIST_PATH = Path(__file__).resolve().parent / 'common-passwords.txt.gz'
+
+    def __init__(self, password_list_path=DEFAULT_PASSWORD_LIST_PATH):
+        try:
+            with gzip.open(password_list_path, 'rt', encoding='utf-8') as f:
+                self.passwords = {x.strip() for x in f}
+        except OSError:
+            with open(password_list_path) as f:
+                self.passwords = {x.strip() for x in f}
+
+    def validate(self, password, user=None):
+        if password.lower().strip() in self.passwords:
+            raise ValidationError(
+                _("This password is too common."),
+                code='password_too_common',
+            )
+
+    def get_help_text(self):
+        return _('Your password can’t be a commonly used password.')
+
+
+class NumericPasswordValidator:
+    """
+    Validate whether the password is alphanumeric.
+    """
+    def validate(self, password, user=None):
+        if password.isdigit():
+            raise ValidationError(
+                _("This password is entirely numeric."),
+                code='password_entirely_numeric',
+            )
+
+    def get_help_text(self):
+        return _('Your password can’t be entirely numeric.')

+ 8 - 0
_user/serializers.py

@@ -0,0 +1,8 @@
+from rest_framework import serializers
+from .models import User, LoginToken
+
+
+class UserSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = User
+        fields = ('username', 'password', 'email')

+ 14 - 0
_user/urls.py

@@ -0,0 +1,14 @@
+# The views used below are normally mapped in the AdminSite instance.
+# This URLs file is used to provide a reliable view deployment for test purposes.
+# It is also provided as a convenience to those who want to deploy these URLs
+# elsewhere.
+
+from django.conf.urls import url
+from django.urls import path
+from . import views
+from rest_framework.urlpatterns import format_suffix_patterns
+
+urlpatterns = [
+]
+
+urlpatterns = format_suffix_patterns(urlpatterns)

+ 25 - 0
_user/validators.py

@@ -0,0 +1,25 @@
+import re
+
+from django.core import validators
+from django.utils.deconstruct import deconstructible
+from django.utils.translation import gettext_lazy as _
+
+
+@deconstructible
+class ASCIIUsernameValidator(validators.RegexValidator):
+    regex = r'^[\w]+\Z'
+    message = _(
+        'Enter a valid username. This value may contain only English letters, '
+        'numbers, and _ characters.'
+    )
+    flags = re.ASCII
+
+
+@deconstructible
+class UnicodeUsernameValidator(validators.RegexValidator):
+    regex = r'^[\w]+\Z'
+    message = _(
+        'Enter a valid username. This value may contain only letters, '
+        'numbers, and _ characters.'
+    )
+    flags = 0

+ 123 - 0
_user/views.py

@@ -0,0 +1,123 @@
+from django.shortcuts import render
+
+# Create your views here.
+
+from datetime import datetime, time
+from .models import User, LoginToken
+from django.http import JsonResponse, HttpResponse
+from rest_framework.decorators import api_view
+from django.middleware.csrf import rotate_token
+
+
+def auth_with_username_or_email(username, password):
+    if '@' in username:
+        user = User.objects.get(email=username, password=password)
+    else:
+        user = User.objects.get(username=username, password=password)
+    return user
+
+
+@api_view(['POST'])
+def register(request):
+    username = request.data.get('username', '')
+    password = request.data.get('password', '')
+    email = request.data.get('email', '')
+
+    try:
+        User.objects.create(username=username, password=password, email=email)
+        print('注册成功')
+        return JsonResponse({'code': 200})
+    except Exception as e:
+        print(e)
+        return JsonResponse({'code': 303, 'error': str(e)}, status=303)
+
+
+@api_view(['POST'])
+def login(request):
+    username = request.data.get('username', '')
+    password = request.data.get('password', '')
+    token = request.data.get('token', '')
+
+    try:
+        user = auth_with_username_or_email(username, password)
+        print(user)
+    except Exception as e:
+        print(e)
+        print('用户名或密码错误')
+        return JsonResponse({'code': 303, 'error': '用户名或密码错误'}, status=303)
+
+    print(f'token = {token}')
+    if user.check_token(token):
+        try:
+            user_token = user.tokens.get(token=token)
+            print('已登录')
+            user_token.delete()
+            # return JsonResponse({'code': 303, 'msg': '已登录'}, status=303)
+        except Exception as e:
+            print('token无效')
+    else:
+        print('token已过期')
+
+    user.last_login = datetime.now()
+
+    new_token = user.make_token()
+    user_token = LoginToken()
+    user_token.user = user
+    user_token.token = new_token
+    user_token.save()
+
+    if hasattr(request, '_user'):
+        print('设置reqeust._user')
+        request.user = user
+
+    print('登录成功')
+    print(f'new_token = {new_token}')
+    return JsonResponse({'code': 200, 'token': new_token})
+
+
+@api_view(['POST'])
+def logout(request):
+    username = request.data.get('username', '')
+    token = request.data.get('token', '')
+    try:
+        user = User.objects.get(username=username)
+        try:
+            user_token = user.tokens.get(token=token)
+            user_token.delete()
+        except Exception as e:
+            print(e)
+            print('token无效')
+        return JsonResponse({'code': 200})
+    except Exception as e:
+        print(e)
+        return JsonResponse({'code': 303, 'error': str(e)}, status=303)
+
+
+@api_view(['POST'])
+def reset_password(request):
+    username = request.data.get('username', '')
+    password = request.data.get('password', '')
+    try:
+        user = User.objects.get(username=username)
+        token = request.data.get('token')
+        if token:
+            print(f'token={token}')
+            if user.check_token(token):
+                # 重置密码
+                print("验证码有效")
+                user.password = password
+                user.save()
+                return JsonResponse({'code': 200})
+            else:
+                print("验证码无效")
+                return JsonResponse({'code': 303, 'error': '验证码错误'}, status=303)
+        else:
+            # 发送验证码
+            token = user.make_token()
+            print(f'')
+            print(f'发送验证码 email = {user.email} token = {token}')
+            user.send_email('ST网盘重置密码验证码', token)
+            return JsonResponse({'code': 200})
+    except Exception as e:
+        print(e)
+        return JsonResponse({'code': 303, 'error': str(e)}, status=303)

+ 10 - 5
account/admin.py

@@ -1,15 +1,20 @@
 from django.contrib import admin
-from .models import Profile, Devices
+from .models import Profile, User, LoginToken
 
 
 # Register your models here.
 class ProfileAdmin(admin.ModelAdmin):
-    list_display = ["user", "email", "root_folder_id"]
+    list_display = ["user", "root_folder_id"]
 
 
-class DevicesAdmin(admin.ModelAdmin):
-    list_display = ["user", "last_login_time", "device_uid", "token"]
+class UserAdmin(admin.ModelAdmin):
+    list_display = ["username", "password", "email"]
+
+
+class LoginTokenAdmin(admin.ModelAdmin):
+    list_display = ["user", "token"]
 
 
 admin.site.register(Profile, ProfileAdmin)
-admin.site.register(Devices, DevicesAdmin)
+admin.site.register(User, UserAdmin)
+admin.site.register(LoginToken, LoginTokenAdmin)

+ 42 - 0
account/decorators.py

@@ -0,0 +1,42 @@
+from functools import wraps
+from urllib.parse import urlparse
+
+from django.conf import settings
+from django.shortcuts import resolve_url
+from django.http import JsonResponse
+
+from .models import User, LoginToken
+
+
+def user_passes_test(test_func, error):
+    def decorator(view_func):
+        @wraps(view_func)
+        def _wrapped_view(request, *args, **kwargs):
+            if test_func(request):
+                return view_func(request, *args, **kwargs)
+            return JsonResponse({'code': 401, 'error': error}, status=401)
+        return _wrapped_view
+    return decorator
+
+
+def login_required(function=None, error='error'):
+    """
+    Decorator for views that checks that the _user is logged in, redirecting
+    to the log-in page if necessary.
+    """
+    def is_login(request):
+        username = request.data.get('username', '')
+        token = request.data.get('token', '')
+        try:
+            user = User.objects.get(username='username')
+            if user.check_token(token):
+                user.tokens.get(token=token)
+                return True
+        except:
+            return False
+        return False
+
+    actual_decorator = user_passes_test(is_login, '请登录')
+    if function:
+        return actual_decorator(function)
+    return actual_decorator

+ 1 - 1
account/form.py

@@ -1,5 +1,5 @@
 from django import forms
-from django.contrib.auth.models import User
+from user.models import User
 from .models import Profile
 
 

+ 0 - 37
account/migrations/0001_initial.py

@@ -1,37 +0,0 @@
-# Generated by Django 3.2.7 on 2021-09-08 15:01
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-    initial = True
-
-    dependencies = [
-        ('folder', '0001_initial'),
-        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='Profile',
-            fields=[
-                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('email', models.CharField(blank=True, max_length=20)),
-                ('root_folder', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='root_folder', to='folder.folder')),
-                ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
-            ],
-        ),
-        migrations.CreateModel(
-            name='Devices',
-            fields=[
-                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('last_login_time', models.DateField(auto_now=True)),
-                ('device_uid', models.CharField(default='233', max_length=100, unique=True)),
-                ('token', models.CharField(default='233', max_length=100, unique=True)),
-                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='devices', to=settings.AUTH_USER_MODEL)),
-            ],
-        ),
-    ]

+ 0 - 27
account/migrations/0002_auto_20210908_2303.py

@@ -1,27 +0,0 @@
-# Generated by Django 3.2.7 on 2021-09-08 15:03
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('folder', '0001_initial'),
-        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
-        ('account', '0001_initial'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='profile',
-            name='root_folder',
-            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='root_folder', to='folder.folder'),
-        ),
-        migrations.AlterField(
-            model_name='profile',
-            name='user',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL),
-        ),
-    ]

+ 0 - 21
account/migrations/0003_alter_profile_user.py

@@ -1,21 +0,0 @@
-# Generated by Django 3.2.7 on 2021-09-08 15:06
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
-        ('account', '0002_auto_20210908_2303'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='profile',
-            name='user',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
-        ),
-    ]

+ 0 - 27
account/migrations/0004_auto_20210908_2313.py

@@ -1,27 +0,0 @@
-# Generated by Django 3.2.7 on 2021-09-08 15:13
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('folder', '0001_initial'),
-        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
-        ('account', '0003_alter_profile_user'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='profile',
-            name='root_folder',
-            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='profile', to='folder.folder'),
-        ),
-        migrations.AlterField(
-            model_name='profile',
-            name='user',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL),
-        ),
-    ]

+ 0 - 0
account/migrations/__init__.py


+ 100 - 27
account/models.py

@@ -1,48 +1,110 @@
-import hashlib
-import datetime
-
+from datetime import datetime, time
+from django.core.mail import send_mail
 from django.db import models
+from django.utils.translation import gettext_lazy as _
+from django.utils.http import base36_to_int, int_to_base36
+from django.conf import settings
+from django.utils.crypto import constant_time_compare, salted_hmac
+from .validators import ASCIIUsernameValidator
+
+
+class User(models.Model):
+    username = models.CharField(
+        _('username'),
+        max_length=25,
+        unique=True,
+        help_text=_('Required. 25 characters or fewer. Letters, digits and _ only.'),
+        validators=[ASCIIUsernameValidator()],
+        error_messages={
+            'unique': _("A _user with that username already exists."),
+        },
+    )
+    password = models.CharField(_('password'), max_length=128)
+    last_login = models.DateTimeField(_('last login'), blank=True, null=True)
+    email = models.EmailField(_('email address'), unique=True)
+
+    class Meta:
+        db_table = '_user'
+        verbose_name = verbose_name_plural = '用户信息表'
+
+    def set_password(self, password):
+        # TODO: 密码强度检验,密码hash存储
+        self.password = password
+
+    def send_email(self, subject, message, from_email=None, **kwargs):
+        send_mail(subject, message, from_email, [self.email], **kwargs)
+
+    def make_token(self):
+        return self._make_token(_timestamp())
+
+    def check_token(self, token):
+        if not token:
+            return False
+        try:
+            ts_b36, hash_str = token.split('-')
+        except ValueError:
+            return False
+
+        try:
+            ts = base36_to_int(ts_b36)
+        except ValueError:
+            return False
+
+        if self._make_token(ts) != token:
+            return False
+
+        timestamp = _timestamp()
+        if (timestamp - ts) > settings.PASSWORD_RESET_TIMEOUT:
+            return False
+
+        return True
+
+    def _make_token(self, timestamp):
+        ts_b36 = int_to_base36(timestamp)
+        salt = settings.SALT
+        value = self._make_hash_value(timestamp)
+        secret = settings.SECRET_KEY
+        hash_str = salted_hmac(
+            salt, value, secret=secret, algorithm='sha256'
+        ).hexdigest()[::2]
+        token = "%s-%s" % (ts_b36, hash_str)
+        return token
+
+    def _make_hash_value(self, timestamp):
+        return f'{self.pk}{self.password}{timestamp}{self.email}'
+
+
+def _timestamp():
+    dt = datetime.now()
+    return int((dt - datetime(2001, 1, 1)).total_seconds())
+
+
+class LoginToken(models.Model):
+    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='tokens')
+    token = models.CharField(max_length=256)
+
+
 from folder.models import Folder
-from django.contrib.auth.models import User
 # 引入内置信号
 from django.db.models.signals import post_save
 # 引入信号接收器的装饰器
 from django.dispatch import receiver
 
 
-# Create your views here.
 class Profile(models.Model):
     # 对应django自带的user
-    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='profile')
-    # 邮箱
-    email = models.CharField(max_length=20, blank=True)
+    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
     # 对应的根目录
     root_folder = models.ForeignKey(Folder, null=True, on_delete=models.DO_NOTHING, related_name='profile')
 
     def __str__(self):
-        return 'user {}'.format(self.user.username)
-
-
-class Devices(models.Model):
-    # 对应django自带的User
-    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='devices')
-    # 设备登录时间
-    last_login_time = models.DateField(auto_now=True)
-    # 设备码
-    device_uid = models.CharField(max_length=100, default='233', blank=False, unique=True)
-    # 登录凭证
-    token = models.CharField(max_length=100, default='233', blank=False, unique=True)
-
-    def gen_token(self):
-        _token = self.device_uid + self.user.username + datetime.datetime.now().strftime("%Y%m%d%H%M%S")
-        sha1 = hashlib.sha1()
-        sha1.update(_token.encode())
-        self.token = sha1.hexdigest()
+        return '_user {}'.format(self.user.username)
 
 
 # 信号接收函数,每当新建User实例的时候自动调用
 @receiver(post_save, sender=User)
 def create_user_profile(sender, instance, created, **kwargs):
+    # pass
     if created:
         Profile.objects.create(user=instance)
 
@@ -50,4 +112,15 @@ def create_user_profile(sender, instance, created, **kwargs):
 # 信号接收函数,每当更新User实例的时候自动调用
 @receiver(post_save, sender=User)
 def save_user_profile(sender, instance, **kwargs):
-    instance.profile.save()
+    pass
+    # instance.profile.save()
+
+
+# 信号接收函数,每当新建Profile实例的时候自动调用
+@receiver(post_save, sender=Profile)
+def create_root_folder(sender, instance, created, **kwargs):
+    # pass
+    if created:
+        root_folder = Folder.objects.create()
+        instance.root_folder = root_folder
+        instance.save()

+ 8 - 0
account/serializers.py

@@ -0,0 +1,8 @@
+from rest_framework import serializers
+from .models import User, LoginToken
+
+
+class UserSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = User
+        fields = ('username', 'password', 'email')

+ 5 - 3
account/urls.py

@@ -3,7 +3,9 @@ from django.urls import path
 from . import views
 # Create your views here.
 urlpatterns = [
-    path('login/', views.user_login, name='login'),
-    path('logout/', views.user_logout, name='logout'),
-    path('register/', views.user_register, name='register'),
+    # path('register/', views.user_register, name='register'),
+    path('login/', views.login, name='login'),
+    path('logout/', views.logout, name='logout'),
+    path('register/', views.register, name='register'),
+    path('reset_password/', views.reset_password, name='reset_password')
 ]

+ 25 - 0
account/validators.py

@@ -0,0 +1,25 @@
+import re
+
+from django.core import validators
+from django.utils.deconstruct import deconstructible
+from django.utils.translation import gettext_lazy as _
+
+
+@deconstructible
+class ASCIIUsernameValidator(validators.RegexValidator):
+    regex = r'^[\w]+\Z'
+    message = _(
+        'Enter a valid username. This value may contain only English letters, '
+        'numbers, and _ characters.'
+    )
+    flags = re.ASCII
+
+
+@deconstructible
+class UnicodeUsernameValidator(validators.RegexValidator):
+    regex = r'^[\w]+\Z'
+    message = _(
+        'Enter a valid username. This value may contain only letters, '
+        'numbers, and _ characters.'
+    )
+    flags = 0

+ 117 - 135
account/views.py

@@ -1,140 +1,122 @@
-import string
-
 from django.shortcuts import render
 
-from folder.models import Folder
-from .form import UserLoginForm, UserRegisterForm
-from django.contrib.auth import authenticate, login, logout
-from django.http import HttpResponse
-from .models import Devices, Profile
-from django.contrib.auth.models import User
-import random
-import datetime
-import json
-
-"""
-在此处修改token过期时间,30代表30天过期
-"""
-expiration_date = 30
-DEBUG = True
-
-
-def user_login(request):
-    if request.method == 'POST':
-        data = request.POST
-        # 检测是否有登录凭证
-        if data['token'] != 'token':
-            try:
-                device = Devices.objects.filter(token__exact=data['token']).get()
-                sub_time = (device.last_login_time - datetime.date.today()).total_seconds() / (3600 * 24)
-                # 检查token是否过期
-                if sub_time < expiration_date:
-                    # 更新此user此设备的token
-                    # token由设备uid、用户名、当前时间hash得来
-                    device.gen_token()
-                    device.save()
-                    # 登录
-                    user = device.user
-                    login(request, user)
-                    response = {'token': device.token}
-                    return HttpResponse(json.dumps(response), status=200)
-                    # return redirect("chat:index")
-                else:
-                    return HttpResponse(status=420)
-            # 已过期的token并且已经被删除
-            except Devices.DoesNotExist:
-                return HttpResponse(status=420)
-        else:
-            # 检测账号密码是否匹配数据库中的一个用户
-            # 如果均匹配,则返回此User对象
-            user = authenticate(username=data['username'], password=data['password'])
-            if user:
-                if data['token'] == 'token':
-                    # 新建一个该user的设备
-                    device = create_new_device(user)
-                else:
-                    # 此时,客户端带来了过期的旧token,现在需要更新旧的token
-                    try:
-                        device = Devices.objects.filter(token__exact=data['token']).get()
-                        device.gen_token()
-                        device.save()
-                    except Devices.DoesNotExist:
-                        # 新建一个该user的设备
-                        device = create_new_device(user)
-                login(request, user)
-                response = {'token': device.token}
-                return HttpResponse(json.dumps(response), status=200)
-            else:
-                return HttpResponse(status=401)
-    # 用于测试,登录界面
-    elif request.method == 'GET':
-        if DEBUG:
-            user_login_form = UserLoginForm()
-            context = {'form': user_login_form}
-            return render(request, 'login.html', context)
-    else:
-        # 请求方法错误,请使用POST
-        return HttpResponse(status=400)
-
-
-# 新建一个该user的设备
-def create_new_device(user):
-    device = Devices()
-    device.user = user
-    device.device_uid = generate_random_str(100)
-    device.gen_token()
-    device.save()
-    return device
-
-
-def generate_random_str(random_length=16):
-    """
-    生成一个指定长度的随机字符串
-    """
-    random_str = ''
-    base_str = 'ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789'
-    length = len(base_str) - 1
-    for i in range(random_length):
-        random_str += base_str[random.randint(0, length)]
-    return random_str
-
-
-def user_logout(request):
-    logout(request)
-    return HttpResponse(status=200)
-
-
-def user_register(request):
-    if request.method == 'POST':
-        user_register_form = UserRegisterForm(data=request.POST)
-        if user_register_form.is_valid():
-            # 新建一个user,但是不提交
-            new_user = user_register_form.save(commit=False)
-            # 设置密码
-            new_user.set_password(user_register_form.cleaned_data['password'])
-            # 保存
-            new_user.save()
-
-            user_id_random = ''.join(random.sample(string.ascii_letters + string.digits, 8))
-            folder_1 = Folder.objects.create(folder_id=user_id_random,
-                                             folder_name=request.user.username,
-                                             father_folder=None)
-            folder_1.save()
-            user_1 = Profile.objects.create(user=Profile.user.username,
-                                            email=Profile.email,
-                                            root_folder=folder_1)
-            user_1.save()
-            return HttpResponse(status=200)
-        else:
-            return HttpResponse(status=400)
-    # 用于测试
-    elif request.method == 'GET':
-        if DEBUG:
-            user_register_form = UserRegisterForm()
-            context = {'form': user_register_form}
-            return render(request, 'register.html', context)
-    else:
-        return HttpResponse(status=400)
+# Create your views here.
 
+from datetime import datetime, time
+from .models import User, LoginToken
+from django.http import JsonResponse, HttpResponse
+from rest_framework.decorators import api_view
 
 
+def auth_with_username_or_email(username, password):
+    if '@' in username:
+        user = User.objects.get(email=username, password=password)
+    else:
+        user = User.objects.get(username=username, password=password)
+    return user
+
+
+@api_view(['POST'])
+def register(request):
+    username = request.data.get('username', '')
+    password = request.data.get('password', '')
+    email = request.data.get('email', '')
+
+    try:
+        User.objects.create(username=username, password=password, email=email)
+        print('注册成功')
+        return JsonResponse({'code': 200})
+    except Exception as e:
+        print(e)
+        return JsonResponse({'code': 303, 'error': str(e)}, status=303)
+
+
+@api_view(['POST'])
+def login(request):
+    username = request.data.get('username', '')
+    password = request.data.get('password', '')
+    token = request.data.get('token', '')
+
+    try:
+        user = auth_with_username_or_email(username, password)
+        print(user)
+    except Exception as e:
+        print(e)
+        print('用户名或密码错误')
+        return JsonResponse({'code': 303, 'error': '用户名或密码错误'}, status=303)
+
+    print(f'token = {token}')
+    if user.check_token(token):
+        try:
+            user_token = user.tokens.get(token=token)
+            print('已登录')
+            user_token.delete()
+            # return JsonResponse({'code': 303, 'msg': '已登录'}, status=303)
+        except Exception as e:
+            print('token无效')
+    else:
+        print('token已过期')
+
+    user.last_login = datetime.now()
+
+    new_token = user.make_token()
+    user_token = LoginToken()
+    user_token.user = user
+    user_token.token = new_token
+    user_token.save()
+
+    if hasattr(request, '_user'):
+        print('设置reqeust._user')
+        request.user = user
+
+    print('登录成功')
+    print(f'new_token = {new_token}')
+    return JsonResponse({'code': 200, 'token': new_token})
+
+
+@api_view(['POST'])
+def logout(request):
+    username = request.data.get('username', '')
+    token = request.data.get('token', '')
+    try:
+        user = User.objects.get(username=username)
+        try:
+            user_token = user.tokens.get(token=token)
+            user_token.delete()
+        except Exception as e:
+            print(e)
+            print('token无效')
+        return JsonResponse({'code': 200})
+    except Exception as e:
+        print(e)
+        return JsonResponse({'code': 303, 'error': str(e)}, status=303)
+
+
+@api_view(['POST'])
+def reset_password(request):
+    username = request.data.get('username', '')
+    password = request.data.get('password', '')
+    try:
+        user = User.objects.get(username=username)
+        token = request.data.get('token')
+        if token:
+            print(f'token={token}')
+            if user.check_token(token):
+                # 重置密码
+                print("验证码有效")
+                user.password = password
+                user.save()
+                return JsonResponse({'code': 200})
+            else:
+                print("验证码无效")
+                return JsonResponse({'code': 303, 'error': '验证码错误'}, status=303)
+        else:
+            # 发送验证码
+            token = user.make_token()
+            print(f'')
+            print(f'发送验证码 email = {user.email} token = {token}')
+            user.send_email('ST网盘重置密码验证码', token)
+            return JsonResponse({'code': 200})
+    except Exception as e:
+        print(e)
+        return JsonResponse({'code': 303, 'error': str(e)}, status=303)

BIN
db.sqlite3


+ 4 - 3
folder/models.py

@@ -1,6 +1,5 @@
 from django.db import models
 
-
 # 文件夹表
 from django.db.models import SET_NULL
 
@@ -9,9 +8,11 @@ class Folder(models.Model):
     # 文件夹id
     folder_id = models.AutoField(primary_key=True)
     # 文件夹名
-    folder_name = models.CharField(max_length=50, blank=False)
+    folder_name = models.CharField(max_length=50, blank=False, default='root')
     # 父节点
-    father_folder = models.ForeignKey('self', blank=True, on_delete=SET_NULL, null=True, related_name='the_father_folder')
+    father_folder = models.ForeignKey('self', blank=True, on_delete=SET_NULL, null=True,
+                                      related_name='the_father_folder')
 
     def __unicode__(self):
         return self.folder_id
+

+ 1 - 2
group/models.py

@@ -1,7 +1,6 @@
 from django.db import models
 
-from django.contrib.auth.models import User
-
+from account.models import User
 
 # 群表
 from folder.models import Folder

+ 13 - 1
st_cloud/settings.py

@@ -21,6 +21,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent
 
 # SECURITY WARNING: keep the secret key used in production secret!
 SECRET_KEY = 'django-insecure-h1r^p(6-s&@7u!q(sv%_@97fxv(ikbi7d9p#i9+-o_3&pbpw(j'
+SALT = 'sa0v-038auwmd-r0awvy4-0y4vs9mdy9-aby09384vy-amr9tv8ybsva9v4y'
 
 # SECURITY WARNING: don't run with debug turned on in production!
 DEBUG = True
@@ -41,7 +42,7 @@ INSTALLED_APPS = [
     'group',
     'folder',
     'file',
-    'account',
+    'account'
 ]
 
 
@@ -91,6 +92,8 @@ DATABASES = {
 # Password validation
 # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
 
+PASSWORD_RESET_TIMEOUT = 3600
+
 AUTH_PASSWORD_VALIDATORS = [
     {
         'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
@@ -130,3 +133,12 @@ STATIC_URL = '/static/'
 # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
 
 DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+
+EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
+
+EMAIL_USE_TLS = True
+EMAIL_HOST = 'smtp.qq.com'
+EMAIL_PORT = 25
+EMAIL_HOST_USER = 'lin-xinyuan@qq.com'
+EMAIL_HOST_PASSWORD = 'terwmysqpvxaeahe'
+DEFAULT_FROM_EMAIL = f'ST网盘 <{EMAIL_HOST_USER}>'

+ 1 - 1
st_cloud/urls.py

@@ -21,5 +21,5 @@ urlpatterns = [
     path('group/', include(('group.urls', "group"), namespace='group')),
     path('folder/', include(('folder.urls', "folder"), namespace='folder')),
     path('file/', include(('file.urls', "file"), namespace='file')),
-    path('account/', include(('account.urls', "account"), namespace='account')),
+    path('account/', include(('account.urls', "account"), namespace='account'))
 ]