This commit is contained in:
2025-10-02 00:54:34 +02:00
commit 84b34c9615
200 changed files with 42048 additions and 0 deletions

View File

105
backend/account/admin.py Normal file
View File

@@ -0,0 +1,105 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import CustomUser
from trznice.admin import custom_admin_site
from django.core.exceptions import PermissionDenied
from .forms import CustomUserCreationForm
from django.db.models import Q
# @admin.register(CustomUser)
class CustomUserAdmin(UserAdmin):
model = CustomUser
add_form = CustomUserCreationForm
list_display = (
"id", "username", "first_name", "last_name", "email", "role",
"create_time", "account_type", "is_active", "is_staff", "email_verified", "is_deleted"
)
list_filter = ("role", "account_type", "is_deleted", "is_active", "is_staff", "email_verified")
search_fields = ("username", "email", "phone_number")
ordering = ("-create_time",)
readonly_fields = ("create_time", "id") # zde
fieldsets = (
(None, {"fields": ("username", "first_name", "last_name", "email", "password")}),
("Osobní údaje", {"fields": ("role", "account_type", "phone_number", "var_symbol", "bank_account", "ICO", "city", "street", "PSC")}),
("Práva a stav", {"fields": ("is_active", "is_staff", "is_superuser", "email_verified", "is_deleted", "deleted_at", "groups", "user_permissions")}),
("Důležité časy", {"fields": ("last_login",)}), # create_time vyjmuto odsud
)
add_fieldsets = (
(None, {
"classes": ("wide",),
"fields": (
"username", "email", "role", "account_type",
"password1", "password2", # ✅ REQUIRED!
),
}),
)
def get_form(self, request, obj=None, **kwargs):
if not obj and getattr(request.user, "role", None) == "cityClerk":
form = CustomUserCreationForm
# Modify choices of the role field in the form class itself
form.base_fields["role"].choices = [
("", "---------"),
("seller", "Prodejce"),
]
return form
return super().get_form(request, obj, **kwargs)
def formfield_for_choice_field(self, db_field, request, **kwargs):
if db_field.name == "role" and request.user.role == "cityClerk":
# Restrict choices to only blank and "seller"
kwargs["choices"] = [
("", "---------"),
("seller", "Prodejce"),
]
return super().formfield_for_choice_field(db_field, request, **kwargs)
def get_list_display(self, request):
if request.user.role == "cityClerk":
return ("email", "username", "role", "account_type", "email_verified") # Keep it minimal
return super().get_list_display(request)
def get_fieldsets(self, request, obj=None):
# "add" view = creating a new user
if obj is None and request.user.role == "cityClerk":
return (
(None, {
"classes": ("wide",),
"fields": ("username", "email", "role", "account_type", "password1", "password2"),
}),
)
# "change" view
if request.user.role == "cityClerk":
return (
(None, {"fields": ("email", "username", "password")}),
("Osobní údaje", {"fields": ("role", "account_type", "phone_number", "var_symbol", "bank_account", "ICO", "city", "street", "PSC")}),
)
# Default for other users
return super().get_fieldsets(request, obj)
def get_queryset(self, request):
qs = self.model.all_objects.all()
if request.user.role == "cityClerk":
return qs.filter(
Q(role__in=["seller", ""]) | (Q(role__isnull=True)) & Q(is_superuser=False) | Q(is_deleted=False))
return qs
def save_model(self, request, obj, form, change):
if request.user.role == "cityClerk":
if obj.role not in ["", None, "seller"]:
raise PermissionDenied("City clerk can't assign this role.")
super().save_model(request, obj, form, change)
custom_admin_site.register(CustomUser, CustomUserAdmin)

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

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

108
backend/account/email.py Normal file
View File

@@ -0,0 +1,108 @@
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes
from django.urls import reverse
from django.core.mail import send_mail
from .tokens import *
from django.contrib.auth import get_user_model
User = get_user_model()
from django.conf import settings
from rest_framework.response import Response
import logging
logger = logging.getLogger(__name__)
# This function sends a password reset email to the user.
def send_password_reset_email(user, request):
uid = urlsafe_base64_encode(force_bytes(user.pk))
token = password_reset_token.make_token(user)
url = f"{settings.FRONTEND_URL}/reset-password/{uid}/{token}"
send_email_with_context(
subject="Obnova hesla",
message=f"Pro obnovu hesla klikni na následující odkaz:\n{url}",
recipients=[user.email],
)
# This function sends an email to the user for email verification after registration.
def send_email_verification(user):
uid = urlsafe_base64_encode(force_bytes(user.pk))
token = account_activation_token.make_token(user)
url = f"{settings.FRONTEND_URL}/email-verification/?uidb64={uid}&token={token}"
message = f"Ověřte svůj e-mail kliknutím na odkaz:\n{url}"
logger.debug(f"\nEMAIL OBSAH:\n {message}\nKONEC OBSAHU")
send_email_with_context(
recipients=user.email,
subject="Ověření e-mailu",
message=f"{message}"
)
def send_email_clerk_add_var_symbol(user):
uid = urlsafe_base64_encode(force_bytes(user.pk))
token = account_activation_token.make_token(user)
# url = f"http://localhost:5173/clerk/add-var-symbol/{uid}/" # NEVIM
url = f"URL"
message = f"Byl vytvořen nový uživatel:\n {user.firstname} {user.secondname} {user.email} .\n Doplňte variabilní symbol {url} ."
if settings.EMAIL_BACKEND == 'django.core.mail.backends.console.EmailBackend':
logger.debug("\nEMAIL OBSAH:\n",message, "\nKONEC OBSAHU")
send_email_with_context(
recipients=user.email,
subject="Doplnění variabilního symbolu",
message=message
)
def send_email_clerk_accepted(user):
uid = urlsafe_base64_encode(force_bytes(user.pk))
token = account_activation_token.make_token(user)
message = f"Úředník potvrdil vaší registraci. Můžete se přihlásit."
if settings.EMAIL_BACKEND == 'django.core.mail.backends.console.EmailBackend':
logger.debug("\nEMAIL OBSAH:\n",message, "\nKONEC OBSAHU")
send_email_with_context(
recipients=user.email,
subject="Úředník potvrdil váší registraci",
message=message
)
def send_email_with_context(recipients, subject, message):
"""
General function to send emails with a specific context.
"""
if isinstance(recipients, str):
recipients = [recipients]
try:
send_mail(
subject=subject,
message=message,
from_email=None,
recipient_list=recipients,
fail_silently=False,
)
return True
except Exception as e:
if settings.EMAIL_BACKEND == 'django.core.mail.backends.console.EmailBackend':
logger.error(f"email se neodeslal... DEBUG: {e}")
pass
else:
return Response({"error": f"E-mail se neodeslal, důvod: {e}"}, status=500)

