Spaces:
Paused
Paused
| """ | |
| Authentication views supporting both password and magic link login. | |
| """ | |
| from django.shortcuts import render, redirect | |
| from django.contrib.auth import authenticate, login, logout | |
| from django.contrib.auth.decorators import login_required | |
| from django.contrib import messages | |
| from django.views.decorators.http import require_http_methods, require_POST | |
| from django.views.decorators.csrf import csrf_protect | |
| from django.http import HttpRequest, HttpResponse | |
| from django.urls import reverse | |
| from django.utils import timezone | |
| from core.models import MagicToken, User, UserInvite, PasswordResetToken | |
| from core.models.settings import GlobalSettings | |
| from core.email import send_magic_link_email, send_password_reset_email | |
| from core.forms import PasswordLoginForm, SetPasswordForm | |
| def login_view(request: HttpRequest) -> HttpResponse: | |
| """ | |
| Display login form and handle both password and magic link authentication. | |
| GET: Show login form with password (primary) and magic link (secondary) options | |
| POST: Handle password auth or magic link request based on form submission | |
| """ | |
| if request.user.is_authenticated: | |
| return redirect("cpanel:dashboard") | |
| settings = GlobalSettings.get_settings() | |
| email_enabled = settings.email_backend != GlobalSettings.EmailBackend.DISABLED | |
| password_form = PasswordLoginForm() | |
| error_message = None | |
| if request.method == "POST": | |
| action = request.POST.get("action", "password") | |
| if action == "password": | |
| # Password authentication | |
| password_form = PasswordLoginForm(request.POST) | |
| if password_form.is_valid(): | |
| email = password_form.cleaned_data["email"].lower() | |
| password = password_form.cleaned_data["password"] | |
| user = authenticate(request, username=email, password=password) | |
| if user is not None: | |
| login(request, user) | |
| messages.success(request, f"Welcome back, {user.email}!") | |
| return redirect("cpanel:dashboard") | |
| else: | |
| # Check if user exists to give appropriate error | |
| try: | |
| user_obj = User.objects.get(email=email) | |
| if not user_obj.has_usable_password(): | |
| if email_enabled: | |
| error_message = "This account doesn't have a password set. Use the magic link option below." | |
| else: | |
| error_message = "This account doesn't have a password set. Please contact an administrator." | |
| else: | |
| error_message = "Invalid email or password." | |
| except User.DoesNotExist: | |
| error_message = "Invalid email or password." | |
| elif action == "magic_link": | |
| # Magic link flow | |
| return _handle_magic_link_request(request) | |
| return render(request, "auth/login.html", { | |
| "password_form": password_form, | |
| "email_enabled": email_enabled, | |
| "error_message": error_message, | |
| }) | |
| def _handle_magic_link_request(request: HttpRequest) -> HttpResponse: | |
| """Handle magic link login request.""" | |
| email = request.POST.get("magic_email", "").strip().lower() | |
| if not email: | |
| messages.error(request, "Please enter your email address.") | |
| return redirect("auth:login") | |
| if "@" not in email or "." not in email: | |
| messages.error(request, "Please enter a valid email address.") | |
| return redirect("auth:login") | |
| # Check if user exists or if this is the first user | |
| user_exists = User.objects.filter(email=email).exists() | |
| is_first_user = User.objects.count() == 0 | |
| # If user doesn't exist and it's not the first user, check registration settings | |
| if not user_exists and not is_first_user: | |
| settings = GlobalSettings.get_settings() | |
| if not settings.allow_registration: | |
| # Check for valid invite | |
| has_valid_invite = UserInvite.objects.filter( | |
| email=email, | |
| used_at__isnull=True, | |
| expires_at__gt=timezone.now() | |
| ).exists() | |
| if not has_valid_invite: | |
| messages.error( | |
| request, | |
| "Registration is invite-only. Please contact an administrator." | |
| ) | |
| return redirect("auth:login") | |
| ip_address = get_client_ip(request) | |
| token = MagicToken.create_for_email(email, ip_address) | |
| # Check if we should show magic link directly (email disabled) | |
| settings = GlobalSettings.get_settings() | |
| show_magic_link = settings.email_backend == GlobalSettings.EmailBackend.DISABLED | |
| if not show_magic_link: | |
| send_magic_link_email(request, token) | |
| # Store in session for magic_link_sent_view | |
| request.session["magic_token_id"] = str(token.id) | |
| request.session["show_magic_link"] = show_magic_link | |
| return redirect("auth:magic_link_sent") | |
| def magic_link_sent_view(request: HttpRequest) -> HttpResponse: | |
| """ | |
| Confirmation page shown after magic link is sent. | |
| Shows the magic link directly if email is disabled. | |
| """ | |
| context = {} | |
| show_magic_link = request.session.pop("show_magic_link", False) | |
| magic_token_id = request.session.pop("magic_token_id", None) | |
| if show_magic_link and magic_token_id: | |
| try: | |
| token = MagicToken.objects.get(id=magic_token_id) | |
| if token.is_valid(): | |
| verify_url = reverse("auth:verify", kwargs={"token": token.token}) | |
| context["magic_link_url"] = request.build_absolute_uri(verify_url) | |
| context["show_magic_link"] = True | |
| except MagicToken.DoesNotExist: | |
| pass | |
| return render(request, "auth/magic_link_sent.html", context) | |
| def verify_view(request: HttpRequest, token: str) -> HttpResponse: | |
| """ | |
| Verify magic link token and log user in. | |
| """ | |
| try: | |
| magic_token = MagicToken.objects.get(token=token) | |
| except MagicToken.DoesNotExist: | |
| return render(request, "auth/verify.html", { | |
| "error": "Invalid link", | |
| "message": "This magic link is invalid. Please request a new one." | |
| }) | |
| if not magic_token.is_valid(): | |
| if magic_token.used_at: | |
| error_message = "This magic link has already been used." | |
| else: | |
| error_message = "This magic link has expired. Please request a new one." | |
| return render(request, "auth/verify.html", { | |
| "error": "Link expired", | |
| "message": error_message | |
| }) | |
| try: | |
| user = magic_token.consume() | |
| except ValueError as e: | |
| return render(request, "auth/verify.html", { | |
| "error": "Verification failed", | |
| "message": str(e) | |
| }) | |
| login(request, user, backend='django.contrib.auth.backends.ModelBackend') | |
| messages.success(request, f"Welcome back, {user.email}!") | |
| return redirect("cpanel:dashboard") | |
| def logout_view(request: HttpRequest) -> HttpResponse: | |
| """ | |
| Log user out and redirect to login page. | |
| """ | |
| logout(request) | |
| messages.info(request, "You have been logged out.") | |
| return redirect("auth:login") | |
| def accept_invite_view(request: HttpRequest, token: str) -> HttpResponse: | |
| """ | |
| Handle invite link - validates invite and creates magic token for the invited email. | |
| """ | |
| try: | |
| invite = UserInvite.objects.get(token=token) | |
| except UserInvite.DoesNotExist: | |
| return render(request, "auth/invite_invalid.html", { | |
| "error": "Invalid invite link", | |
| "message": "This invite link is invalid or has already been used." | |
| }) | |
| if not invite.is_valid(): | |
| if invite.used_at: | |
| error_message = "This invite has already been used." | |
| else: | |
| error_message = "This invite has expired. Please request a new one from an administrator." | |
| return render(request, "auth/invite_invalid.html", { | |
| "error": "Invite expired", | |
| "message": error_message | |
| }) | |
| # Create magic token for this email | |
| ip_address = get_client_ip(request) | |
| magic_token = MagicToken.create_for_email(invite.email, ip_address) | |
| # Mark invite as used | |
| invite.mark_used(magic_token.user) | |
| # Check if we should show magic link directly (email disabled) | |
| settings = GlobalSettings.get_settings() | |
| show_magic_link = settings.email_backend == GlobalSettings.EmailBackend.DISABLED | |
| if not show_magic_link: | |
| send_magic_link_email(request, magic_token) | |
| # Store in session for magic_link_sent_view | |
| request.session["magic_token_id"] = str(magic_token.id) | |
| request.session["show_magic_link"] = show_magic_link | |
| return redirect("auth:magic_link_sent") | |
| # ============================================================================= | |
| # Password Management Views | |
| # ============================================================================= | |
| def change_password_view(request: HttpRequest) -> HttpResponse: | |
| """Allow users to set or change their password.""" | |
| if request.method == "POST": | |
| form = SetPasswordForm(request.POST) | |
| if form.is_valid(): | |
| password = form.cleaned_data["password"] | |
| request.user.set_password(password) | |
| request.user.save() | |
| # Re-authenticate to update session | |
| login(request, request.user) | |
| messages.success(request, "Password updated successfully.") | |
| return redirect("cpanel:settings") | |
| else: | |
| form = SetPasswordForm() | |
| return render(request, "auth/change_password.html", { | |
| "form": form, | |
| "has_password": request.user.has_usable_password(), | |
| }) | |
| def forgot_password_view(request: HttpRequest) -> HttpResponse: | |
| """Request a password reset email.""" | |
| settings = GlobalSettings.get_settings() | |
| email_enabled = settings.email_backend != GlobalSettings.EmailBackend.DISABLED | |
| if not email_enabled: | |
| messages.error(request, "Password reset is not available. Please contact an administrator.") | |
| return redirect("auth:login") | |
| if request.method == "POST": | |
| email = request.POST.get("email", "").strip().lower() | |
| if not email: | |
| messages.error(request, "Please enter your email address.") | |
| return render(request, "auth/forgot_password.html") | |
| # Always show success message to prevent email enumeration | |
| messages.success( | |
| request, | |
| "If an account exists with that email, a password reset link has been sent." | |
| ) | |
| # Only send email if user exists and has verified their account | |
| try: | |
| user = User.objects.get(email=email) | |
| if user.is_verified: | |
| token = PasswordResetToken.create_for_user(user) | |
| send_password_reset_email(request, user, token) | |
| except User.DoesNotExist: | |
| pass | |
| return redirect("auth:login") | |
| return render(request, "auth/forgot_password.html") | |
| def reset_password_view(request: HttpRequest, token: str) -> HttpResponse: | |
| """Reset password using a token from email.""" | |
| try: | |
| reset_token = PasswordResetToken.objects.get(token=token) | |
| except PasswordResetToken.DoesNotExist: | |
| return render(request, "auth/reset_password.html", { | |
| "error": "Invalid link", | |
| "message": "This password reset link is invalid. Please request a new one." | |
| }) | |
| if not reset_token.is_valid(): | |
| if reset_token.used_at: | |
| error_message = "This password reset link has already been used." | |
| else: | |
| error_message = "This password reset link has expired. Please request a new one." | |
| return render(request, "auth/reset_password.html", { | |
| "error": "Link expired", | |
| "message": error_message | |
| }) | |
| if request.method == "POST": | |
| form = SetPasswordForm(request.POST) | |
| if form.is_valid(): | |
| user = reset_token.consume() | |
| user.set_password(form.cleaned_data["password"]) | |
| user.save() | |
| login(request, user, backend='django.contrib.auth.backends.ModelBackend') | |
| messages.success(request, "Password has been reset successfully.") | |
| return redirect("cpanel:dashboard") | |
| else: | |
| form = SetPasswordForm() | |
| return render(request, "auth/reset_password.html", { | |
| "form": form, | |
| "token": token, | |
| }) | |
| def get_client_ip(request: HttpRequest) -> str: | |
| """Extract client IP from request headers.""" | |
| x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR") | |
| if x_forwarded_for: | |
| return x_forwarded_for.split(",")[0].strip() | |
| return request.META.get("REMOTE_ADDR", "") | |