init
This commit is contained in:
0
backend/account/__init__.py
Normal file
0
backend/account/__init__.py
Normal file
105
backend/account/admin.py
Normal file
105
backend/account/admin.py
Normal 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
6
backend/account/apps.py
Normal 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
108
backend/account/email.py
Normal 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)
|
||||
30
backend/account/filters.py
Normal file
30
backend/account/filters.py
Normal 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
16
backend/account/forms.py
Normal 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
|
||||
@@ -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."))
|
||||
59
backend/account/migrations/0001_initial.py
Normal file
59
backend/account/migrations/0001_initial.py
Normal 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()),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
backend/account/migrations/__init__.py
Normal file
0
backend/account/migrations/__init__.py
Normal file
199
backend/account/models.py
Normal file
199
backend/account/models.py
Normal 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
|
||||
|
||||
|
||||
72
backend/account/permissions.py
Normal file
72
backend/account/permissions.py
Normal 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'
|
||||
|
||||
224
backend/account/serializers.py
Normal file
224
backend/account/serializers.py
Normal 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
130
backend/account/tasks.py
Normal 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
3
backend/account/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
33
backend/account/tokens.py
Normal file
33
backend/account/tokens.py
Normal 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
28
backend/account/urls.py
Normal 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
62
backend/account/utils.py
Normal 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
409
backend/account/views.py
Normal 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)
|
||||
Reference in New Issue
Block a user