View File

@@ -0,0 +1,30 @@
import django_filters
from django.contrib.auth import get_user_model
User = get_user_model()
class UserFilter(django_filters.FilterSet):
role = django_filters.CharFilter(field_name="role", lookup_expr="exact")
account_type = django_filters.CharFilter(field_name="account_type", 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")
city = django_filters.CharFilter(field_name="city", lookup_expr="icontains")
street = django_filters.CharFilter(field_name="street", lookup_expr="icontains")
PSC = django_filters.CharFilter(field_name="PSC", lookup_expr="exact")
ICO = django_filters.CharFilter(field_name="ICO", lookup_expr="exact")
RC = django_filters.CharFilter(field_name="RC", lookup_expr="exact")
var_symbol = django_filters.NumberFilter(field_name="var_symbol")
bank_account = django_filters.CharFilter(field_name="bank_account", lookup_expr="icontains")
GDPR = django_filters.BooleanFilter(field_name="GDPR")
is_active = django_filters.BooleanFilter(field_name="is_active")
email_verified = django_filters.BooleanFilter(field_name="email_verified")
create_time_after = django_filters.IsoDateTimeFilter(field_name="create_time", lookup_expr="gte")
create_time_before = django_filters.IsoDateTimeFilter(field_name="create_time", lookup_expr="lte")
class Meta:
model = User
fields = [
"role", "account_type", "email", "phone_number", "city", "street", "PSC",
"ICO", "RC", "var_symbol", "bank_account", "GDPR", "is_active", "email_verified",
"create_time_after", "create_time_before"
]

16
backend/account/forms.py Normal file
View File

@@ -0,0 +1,16 @@
from django import forms
from django.contrib.auth.forms import UserCreationForm
from .models import CustomUser # adjust import to your app
#using: admin.py
class CustomUserCreationForm(UserCreationForm):
class Meta:
model = CustomUser
fields = ("username", "email", "role", "account_type", "password1", "password2")
def save(self, commit=True):
user = super().save(commit=False)
# Optional logic: assign role-based permissions here if needed
if commit:
user.save()
return user

View File

@@ -0,0 +1,40 @@
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from getpass import getpass
class Command(BaseCommand):
help = 'Vytvoří superuživatele s is_active=True a potvrzením hesla'
def handle(self, *args, **kwargs):
User = get_user_model()
# Zadání údajů
username = input("Username: ").strip()
email = input("Email: ").strip()
# Heslo s potvrzením
while True:
password = getpass("Password: ")
password2 = getpass("Confirm password: ")
if password != password2:
self.stdout.write(self.style.ERROR("❌ Hesla se neshodují. Zkus to znovu."))
else:
break
# Kontrola duplicity
if User.objects.filter(username=username).exists():
self.stdout.write(self.style.ERROR("⚠️ Uživatel s tímto username už existuje."))
return
# Vytvoření uživatele
user = User.objects.create_superuser(
username=username,
email=email,
password=password
)
user.is_active = True
if hasattr(user, 'email_verified'):
user.email_verified = True
user.save()
self.stdout.write(self.style.SUCCESS(f"✅ Superuživatel '{username}' úspěšně vytvořen."))

View File

@@ -0,0 +1,59 @@
# Generated by Django 5.2.4 on 2025-08-07 15:13
import account.models
import django.contrib.auth.validators
import django.core.validators
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='CustomUser',
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')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('is_deleted', models.BooleanField(default=False)),
('deleted_at', models.DateTimeField(blank=True, null=True)),
('role', models.CharField(blank=True, choices=[('admin', 'Administrátor'), ('seller', 'Prodejce'), ('squareManager', 'Správce tržiště'), ('cityClerk', 'Úředník'), ('checker', 'Kontrolor')], max_length=32, null=True)),
('account_type', models.CharField(blank=True, choices=[('company', 'Firma'), ('individual', 'Fyzická osoba')], max_length=32, null=True)),
('email_verified', models.BooleanField(default=False)),
('phone_number', models.CharField(blank=True, max_length=16, unique=True, validators=[django.core.validators.RegexValidator('^\\+?\\d{9,15}$', message='Zadejte platné telefonní číslo.')])),
('email', models.EmailField(db_index=True, max_length=254, unique=True)),
('create_time', models.DateTimeField(auto_now_add=True)),
('var_symbol', models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(9999999999), django.core.validators.MinValueValidator(0)])),
('bank_account', models.CharField(blank=True, max_length=255, null=True, validators=[django.core.validators.RegexValidator(code='invalid_bank_account', message='Zadejte platné číslo účtu ve formátu [prefix-]číslo_účtu/kód_banky, např. 1234567890/0100 nebo 123-4567890/0100.', regex='^(\\d{0,6}-)?\\d{10}/\\d{4}$')])),
('ICO', models.CharField(blank=True, max_length=8, null=True, validators=[django.core.validators.RegexValidator(code='invalid_ico', message='IČO musí obsahovat přesně 8 číslic.', regex='^\\d{8}$')])),
('RC', models.CharField(blank=True, max_length=11, null=True, validators=[django.core.validators.RegexValidator(code='invalid_rc', message='Rodné číslo musí být ve formátu 123456/7890.', regex='^\\d{6}\\/\\d{3,4}$')])),
('city', models.CharField(blank=True, max_length=100, null=True)),
('street', models.CharField(blank=True, max_length=200, null=True)),
('PSC', models.CharField(blank=True, max_length=5, null=True, validators=[django.core.validators.RegexValidator(code='invalid_psc', message='PSČ musí obsahovat přesně 5 číslic.', regex='^\\d{5}$')])),
('GDPR', models.BooleanField(default=False)),
('is_active', models.BooleanField(default=False)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to.', related_name='customuser_set', related_query_name='customuser', to='auth.group')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='customuser_set', related_query_name='customuser', to='auth.permission')),
],
options={
'abstract': False,
},
managers=[
('objects', account.models.CustomUserActiveManager()),
('all_objects', account.models.CustomUserAllManager()),
],
),
]

