feat: Implement MFA authentication, add ride statistics model, and update various services, APIs, and tests across the application.

This commit is contained in:
pacnpal
2025-12-28 17:32:53 -05:00
parent aa56c46c27
commit c95f99ca10
452 changed files with 7948 additions and 6073 deletions

View File

@@ -1,41 +1,42 @@
from django.views.generic import DetailView, TemplateView
from django.contrib.auth import get_user_model
from django.shortcuts import get_object_or_404, redirect, render
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.template.loader import render_to_string
from django.utils.crypto import get_random_string
from django.utils import timezone
from datetime import timedelta
from django.contrib.sites.shortcuts import get_current_site
from django.contrib.sites.models import Site
from django.contrib.sites.requests import RequestSite
from django.db.models import QuerySet
from django.http import HttpResponseRedirect, HttpResponse, HttpRequest
from django.urls import reverse
from django.contrib.auth import login
from django.core.files.uploadedfile import UploadedFile
from apps.accounts.models import (
User,
PasswordReset,
EmailVerification,
UserProfile,
)
from apps.lists.models import UserList
from django_forwardemail.services import EmailService
from apps.parks.models import ParkReview
from apps.rides.models import RideReview
from allauth.account.views import LoginView, SignupView
from .mixins import TurnstileMixin
from typing import Dict, Any, Optional, Union, cast
from django_htmx.http import HttpResponseClientRefresh
from contextlib import suppress
import logging
import re
from contextlib import suppress
from datetime import timedelta
from typing import Any, cast
from apps.core.logging import log_exception, log_security_event
from allauth.account.views import LoginView, SignupView
from django.contrib import messages
from django.contrib.auth import get_user_model, login
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.sites.models import Site
from django.contrib.sites.requests import RequestSite
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import UploadedFile
from django.db.models import QuerySet
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils import timezone
from django.utils.crypto import get_random_string
from django.views.generic import DetailView, TemplateView
from django_forwardemail.services import EmailService
from django_htmx.http import HttpResponseClientRefresh
from apps.accounts.models import (
EmailVerification,
PasswordReset,
User,
UserProfile,
)
from apps.core.logging import log_security_event
from apps.lists.models import UserList
from apps.parks.models import ParkReview
from apps.rides.models import RideReview
from .mixins import TurnstileMixin
logger = logging.getLogger(__name__)
@@ -184,7 +185,7 @@ class ProfileView(DetailView):
def get_queryset(self) -> QuerySet[User]:
return User.objects.select_related("profile")
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
context = super().get_context_data(**kwargs)
user = cast(User, self.get_object())
@@ -220,7 +221,7 @@ class ProfileView(DetailView):
class SettingsView(LoginRequiredMixin, TemplateView):
template_name = "accounts/settings.html"
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
context = super().get_context_data(**kwargs)
context["user"] = self.request.user
return context
@@ -283,7 +284,7 @@ class SettingsView(LoginRequiredMixin, TemplateView):
def _handle_password_change(
self, request: HttpRequest
) -> Optional[HttpResponseRedirect]:
) -> HttpResponseRedirect | None:
user = cast(User, request.user)
old_password = request.POST.get("old_password", "")
new_password = request.POST.get("new_password", "")
@@ -385,7 +386,7 @@ def create_password_reset_token(user: User) -> str:
def send_password_reset_email(
user: User, site: Union[Site, RequestSite], token: str
user: User, site: Site | RequestSite, token: str
) -> None:
reset_url = reverse("password_reset_confirm", kwargs={"token": token})
context = {
@@ -435,7 +436,7 @@ def handle_password_reset(
user: User,
new_password: str,
reset: PasswordReset,
site: Union[Site, RequestSite],
site: Site | RequestSite,
) -> None:
user.set_password(new_password)
user.save()
@@ -457,7 +458,7 @@ def handle_password_reset(
def send_password_reset_confirmation(
user: User, site: Union[Site, RequestSite]
user: User, site: Site | RequestSite
) -> None:
context = {
"user": user,