init
This commit is contained in:
0
backend/users/__init__.py
Normal file
0
backend/users/__init__.py
Normal file
BIN
backend/users/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
backend/users/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/users/__pycache__/admin.cpython-313.pyc
Normal file
BIN
backend/users/__pycache__/admin.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/users/__pycache__/apps.cpython-313.pyc
Normal file
BIN
backend/users/__pycache__/apps.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/users/__pycache__/models.cpython-313.pyc
Normal file
BIN
backend/users/__pycache__/models.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/users/__pycache__/permissions.cpython-313.pyc
Normal file
BIN
backend/users/__pycache__/permissions.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/users/__pycache__/serializers.cpython-313.pyc
Normal file
BIN
backend/users/__pycache__/serializers.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/users/__pycache__/urls.cpython-313.pyc
Normal file
BIN
backend/users/__pycache__/urls.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/users/__pycache__/views.cpython-313.pyc
Normal file
BIN
backend/users/__pycache__/views.cpython-313.pyc
Normal file
Binary file not shown.
3
backend/users/admin.py
Normal file
3
backend/users/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
backend/users/apps.py
Normal file
6
backend/users/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UsersConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'users'
|
||||
62
backend/users/migrations/0001_initial.py
Normal file
62
backend/users/migrations/0001_initial.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# Generated by Django 4.2 on 2025-05-01 18:16
|
||||
|
||||
from django.conf import settings
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('role', models.CharField(choices=[('admin', 'Admin'), ('moderator', 'Moderator'), ('user', 'User')], default='user', max_length=10)),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to.', related_name='custom_user_set', to='auth.group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='custom_user_set', to='auth.permission', verbose_name='user permissions')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'user',
|
||||
'verbose_name_plural': 'users',
|
||||
'abstract': False,
|
||||
},
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PasswordResetToken',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('token', models.CharField(max_length=100, unique=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('expires_at', models.DateTimeField()),
|
||||
('used', models.BooleanField(default=False)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Password Reset Token',
|
||||
'verbose_name_plural': 'Password Reset Tokens',
|
||||
},
|
||||
),
|
||||
]
|
||||
0
backend/users/migrations/__init__.py
Normal file
0
backend/users/migrations/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backend/users/migrations/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
backend/users/migrations/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
57
backend/users/models.py
Normal file
57
backend/users/models.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.utils import timezone
|
||||
import secrets
|
||||
import string
|
||||
|
||||
# Create your models here.
|
||||
class User(AbstractUser):
|
||||
ROLES = (
|
||||
('admin', 'Admin'),
|
||||
('moderator', 'Moderator'),
|
||||
('user', 'User'),
|
||||
)
|
||||
role = models.CharField(max_length=10, choices=ROLES, default='user')
|
||||
|
||||
# Add related_name to resolve clashes
|
||||
groups = models.ManyToManyField(
|
||||
'auth.Group',
|
||||
related_name='custom_user_set',
|
||||
blank=True,
|
||||
help_text='The groups this user belongs to.',
|
||||
verbose_name='groups',
|
||||
)
|
||||
user_permissions = models.ManyToManyField(
|
||||
'auth.Permission',
|
||||
related_name='custom_user_set',
|
||||
blank=True,
|
||||
help_text='Specific permissions for this user.',
|
||||
verbose_name='user permissions',
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.username
|
||||
|
||||
class PasswordResetToken(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
token = models.CharField(max_length=100, unique=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
expires_at = models.DateTimeField()
|
||||
used = models.BooleanField(default=False)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.token:
|
||||
# Generate a random token
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
self.token = ''.join(secrets.choice(alphabet) for _ in range(64))
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def is_valid(self):
|
||||
return (
|
||||
not self.used and
|
||||
self.expires_at > timezone.now()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Password Reset Token'
|
||||
verbose_name_plural = 'Password Reset Tokens'
|
||||
17
backend/users/permissions.py
Normal file
17
backend/users/permissions.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from rest_framework.permissions import BasePermission, SAFE_METHODS
|
||||
|
||||
class IsAuthorOrReadOnly(BasePermission):
|
||||
"""
|
||||
Pozwala edytować tylko własny wpis/komentarz.
|
||||
"""
|
||||
def has_object_permission(self, request, view, obj):
|
||||
if request.method in SAFE_METHODS:
|
||||
return True
|
||||
return obj.author == request.user
|
||||
|
||||
class IsEditorOrAdmin(BasePermission):
|
||||
"""
|
||||
Pozwala publikować treści tylko edytorom lub adminom.
|
||||
"""
|
||||
def has_permission(self, request, view):
|
||||
return request.user.is_authenticated and request.user.role in ['admin', 'editor']
|
||||
66
backend/users/serializers.py
Normal file
66
backend/users/serializers.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from rest_framework import serializers
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.password_validation import validate_password
|
||||
from django.core.exceptions import ValidationError
|
||||
from .models import PasswordResetToken
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
class UserRegistrationSerializer(serializers.ModelSerializer):
|
||||
password = serializers.CharField(write_only=True, required=True, validators=[validate_password])
|
||||
password2 = serializers.CharField(write_only=True, required=True)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('username', 'password', 'password2', 'email', 'first_name', 'last_name', 'role')
|
||||
extra_kwargs = {
|
||||
'first_name': {'required': True},
|
||||
'last_name': {'required': True},
|
||||
'email': {'required': True},
|
||||
'role': {'default': 'user'}
|
||||
}
|
||||
|
||||
def validate(self, attrs):
|
||||
if attrs['password'] != attrs['password2']:
|
||||
raise serializers.ValidationError({"password": "Password fields didn't match."})
|
||||
return attrs
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data.pop('password2')
|
||||
user = User.objects.create_user(**validated_data)
|
||||
return user
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('id', 'username', 'email', 'first_name', 'last_name', 'role')
|
||||
read_only_fields = ('role', 'is_staff', 'is_active', 'is_superuser', 'date_joined', 'last_login', 'groups', 'user_permissions', 'id')
|
||||
|
||||
class ResetPasswordSerializer(serializers.Serializer):
|
||||
email = serializers.EmailField(required=True)
|
||||
|
||||
def validate_email(self, value):
|
||||
try:
|
||||
User.objects.get(email=value)
|
||||
except User.DoesNotExist:
|
||||
# Don't reveal that the email doesn't exist
|
||||
pass
|
||||
return value
|
||||
|
||||
class ResetPasswordConfirmSerializer(serializers.Serializer):
|
||||
token = serializers.UUIDField(required=True)
|
||||
new_password = serializers.CharField(write_only=True, required=True, validators=[validate_password])
|
||||
confirm_password = serializers.CharField(write_only=True, required=True)
|
||||
|
||||
def validate(self, attrs):
|
||||
if attrs['new_password'] != attrs['confirm_password']:
|
||||
raise serializers.ValidationError({"new_password": "Password fields didn't match."})
|
||||
|
||||
try:
|
||||
reset_token = PasswordResetToken.objects.get(token=attrs['token'])
|
||||
if not reset_token.is_valid():
|
||||
raise serializers.ValidationError({"token": "Invalid or expired token."})
|
||||
except PasswordResetToken.DoesNotExist:
|
||||
raise serializers.ValidationError({"token": "Invalid token."})
|
||||
|
||||
return attrs
|
||||
3
backend/users/tests.py
Normal file
3
backend/users/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
22
backend/users/urls.py
Normal file
22
backend/users/urls.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from django.urls import path
|
||||
from rest_framework_simplejwt.views import (
|
||||
TokenObtainPairView,
|
||||
TokenRefreshView,
|
||||
)
|
||||
from .views import (
|
||||
RegisterView,
|
||||
UserDetailView,
|
||||
ChangePasswordView,
|
||||
ResetPasswordView,
|
||||
ResetPasswordConfirmView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path('login/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
|
||||
path('refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
||||
path('register/', RegisterView.as_view(), name='register'),
|
||||
path('me/', UserDetailView.as_view(), name='user-detail'),
|
||||
path('change-password/', ChangePasswordView.as_view(), name='change-password'),
|
||||
path('reset-password/', ResetPasswordView.as_view(), name='reset-password'),
|
||||
path('reset-password/confirm/', ResetPasswordConfirmView.as_view(), name='reset-password-confirm'),
|
||||
]
|
||||
174
backend/users/views.py
Normal file
174
backend/users/views.py
Normal file
@@ -0,0 +1,174 @@
|
||||
from django.shortcuts import render
|
||||
from rest_framework import generics, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import AllowAny, IsAdminUser, IsAuthenticated
|
||||
from rest_framework.views import APIView
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.password_validation import validate_password
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
from django.core.mail import send_mail
|
||||
from django.conf import settings
|
||||
from .serializers import UserRegistrationSerializer, UserSerializer, ResetPasswordSerializer, ResetPasswordConfirmSerializer
|
||||
from .models import User, PasswordResetToken
|
||||
import secrets
|
||||
import string
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
# Create your views here.
|
||||
|
||||
class RegisterView(generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for user registration.
|
||||
|
||||
create:
|
||||
Register a new user with the following fields:
|
||||
- username (required)
|
||||
- password (required)
|
||||
- password2 (required, must match password)
|
||||
- email (required)
|
||||
- first_name (required)
|
||||
- last_name (required)
|
||||
- role (optional, defaults to 'user')
|
||||
"""
|
||||
permission_classes = (AllowAny,)
|
||||
serializer_class = UserRegistrationSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_create(serializer)
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(
|
||||
{"message": "User registered successfully. Please login to continue."},
|
||||
status=status.HTTP_201_CREATED,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
class UserViewSet(generics.ListAPIView):
|
||||
"""
|
||||
API endpoint for viewing and editing users.
|
||||
"""
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = [IsAdminUser]
|
||||
|
||||
class UserDetailView(generics.RetrieveUpdateAPIView):
|
||||
"""
|
||||
API endpoint for retrieving and updating the current user's data.
|
||||
"""
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_object(self):
|
||||
return self.request.user
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
partial = kwargs.pop('partial', False)
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance, data=request.data, partial=partial)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_update(serializer)
|
||||
return Response(serializer.data)
|
||||
|
||||
class ChangePasswordView(APIView):
|
||||
"""
|
||||
API endpoint for changing user password.
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
user = request.user
|
||||
old_password = request.data.get('old_password')
|
||||
new_password = request.data.get('new_password')
|
||||
|
||||
if not old_password or not new_password:
|
||||
return Response(
|
||||
{"detail": "Both old and new password are required."},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
if not user.check_password(old_password):
|
||||
return Response(
|
||||
{"detail": "Current password is incorrect."},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
try:
|
||||
validate_password(new_password, user)
|
||||
except ValidationError as e:
|
||||
return Response(
|
||||
{"detail": list(e.messages)},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
user.set_password(new_password)
|
||||
user.save()
|
||||
return Response(
|
||||
{"detail": "Password successfully updated."},
|
||||
status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
class ResetPasswordView(generics.GenericAPIView):
|
||||
permission_classes = [AllowAny]
|
||||
serializer_class = ResetPasswordSerializer
|
||||
|
||||
def post(self, request):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
email = serializer.validated_data['email']
|
||||
try:
|
||||
user = User.objects.get(email=email)
|
||||
# Create a new reset token
|
||||
token = PasswordResetToken.objects.create(
|
||||
user=user,
|
||||
expires_at=timezone.now() + timedelta(hours=24)
|
||||
)
|
||||
|
||||
# Send reset email
|
||||
reset_url = f"{settings.FRONTEND_URL}/reset-password/{token.token}"
|
||||
send_mail(
|
||||
'Password Reset Request',
|
||||
f'Click the following link to reset your password: {reset_url}',
|
||||
settings.DEFAULT_FROM_EMAIL,
|
||||
[email],
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
return Response({
|
||||
'detail': 'If an account exists with this email, you will receive password reset instructions.'
|
||||
}, status=status.HTTP_200_OK)
|
||||
except User.DoesNotExist:
|
||||
# Don't reveal that the email doesn't exist
|
||||
return Response({
|
||||
'detail': 'If an account exists with this email, you will receive password reset instructions.'
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
class ResetPasswordConfirmView(generics.GenericAPIView):
|
||||
permission_classes = [AllowAny]
|
||||
serializer_class = ResetPasswordConfirmSerializer
|
||||
|
||||
def post(self, request):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
token = serializer.validated_data['token']
|
||||
new_password = serializer.validated_data['new_password']
|
||||
|
||||
reset_token = PasswordResetToken.objects.get(token=token)
|
||||
user = reset_token.user
|
||||
|
||||
# Update password
|
||||
user.set_password(new_password)
|
||||
user.save()
|
||||
|
||||
# Mark token as used
|
||||
reset_token.used = True
|
||||
reset_token.save()
|
||||
|
||||
return Response({
|
||||
'detail': 'Password has been reset successfully.'
|
||||
}, status=status.HTTP_200_OK)
|
||||
Reference in New Issue
Block a user