View File

199
backend/account/models.py Normal file
View File

@@ -0,0 +1,199 @@
import uuid
from django.db import models
from django.contrib.auth.models import AbstractUser, Group, Permission
from django.core.validators import RegexValidator, MinLengthValidator, MaxValueValidator, MinValueValidator
from django.conf import settings
from django.db import models
from django.utils import timezone
from datetime import timedelta
from trznice.models import SoftDeleteModel
from django.contrib.auth.models import UserManager
import logging
logger = logging.getLogger(__name__)
# Custom User Manager to handle soft deletion
class CustomUserActiveManager(UserManager):
def get_queryset(self):
return super().get_queryset().filter(is_deleted=False)
# Custom User Manager to handle all users, including soft deleted
class CustomUserAllManager(UserManager):
def get_queryset(self):
return super().get_queryset()
class CustomUser(SoftDeleteModel, AbstractUser):
groups = models.ManyToManyField(
Group,
related_name="customuser_set", # <- přidáš related_name
blank=True,
help_text="The groups this user belongs to.",
related_query_name="customuser",
)
user_permissions = models.ManyToManyField(
Permission,
related_name="customuser_set", # <- přidáš related_name
blank=True,
help_text="Specific permissions for this user.",
related_query_name="customuser",
)
ROLE_CHOICES = (
('admin', 'Administrátor'),
('seller', 'Prodejce'),
('squareManager', 'Správce tržiště'),
('cityClerk', 'Úředník'),
('checker', 'Kontrolor'),
)
role = models.CharField(max_length=32, choices=ROLE_CHOICES, null=True, blank=True)
ACCOUNT_TYPES = (
('company', 'Firma'),
('individual', 'Fyzická osoba')
)
account_type = models.CharField(max_length=32, choices=ACCOUNT_TYPES, null=True, blank=True)
email_verified = models.BooleanField(default=False)
phone_number = models.CharField(
unique=True,
max_length=16,
blank=True,
validators=[RegexValidator(r'^\+?\d{9,15}$', message="Zadejte platné telefonní číslo.")]
)
email = models.EmailField(unique=True, db_index=True)
create_time = models.DateTimeField(auto_now_add=True)
var_symbol = models.PositiveIntegerField(null=True, blank=True, validators=[
MaxValueValidator(9999999999),
MinValueValidator(0)
],
)
bank_account = models.CharField(
max_length=255,
null=True,
blank=True,
validators=[
RegexValidator(
regex=r'^(\d{0,6}-)?\d{10}/\d{4}$', # r'^(\d{0,6}-)?\d{2,10}/\d{4}$' for range 2-10 digits
message="Zadejte platné číslo účtu ve formátu [prefix-]číslo_účtu/kód_banky, např. 1234567890/0100 nebo 123-4567890/0100.",
code='invalid_bank_account'
)
],
)
ICO = models.CharField(
max_length=8,
blank=True,
null=True,
validators=[
RegexValidator(
regex=r'^\d{8}$',
message="IČO musí obsahovat přesně 8 číslic.",
code='invalid_ico'
)
]
)
RC = models.CharField(
max_length=11,
blank=True,
null=True,
validators=[
RegexValidator(
regex=r'^\d{6}\/\d{3,4}$',
message="Rodné číslo musí být ve formátu 123456/7890.",
code='invalid_rc'
)
]
)
city = models.CharField(null=True, blank=True, max_length=100)
street = models.CharField(null=True, blank=True, max_length=200)
PSC = models.CharField(
max_length=5,
blank=True,
null=True,
validators=[
RegexValidator(
regex=r'^\d{5}$',
message="PSČ musí obsahovat přesně 5 číslic.",
code='invalid_psc'
)
]
)
GDPR = models.BooleanField(default=False)
is_active = models.BooleanField(default=False)
objects = CustomUserActiveManager()
all_objects = CustomUserAllManager()
REQUIRED_FIELDS = ['email']
def __str__(self):
return f"{self.email} at {self.create_time.strftime('%d-%m-%Y %H:%M:%S')}"
def generate_login(self, first_name, last_name):
"""
Vygeneruje login ve formátu: prijmeni + 2 písmena jména bez diakritiky.
Přidá číslo pokud už login existuje.
"""
from django.utils.text import slugify
base_login = slugify(f"{last_name}{first_name[:2]}")
login = base_login
counter = 1
while CustomUser.objects.filter(username=login).exists():
login = f"{base_login}{counter}"
counter += 1
return login
def delete(self, *args, **kwargs):
self.is_active = False
self.tickets.all().update(is_deleted=True, deleted_at=timezone.now())
self.user_reservations.all().update(is_deleted=True, deleted_at=timezone.now())
self.orders.all().update(is_deleted=True, deleted_at=timezone.now())
return super().delete(*args, **kwargs)
def save(self, *args, **kwargs):
is_new = self.pk is None # check BEFORE saving
if is_new:
# Ensure first_name and last_name are provided before generating login
if self.first_name and self.last_name:
self.username = self.generate_login(self.first_name, self.last_name)
if self.is_superuser or self.role in ["admin", "cityClerk", "squareManager"]:
# self.is_staff = True
self.is_active = True
if self.role == 'admin':
self.is_staff = True
self.is_superuser = True
if self.is_superuser:
self.role = 'admin'
else:
self.is_staff = False
return super().save(*args, **kwargs)
# NEMAZAT prozatim to nechame, kdybychom to potrebovali
# Now assign permissions after user exists
# if is_new and self.role:
if self.role:
from account.utils import assign_permissions_based_on_role
logger.debug(f"Assigning permissions to: {self.email} with role {self.role}")
assign_permissions_based_on_role(self)
# super().save(*args, **kwargs) # save once, after prep

View File

