posts are done

This commit is contained in:
2026-05-19 00:08:02 +02:00
parent 202ce22102
commit 2e9e3ed41b
35 changed files with 1528 additions and 272 deletions

View File

@@ -4,6 +4,7 @@ from django.contrib.auth import get_user_model
User = get_user_model()
class UserFilter(django_filters.FilterSet):
username = django_filters.CharFilter(field_name="username", lookup_expr="exact")
role = django_filters.CharFilter(field_name="role", lookup_expr="exact")
email = django_filters.CharFilter(field_name="email", lookup_expr="icontains")
phone_number = django_filters.CharFilter(field_name="phone_number", lookup_expr="icontains")
@@ -19,6 +20,6 @@ class UserFilter(django_filters.FilterSet):
class Meta:
model = User
fields = [
"role", "email", "phone_number", "city", "street", "postal_code", "gdpr", "is_active", "email_verified",
"username", "role", "email", "phone_number", "city", "street", "postal_code", "gdpr", "is_active", "email_verified",
"create_time_after", "create_time_before"
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2026-05-18 20:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0002_customuser_avatar'),
]
operations = [
migrations.AddField(
model_name='customuser',
name='banner',
field=models.ImageField(blank=True, null=True, upload_to='banners/'),
),
]

View File

@@ -44,9 +44,9 @@ class CustomUser(SoftDeleteModel, AbstractUser):
class Role(models.TextChoices):
ADMIN = "admin", "cz#Administrátor"
MANAGER = "mod", "cz#Moderator"
CUSTOMER = "regular", "cz#Regular"
REGULAR = "regular", "cz#Regular"
role = models.CharField(max_length=20, choices=Role.choices, default=Role.CUSTOMER)
role = models.CharField(max_length=20, choices=Role.choices, default=Role.REGULAR)
phone_number = models.CharField(
null=True,
@@ -79,6 +79,7 @@ class CustomUser(SoftDeleteModel, AbstractUser):
country = models.CharField(null=True, blank=True, max_length=100)
avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)
banner = models.ImageField(upload_to='banners/', null=True, blank=True)
# firemní fakturační údaje
company_name = models.CharField(max_length=255, blank=True)

View File

@@ -21,8 +21,8 @@ class PublicUserSerializer(serializers.ModelSerializer):
"""Minimal read-only profile returned to non-owner authenticated users."""
class Meta:
model = User
fields = ['id', 'username', 'first_name', 'last_name', 'avatar', 'city', 'role', 'create_time']
read_only_fields = ['id', 'username', 'first_name', 'last_name', 'avatar', 'city', 'role', 'create_time']
fields = ['id', 'username', 'first_name', 'last_name', 'avatar', 'banner', 'city', 'role', 'create_time']
read_only_fields = ['id', 'username', 'first_name', 'last_name', 'avatar', 'banner', 'city', 'role', 'create_time']
class CustomUserSerializer(serializers.ModelSerializer):
@@ -44,6 +44,7 @@ class CustomUserSerializer(serializers.ModelSerializer):
"gdpr",
"is_active",
"avatar",
"banner",
]
read_only_fields = ["id", "create_time", "gdpr", "username"] # <-- removed "account_type"
@@ -201,6 +202,23 @@ class PasswordResetConfirmSerializer(serializers.Serializer):
)
def validate_password(self, value):
import re
if len(value) < 8:
raise serializers.ValidationError("Heslo musí mít alespoň 8 znaků.")
if not re.search(r"[A-Z]", value):
raise serializers.ValidationError("Musí obsahovat velké písmeno.")
if not re.search(r"[a-z]", value):
raise serializers.ValidationError("Musí obsahovat malé písmeno.")
if not re.search(r"\d", value):
raise serializers.ValidationError("Musí obsahovat číslici.")
return value
class ChangePasswordSerializer(serializers.Serializer):
current_password = serializers.CharField(write_only=True)
new_password = serializers.CharField(write_only=True)
def validate_new_password(self, value):
import re
if len(value) < 8:
raise serializers.ValidationError("Heslo musí mít alespoň 8 znaků.")

View File

@@ -20,6 +20,7 @@ urlpatterns = [
# Password reset endpoints
path('password-reset/', views.PasswordResetRequestView.as_view(), name='password-reset-request'),
path('password-reset-confirm/<uidb64>/<token>/', views.PasswordResetConfirmView.as_view(), name='password-reset-confirm'),
path('password-change/', views.ChangePasswordView.as_view(), name='password-change'),
# User CRUD (list, retrieve, update, delete)
path('', include(router.urls)), #/users/

View File

@@ -232,36 +232,29 @@ class UserView(viewsets.ModelViewSet):
},
)
def get_permissions(self):
# Only admin can list or create users
if self.action in ['list', 'create']:
if self.action == 'create':
return [OnlyRolesAllowed("admin")()]
# Only admin or the user themselves can update or delete
elif self.action in ['update', 'partial_update', 'destroy']:
if self.action in ['list', 'retrieve']:
return [IsAuthenticated()]
if self.action in ['update', 'partial_update', 'destroy']:
user = getattr(self, 'request', None) and getattr(self.request, 'user', None)
# Admins can modify any user
if user and getattr(user, 'is_authenticated', False) and getattr(user, 'role', None) == 'admin':
return [OnlyRolesAllowed("admin")()]
# Users can modify their own record
if user and getattr(user, 'is_authenticated', False) and self.kwargs.get('pk') and str(getattr(user, 'id', '')) == self.kwargs['pk']:
lookup = self.kwargs.get('pk', '')
if user and getattr(user, 'is_authenticated', False) and lookup and (
str(getattr(user, 'id', '')) == lookup
):
return [IsAuthenticated()]
# Fallback - deny access (prevents AttributeError for AnonymousUser)
return [OnlyRolesAllowed("admin")()]
# Any authenticated user can retrieve a profile (serializer limits fields for non-owner/non-admin)
elif self.action == 'retrieve':
return [IsAuthenticated()]
return super().get_permissions()
def get_serializer_class(self):
user = getattr(self.request, 'user', None)
pk = self.kwargs.get('pk')
is_self = pk and user and str(getattr(user, 'id', '')) == str(pk)
is_admin = user and (getattr(user, 'role', None) == 'admin' or getattr(user, 'is_superuser', False))
if self.action == 'retrieve' and not is_self and not is_admin:
if self.action in ['retrieve', 'list'] and not is_admin:
return PublicUserSerializer
return CustomUserSerializer
@@ -285,6 +278,29 @@ class CurrentUserView(APIView):
return Response(serializer.data)
@extend_schema(
tags=["account"],
summary="Change password for the authenticated user",
request=ChangePasswordSerializer,
responses={
200: OpenApiResponse(description="Password changed successfully."),
400: OpenApiResponse(description="Invalid current password or validation error."),
},
)
class ChangePasswordView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
serializer = ChangePasswordSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = request.user
if not user.check_password(serializer.validated_data['current_password']):
return Response({"current_password": "Nesprávné heslo."}, status=status.HTTP_400_BAD_REQUEST)
user.set_password(serializer.validated_data['new_password'])
user.save()
return Response({"detail": "Heslo bylo úspěšně změněno."})
#------------------------------------------------REGISTRACE--------------------------------------------------------------
#1. registration API