This commit is contained in:
Jakub Kaniecki
2025-05-18 16:23:03 +02:00
commit 12c76e3e5a
220 changed files with 31696 additions and 0 deletions

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
backend/users/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
backend/users/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'users'

View 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',
},
),
]

View File

57
backend/users/models.py Normal file
View 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'

View 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']

View 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
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

22
backend/users/urls.py Normal file
View 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
View 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)