@@ -0,0 +1,72 @@
from rest_framework.permissions import BasePermission, SAFE_METHODS
from rest_framework.permissions import IsAuthenticated
from rest_framework_api_key.permissions import HasAPIKey
#Podle svého uvážení (NEPOUŽÍVAT!!!)
class RolePermission(BasePermission):
allowed_roles = []
def has_permission(self, request, view):
# Je uživatel přihlášený a má roli z povolených?
user_has_role = (
request.user and
request.user.is_authenticated and
getattr(request.user, "role", None) in self.allowed_roles
)
# Má API klíč?
has_api_key = HasAPIKey().has_permission(request, view)
return user_has_role or has_api_key
#TOHLE POUŽÍT!!!
#Prostě stačí vložit: RoleAllowed('seller','cityClerk')
def RoleAllowed(*roles):
class SafeOrRolePermission(BasePermission):
"""
Allows safe methods for any authenticated user.
Allows unsafe methods only for users with specific roles.
Args:
RolerAllowed('seller', 'cityClerk')
"""
def has_permission(self, request, view):
# Allow safe methods for any authenticated user
if request.method in SAFE_METHODS:
return IsAuthenticated().has_permission(request, view)
# Otherwise, check the user's role
user = request.user
return user and user.is_authenticated and getattr(user, "role", None) in roles
return SafeOrRolePermission
# FIXME: je tohle nutné???
def OnlyRolesAllowed(*roles):
class SafeOrRolePermission(BasePermission):
"""
Allows all methods only for users with specific roles.
"""
def has_permission(self, request, view):
# Otherwise, check the user's role
user = request.user
return user and user.is_authenticated and getattr(user, "role", None) in roles
return SafeOrRolePermission
# For Settings.py
class AdminOnly(BasePermission):
""" Allows access only to users with the 'admin' role.
Args:
BasePermission (rest_framework.permissions.BasePermission): Base class for permission classes.
"""
def has_permission(self, request, view):
return request.user and request.user.is_authenticated and getattr(request.user, 'role', None) == 'admin'

View File

@@ -0,0 +1,224 @@
import re
from django.utils.text import slugify
from django.core.validators import MinValueValidator, MaxValueValidator
from rest_framework import serializers
from rest_framework.exceptions import NotFound
from django.contrib.auth import get_user_model
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from django.utils.translation import gettext_lazy as _
from django.utils.text import slugify
from .permissions import *
from .email import *
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework.exceptions import PermissionDenied
User = get_user_model()
class CustomUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = [
"id",
"username",
"first_name",
"last_name",
"email",
"role",
"account_type",
"email_verified",
"phone_number",
"create_time",
"var_symbol",
"bank_account",
"ICO",
"RC",
"city",
"street",
"PSC",
"GDPR",
"is_active",
]
read_only_fields = ["id", "create_time", "GDPR", "username"] # <-- removed "account_type"
def update(self, instance, validated_data):
user = self.context["request"].user
staff_only_fields = ["role", "email_verified", "var_symbol", "is_active"]
if user.role not in ["admin", "cityClerk"]:
unauthorized = [f for f in staff_only_fields if f in validated_data]
if unauthorized:
raise PermissionDenied(f"You are not allowed to modify: {', '.join(unauthorized)}")
return super().update(instance, validated_data)
# Token obtaining Default Serializer
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
username_field = User.USERNAME_FIELD
def validate(self, attrs):
login = attrs.get("username")
password = attrs.get("password")
# Allow login by username or email
user = User.objects.filter(email__iexact=login).first() or \
User.objects.filter(username__iexact=login).first()
if user is None or not user.check_password(password):
raise serializers.ValidationError(_("No active account found with the given credentials"))
# Call the parent validation to create token
data = super().validate({
self.username_field: user.username,
"password": password
})
data["user_id"] = user.id
data["username"] = user.username
data["email"] = user.email
return data
# user creating section start ------------------------------------------
class UserRegistrationSerializer(serializers.ModelSerializer):
password = serializers.CharField(
write_only=True,
help_text="Heslo musí mít alespoň 8 znaků, obsahovat velká a malá písmena a číslici."
)
class Meta:
model = User
fields = [
'first_name', 'last_name', 'email', 'phone_number', 'account_type',
'password','city', 'street', 'PSC', 'bank_account', 'RC', 'ICO', 'GDPR'
]
extra_kwargs = {
'first_name': {'required': True, 'help_text': 'Křestní jméno uživatele'},
'last_name': {'required': True, 'help_text': 'Příjmení uživatele'},
'email': {'required': True, 'help_text': 'Emailová adresa uživatele'},
'phone_number': {'required': True, 'help_text': 'Telefonní číslo uživatele'},
'account_type': {'required': True, 'help_text': 'Typ účtu'},
'city': {'required': True, 'help_text': 'Město uživatele'},
'street': {'required': True, 'help_text': 'Ulice uživatele'},
'PSC': {'required': True, 'help_text': 'Poštovní směrovací číslo'},
'bank_account': {'required': True, 'help_text': 'Číslo bankovního účtu'},
'RC': {'required': True, 'help_text': 'Rodné číslo'},
'ICO': {'required': True, 'help_text': 'Identifikační číslo organizace'},
'GDPR': {'required': True, 'help_text': 'Souhlas se zpracováním osobních údajů'},
}
def validate_password(self, value):
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("Heslo musí obsahovat alespoň jedno velké písmeno.")
if not re.search(r"[a-z]", value):
raise serializers.ValidationError("Heslo musí obsahovat alespoň jedno malé písmeno.")
if not re.search(r"\d", value):
raise serializers.ValidationError("Heslo musí obsahovat alespoň jednu číslici.")
return value
def validate(self, data):
email = data.get("email")
phone = data.get("phone_number")
dgpr = data.get("GDPR")
if not dgpr:
raise serializers.ValidationError({"GDPR": "Pro registraci musíte souhlasit s GDPR"})
if User.objects.filter(email=email).exists():
raise serializers.ValidationError({"email": "Účet s tímto emailem již existuje."})
if phone and User.objects.filter(phone_number=phone).exists():
raise serializers.ValidationError({"phone_number": "Účet s tímto telefonem již existuje."})
return data
def generate_username(self, first_name, last_name):
# Převod na ascii (bez diakritiky)
base_login = slugify(f"{last_name}{first_name[:2]}")
login = base_login
counter = 1
while User.objects.filter(username=login).exists():
login = f"{base_login}{counter}"
counter += 1
return login
def create(self, validated_data):
password = validated_data.pop("password")
first_name = validated_data.get("first_name", "")
last_name = validated_data.get("last_name", "")
username = self.generate_username(first_name, last_name)
user = User.objects.create(
username=username,
is_active=False, #uživatel je defaultně deaktivovaný
**validated_data
)
user.set_password(password)
user.save()
return user
class UserActivationSerializer(serializers.Serializer):
user_id = serializers.IntegerField()
var_symbol = serializers.IntegerField(validators=[MinValueValidator(0), MaxValueValidator(9999999999)])
def save(self, **kwargs):
try:
user = User.objects.get(pk=self.validated_data['user_id'])
except User.DoesNotExist:
raise NotFound("Uživatel s tímto ID neexistuje.")
user.var_symbol = self.validated_data['var_symbol']
user.is_active = True
user.save()
return user
def to_representation(self, instance):
return {
"id": instance.id,
"email": instance.email,
"var_symbol": instance.var_symbol,
"is_active": instance.is_active,
}
class Meta:
model = User
fields = [
'user_id', 'var_symbol'
]
extra_kwargs = {
'user_id': {'required': True, 'help_text': 'ID uživatele'},
'var_symbol': {'required': True, 'help_text': 'Variablní symbol, zadán úředníkem'},
}
# user creating section end --------------------------------------------
class PasswordResetRequestSerializer(serializers.Serializer):
email = serializers.EmailField(
help_text="E-mail registrovaného a aktivního uživatele, na který bude zaslán reset hesla."
)
def validate_email(self, value):
if not User.objects.filter(email=value, is_active=True).exists():
raise serializers.ValidationError("Účet s tímto emailem neexistuje nebo není aktivní.")
return value
class PasswordResetConfirmSerializer(serializers.Serializer):
password = serializers.CharField(
write_only=True,
help_text="Nové heslo musí mít alespoň 8 znaků, obsahovat velká a malá písmena a číslici."
)
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

130
backend/account/tasks.py Normal file
View File

@@ -0,0 +1,130 @@
from celery import shared_task
from celery.utils.log import get_task_logger
from django.core.mail import send_mail
from django.conf import settings
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes
from .tokens import *
from .models import CustomUser
logger = get_task_logger(__name__)
# This function sends a password reset email to the user.
@shared_task
def send_password_reset_email_task(user_id):
try:
user = CustomUser.objects.get(pk=user_id)
except user.DoesNotExist:
logger.info(f"Task send_password_reset_email has failed. Invalid User ID was sent.")
return 0
uid = urlsafe_base64_encode(force_bytes(user.pk))
token = password_reset_token.make_token(user)
url = f"{settings.FRONTEND_URL}/reset-password/{uid}/{token}"
send_email_with_context(
subject="Obnova hesla",
message=f"Pro obnovu hesla klikni na následující odkaz:\n{url}",
recipients=[user.email],
)
# This function sends an email to the user for email verification after registration.
@shared_task
def send_email_verification_task(user_id):
try:
user = CustomUser.objects.get(pk=user_id)
except user.DoesNotExist:
logger.info(f"Task send_password_reset_email has failed. Invalid User ID was sent.")
return 0
uid = urlsafe_base64_encode(force_bytes(user.pk))
token = account_activation_token.make_token(user)
url = f"{settings.FRONTEND_URL}/email-verification/?uidb64={uid}&token={token}"
message = f"Ověřte svůj e-mail kliknutím na odkaz:\n{url}"
logger.debug(f"\nEMAIL OBSAH:\n {message}\nKONEC OBSAHU")
send_email_with_context(
recipients=user.email,
subject="Ověření e-mailu",
message=f"{message}"
)
@shared_task
def send_email_clerk_add_var_symbol_task(user_id):
try:
user = CustomUser.objects.get(pk=user_id)
except user.DoesNotExist:
logger.info(f"Task send_password_reset_email has failed. Invalid User ID was sent.")
return 0
uid = urlsafe_base64_encode(force_bytes(user.pk))
# url = f"http://localhost:5173/clerk/add-var-symbol/{uid}/" # NEVIM
# TODO: Replace with actual URL once frontend route is ready
url = f"{settings.FRONTEND_URL}/clerk/add-var-symbol/{uid}/"
message = f"Byl vytvořen nový uživatel:\n {user.firstname} {user.secondname} {user.email} .\n Doplňte variabilní symbol {url} ."
if settings.EMAIL_BACKEND == 'django.core.mail.backends.console.EmailBackend':
logger.debug("\nEMAIL OBSAH:\n",message, "\nKONEC OBSAHU")
send_email_with_context(
recipients=user.email,
subject="Doplnění variabilního symbolu",
message=message
)
@shared_task
def send_email_clerk_accepted_task(user_id):
try:
user = CustomUser.objects.get(pk=user_id)
except user.DoesNotExist:
logger.info(f"Task send_password_reset_email has failed. Invalid User ID was sent.")
return 0
uid = urlsafe_base64_encode(force_bytes(user.pk))
token = account_activation_token.make_token(user)
message = f"Úředník potvrdil vaší registraci. Můžete se přihlásit."
if settings.EMAIL_BACKEND == 'django.core.mail.backends.console.EmailBackend':
logger.debug("\nEMAIL OBSAH:\n",message, "\nKONEC OBSAHU")
send_email_with_context(
recipients=user.email,
subject="Úředník potvrdil váší registraci",
message=message
)
def send_email_with_context(recipients, subject, message):
"""
General function to send emails with a specific context.
"""
if isinstance(recipients, str):
recipients = [recipients]
try:
send_mail(
subject=subject,
message=message,
from_email=None,
recipient_list=recipients,
fail_silently=False,
)
if settings.EMAIL_BACKEND == 'django.core.mail.backends.console.EmailBackend':
logger.debug("\nEMAIL OBSAH:\n",message, "\nKONEC OBSAHU")
return True
except Exception as e:
logger.error(f"E-mail se neodeslal: {e}")
return False

3
backend/account/tests.py Normal file
View File

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

33
backend/account/tokens.py Normal file
View File

@@ -0,0 +1,33 @@
from django.contrib.auth.tokens import PasswordResetTokenGenerator
# Subclass PasswordResetTokenGenerator to create a separate token generator
# for account activation. This allows future customization specific to activation tokens,
# even though it currently behaves exactly like the base class.
class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
pass # No changes yet; inherits all behavior from PasswordResetTokenGenerator
# Create an instance of AccountActivationTokenGenerator to be used for generating
# and validating account activation tokens throughout the app.
account_activation_token = AccountActivationTokenGenerator()
# Create an instance of the base PasswordResetTokenGenerator to be used
# for password reset tokens.
password_reset_token = PasswordResetTokenGenerator()
from rest_framework_simplejwt.authentication import JWTAuthentication
#NEMĚNIT CUSTOM SBÍRANÍ COOKIE TOKENU
class CookieJWTAuthentication(JWTAuthentication):
def authenticate(self, request):
raw_token = request.COOKIES.get('access_token')
if not raw_token:
return None
validated_token = self.get_validated_token(raw_token)
return self.get_user(validated_token), validated_token

28
backend/account/urls.py Normal file
View File

@@ -0,0 +1,28 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import *
router = DefaultRouter()
router.register(r'users', UserView, basename='user') # change URL to plural users ?
urlpatterns = [
path('', include(router.urls)), # automaticky přidá všechny cesty z viewsetu
path("user/me/", CurrentUserView.as_view(), name="user-me"), # get current user data
path('token/', CookieTokenObtainPairView.as_view(), name='token_obtain_pair'), #přihlášení (get token)
path('token/refresh/', CookieTokenRefreshView.as_view(), name='token_refresh'), #refresh token
#potom co access token vyprší tak se pomocí refresh tokenu získa další
path('logout/', LogoutView.as_view(), name='logout'), # odhlášení (smaže tokeny)
path('registration/', UserRegistrationViewSet.as_view({'post': 'create'}), name='create_seller'),
#slouží čistě pro email
path("registration/verify-email/<uidb64>/<token>/", EmailVerificationView.as_view(), name="verify-email"),
path("registration/activation-varsymbol/", UserActivationViewSet.as_view(), name="activate_user_and_input_var_symbol"),
path("reset-password/", PasswordResetRequestView.as_view(), name="reset-password-request"),
path("reset-password/<uidb64>/<token>/", PasswordResetConfirmView.as_view(), name="reset-password-confirm"),
]

62
backend/account/utils.py Normal file
View File

@@ -0,0 +1,62 @@
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from booking.models import Event, Reservation, MarketSlot, Square
from product.models import Product, EventProduct
from servicedesk.models import ServiceTicket
from django.contrib.auth import get_user_model
import logging
logger = logging.getLogger(__name__)
def assign_permissions_based_on_role(user):
role_perms = {
"cityClerk": {
"view": [Event, Reservation, MarketSlot, get_user_model(), Product, EventProduct, ServiceTicket],
"add": [Reservation, get_user_model()],
"change": [Reservation, get_user_model()],
# "delete": [Reservation],
},
"squareManager": {
"view": [Event, MarketSlot, Square, Product, EventProduct],
"add": [Event, MarketSlot, Square, Product, EventProduct],
"change": [Event, MarketSlot, Square, Product, EventProduct],
},
# "admin": {
# "view": [Event, Reservation, get_user_model()],
# "add": [Event, Reservation],
# "change": [Event, Reservation],
# "delete": [Event, Reservation],
# },
# etc.
"admin": "all", # Mark this role specially
}
if not user.role:
logger.info("User has no role set")
return
if user.role == "admin":
user.is_staff = True
user.is_superuser = True
# user.save()
return
# Reset in case role changed away from admin
user.is_superuser = False
perms_for_role = role_perms.get(user.role, {})
for action, models in perms_for_role.items():
for model in models:
content_type = ContentType.objects.get_for_model(model)
codename = f"{action}_{model._meta.model_name}"
try:
permission = Permission.objects.get(codename=codename, content_type=content_type)
user.user_permissions.add(permission)
except Permission.DoesNotExist:
# You may log this
pass
# user.save()

409
backend/account/views.py Normal file
View File

@@ -0,0 +1,409 @@
from django.contrib.auth import get_user_model, authenticate
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_str
from django.conf import settings
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import ensure_csrf_cookie
from .serializers import *
from .permissions import *
from .tasks import *
from .models import CustomUser
from .tokens import *
from .filters import UserFilter
from rest_framework import generics, permissions, status, viewsets
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework.views import APIView
from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.exceptions import TokenError, AuthenticationFailed
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import extend_schema, OpenApiResponse, OpenApiExample, OpenApiParameter
User = get_user_model()
#general user view API
import logging
logger = logging.getLogger(__name__)
from rest_framework_simplejwt.views import TokenObtainPairView
#---------------------------------------------TOKENY------------------------------------------------
# Custom Token obtaining view
@extend_schema(
tags=["api"],
summary="Obtain JWT access and refresh tokens (cookie-based)",
request=CustomTokenObtainPairSerializer,
description="Authentication - získaš Access a Refresh token... lze do <username> vložit E-mail nebo username"
)
@method_decorator(ensure_csrf_cookie, name="dispatch")
class CookieTokenObtainPairView(TokenObtainPairView):
permission_classes = [AllowAny]
serializer_class = CustomTokenObtainPairSerializer
def post(self, request, *args, **kwargs):
response = super().post(request, *args, **kwargs)
# Získáme tokeny z odpovědi
access = response.data.get("access")
refresh = response.data.get("refresh")
if not access or not refresh:
return response # Např. při chybě přihlášení
jwt_settings = settings.SIMPLE_JWT
# Access token cookie
response.set_cookie(
key=jwt_settings.get("AUTH_COOKIE", "access_token"),
value=access,
httponly=jwt_settings.get("AUTH_COOKIE_HTTP_ONLY", True),
secure=jwt_settings.get("AUTH_COOKIE_SECURE", not settings.DEBUG),
samesite=jwt_settings.get("AUTH_COOKIE_SAMESITE", "Lax"),
path=jwt_settings.get("AUTH_COOKIE_PATH", "/"),
max_age=int(settings.ACCESS_TOKEN_LIFETIME.total_seconds()),
)
# Refresh token cookie
response.set_cookie(
key=jwt_settings.get("AUTH_COOKIE_REFRESH", "refresh_token"),
value=refresh,
httponly=jwt_settings.get("AUTH_COOKIE_HTTP_ONLY", True),
secure=jwt_settings.get("AUTH_COOKIE_SECURE", not settings.DEBUG),
samesite=jwt_settings.get("AUTH_COOKIE_SAMESITE", "Lax"),
path=jwt_settings.get("AUTH_COOKIE_PATH", "/"),
max_age=int(settings.REFRESH_TOKEN_LIFETIME.total_seconds()),
)
return response
def validate(self, attrs):
username = attrs.get("username")
password = attrs.get("password")
# Přihlaš uživatele ručně
user = authenticate(request=self.context.get('request'), username=username, password=password)
if not user:
raise AuthenticationFailed("Špatné uživatelské jméno nebo heslo.")
if not user.is_active:
raise AuthenticationFailed("Uživatel je deaktivován.")
# Nastav validní uživatele (přebere další logiku ze SimpleJWT)
self.user = user
# Vrátí access a refresh token jako obvykle
return super().validate(attrs)
@extend_schema(
tags=["api"],
summary="Refresh JWT token using cookie",
description="Refresh JWT token"
)
@method_decorator(ensure_csrf_cookie, name="dispatch")
class CookieTokenRefreshView(APIView):
permission_classes = [AllowAny]
def post(self, request):
refresh_token = request.COOKIES.get('refresh_token') or request.data.get('refresh')
if not refresh_token:
return Response({"detail": "Refresh token cookie not found."}, status=status.HTTP_400_BAD_REQUEST)
try:
refresh = RefreshToken(refresh_token)
access_token = str(refresh.access_token)
new_refresh_token = str(refresh) # volitelně nový refresh token
response = Response({
"access": access_token,
"refresh": new_refresh_token,
})
jwt_settings = settings.SIMPLE_JWT
# Access token cookie
response.set_cookie(
key=jwt_settings.get("AUTH_COOKIE", "access_token"),
value=access_token,
httponly=jwt_settings.get("AUTH_COOKIE_HTTP_ONLY", True),
secure=jwt_settings.get("AUTH_COOKIE_SECURE", not settings.DEBUG),
samesite=jwt_settings.get("AUTH_COOKIE_SAMESITE", "Lax"),
path=jwt_settings.get("AUTH_COOKIE_PATH", "/"),
max_age=int(5),
)
# Refresh token cookie
response.set_cookie(
key=jwt_settings.get("AUTH_COOKIE_REFRESH", "refresh_token"),
value=new_refresh_token,
httponly=jwt_settings.get("AUTH_COOKIE_HTTP_ONLY", True),
secure=jwt_settings.get("AUTH_COOKIE_SECURE", not settings.DEBUG),
samesite=jwt_settings.get("AUTH_COOKIE_SAMESITE", "Lax"),
path=jwt_settings.get("AUTH_COOKIE_PATH", "/"),
max_age=int(settings.REFRESH_TOKEN_LIFETIME.total_seconds()),
)
return response
except TokenError:
logger.error("Invalid refresh token used.")
return Response({"detail": "Invalid refresh token."}, status=status.HTTP_401_UNAUTHORIZED)
#---------------------------------------------LOGIN/LOGOUT------------------------------------------------
@extend_schema(
tags=["api"],
summary="Logout user (delete access and refresh token cookies)",
description="Odhlásí uživatele smaže access a refresh token cookies"
)
class LogoutView(APIView):
permission_classes = [AllowAny]
def post(self, request):
response = Response({"detail": "Logout successful"}, status=status.HTTP_200_OK)
# Smazání cookies
response.delete_cookie("access_token", path="/")
response.delete_cookie("refresh_token", path="/")
return response
#--------------------------------------------------------------------------------------------------------------
@extend_schema(
tags=["User"],
responses={200: CustomUserSerializer},
description="Zobrazí všechny uživatele s možností filtrování a řazení.",
)
class UserView(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = CustomUserSerializer
filter_backends = [DjangoFilterBackend]
filterset_class = UserFilter
# Require authentication and role permission
permission_classes = [IsAuthenticated]
class Meta:
model = CustomUser
extra_kwargs = {
"email": {"help_text": "Unikátní e-mailová adresa uživatele."},
"phone_number": {"help_text": "Telefonní číslo ve formátu +420123456789."},
"role": {"help_text": "Role uživatele určující jeho oprávnění v systému."},
"account_type": {"help_text": "Typ účtu firma nebo fyzická osoba."},
"email_verified": {"help_text": "Určuje, zda je e-mail ověřen."},
"create_time": {"help_text": "Datum a čas registrace uživatele (pouze pro čtení).", "read_only": True},
"var_symbol": {"help_text": "Variabilní symbol pro platby, pokud je vyžadován."},
"bank_account": {"help_text": "Číslo bankovního účtu uživatele."},
"ICO": {"help_text": "IČO firmy, pokud se jedná o firemní účet."},
"RC": {"help_text": "Rodné číslo pro fyzické osoby."},
"city": {"help_text": "Město trvalého pobytu / sídla."},
"street": {"help_text": "Ulice a číslo popisné."},
"PSC": {"help_text": "PSČ místa pobytu / sídla."},
"GDPR": {"help_text": "Souhlas se zpracováním osobních údajů."},
"is_active": {"help_text": "Stav aktivace uživatele."},
}
def get_permissions(self):
if self.action in ['list', 'create']: # GET / POST /api/account/users/
return [OnlyRolesAllowed("cityClerk", "admin")()]
elif self.action in ['update', 'partial_update', 'destroy']: # PUT / PATCH / DELETE /api/account/users/{id}
if self.request.user.role in ['cityClerk', 'admin']:
return [OnlyRolesAllowed("cityClerk", "admin")()]
elif self.kwargs.get('pk') and str(self.request.user.id) == self.kwargs['pk']:
return [IsAuthenticated]
else:
# fallback - deny access
return [OnlyRolesAllowed("cityClerk", "admin")()] # or custom DenyAll()
elif self.action == 'retrieve': # GET /api/account/users/{id}
if self.request.user.role in ['cityClerk', 'admin']:
return [OnlyRolesAllowed("cityClerk", "admin")()]
elif self.kwargs.get('pk') and str(self.request.user.id) == self.kwargs['pk']:
return [IsAuthenticated()]
else:
return [OnlyRolesAllowed("cityClerk", "admin")()] # or a custom read-only self-access permission
return super().get_permissions()
# Get current user data
@extend_schema(
tags=["User"],
summary="Get current authenticated user",
description="Vrátí detail aktuálně přihlášeného uživatele podle JWT tokenu nebo session.",
responses={
200: OpenApiResponse(response=CustomUserSerializer),
401: OpenApiResponse(description="Unauthorized, uživatel není přihlášen"),
}
)
class CurrentUserView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
serializer = CustomUserSerializer(request.user)
return Response(serializer.data)
#------------------------------------------------REGISTRACE--------------------------------------------------------------
#1. registration API
@extend_schema(
tags=["User Registration"],
summary="Register a new user (company or individual)",
request=UserRegistrationSerializer,
responses={201: UserRegistrationSerializer},
description="1. Registrace nového uživatele(firmy). Uživateli přijde email s odkazem na ověření.",
)
class UserRegistrationViewSet(ModelViewSet):
queryset = CustomUser.objects.all()
serializer_class = UserRegistrationSerializer
http_method_names = ['post']
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
try:
send_email_verification_task.delay(user.id) # posílaní emailu pro potvrzení registrace - CELERY TASK
except Exception as e:
logger.error(f"Celery not available, using fallback. Error: {e}")
send_email_verification_task(user.id) # posílaní emailu pro potvrzení registrace
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
#2. confirming email
@extend_schema(
tags=["User Registration"],
summary="Verify user email via link",
responses={
200: OpenApiResponse(description="Email úspěšně ověřen."),
400: OpenApiResponse(description="Chybný nebo expirovaný token.")
},
parameters=[
OpenApiParameter(name='uidb64', type=str, location='path', description="Token z E-mailu"),
OpenApiParameter(name='token', type=str, location='path', description="Token uživatele"),
],
description="2. Ověření emailu pomocí odkazu s uid a tokenem. (stačí jenom převzít a poslat)",
)
class EmailVerificationView(APIView):
def get(self, request, uidb64, token):
try:
uid = force_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(pk=uid)
except (User.DoesNotExist, ValueError, TypeError):
return Response({"error": "Neplatný odkaz."}, status=400)
if account_activation_token.check_token(user, token):
user.email_verified = True
user.save()
return Response({"detail": "E-mail byl úspěšně ověřen. Účet čeká na schválení."})
else:
return Response({"error": "Token je neplatný nebo expirovaný."}, status=400)
#3. seller activation API (var_symbol)
@extend_schema(
tags=["User Registration"],
summary="Activate user and set variable symbol (admin/cityClerk only)",
request=UserActivationSerializer,
responses={200: UserActivationSerializer},
description="3. Aktivace uživatele a zadání variabilního symbolu (pouze pro adminy a úředníky).",
)
class UserActivationViewSet(APIView):
permission_classes = [OnlyRolesAllowed('cityClerk', 'admin')]
def patch(self, request, *args, **kwargs):
serializer = UserActivationSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
try:
send_email_clerk_accepted_task.delay(user.id) # posílaní emailu pro informování uživatele o dokončení registrace, uředník doplnil variabilní symbol - CELERY TASK
except Exception as e:
logger.error(f"Celery not available, using fallback. Error: {e}")
send_email_clerk_accepted_task(user.id) # posílaní emailu pro informování uživatele o dokončení registrace, uředník doplnil variabilní symbol
return Response(serializer.to_representation(user), status=status.HTTP_200_OK)
#-------------------------------------------------END REGISTRACE-------------------------------------------------------------
#1. PasswordReset + send Email
@extend_schema(
tags=["User password reset"],
summary="Request password reset (send email)",
request=PasswordResetRequestSerializer,
responses={
200: OpenApiResponse(description="Odeslán email s instrukcemi."),
400: OpenApiResponse(description="Neplatný email.")
},
description="1(a). Požadavek na reset hesla - uživatel zadá svůj email."
)
class PasswordResetRequestView(APIView):
def post(self, request):
serializer = PasswordResetRequestSerializer(data=request.data)
if serializer.is_valid():
try:
user = User.objects.get(email=serializer.validated_data['email'])
except User.DoesNotExist:
# Always return 200 even if user doesn't exist to avoid user enumeration
return Response({"detail": "E-mail s odkazem byl odeslán."})
try:
send_password_reset_email_task.delay(user.id) # posílaní emailu pro obnovení hesla - CELERY TASK
except Exception as e:
logger.error(f"Celery not available, using fallback. Error: {e}")
send_password_reset_email_task(user.id) # posílaní emailu pro obnovení hesla registrace
return Response({"detail": "E-mail s odkazem byl odeslán."})
return Response(serializer.errors, status=400)
#2. Confirming reset
@extend_schema(
tags=["User password reset"],
summary="Confirm password reset via token",
request=PasswordResetConfirmSerializer,
parameters=[
OpenApiParameter(name='uidb64', type=str, location=OpenApiParameter.PATH),
OpenApiParameter(name='token', type=str, location=OpenApiParameter.PATH),
],
responses={
200: OpenApiResponse(description="Heslo bylo změněno."),
400: OpenApiResponse(description="Chybný token nebo data.")
},
description="1(a). Potvrzení resetu hesla pomocí tokenu z emailu."
)
class PasswordResetConfirmView(APIView):
def post(self, request, uidb64, token):
try:
uid = force_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(pk=uid)
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
return Response({"error": "Neplatný odkaz."}, status=400)
if not password_reset_token.check_token(user, token):
return Response({"error": "Token je neplatný nebo expirovaný."}, status=400)
serializer = PasswordResetConfirmSerializer(data=request.data)
if serializer.is_valid():
user.set_password(serializer.validated_data['password'])
user.save()
return Response({"detail": "Heslo bylo úspěšně změněno."})
return Response(serializer.errors, status=400)