136 lines
7.2 KiB
Python
136 lines
7.2 KiB
Python
from django.contrib import messages
|
|
from django.conf import settings
|
|
from django.contrib.auth import login, logout
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
from django.utils import timezone
|
|
from django.views.decorators.http import require_GET, require_http_methods, require_POST
|
|
from .forms import CustomerPasswordLoginForm, CustomerProfileForm, MobileForm, OTPForm, StaffLoginForm
|
|
from apps.customers.models import CustomerProfile
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.db import transaction
|
|
from django.db.models import F
|
|
from .models import MagicLinkAccessLog, OTPChallenge, TicketMagicLink, User
|
|
from .magic_access import grant_ticket_magic_access
|
|
from apps.integrations.sms.providers import SMSResult, get_sms_provider
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
def _authenticated_home(request):
|
|
if not request.user.is_authenticated: return None
|
|
return redirect("staff_tickets:list" if request.user.account_type==User.AccountType.STAFF else "tickets:list")
|
|
|
|
@require_GET
|
|
def home(request):
|
|
"""Send every visitor to the correct login or authenticated workspace."""
|
|
return _authenticated_home(request) or redirect("accounts:login")
|
|
|
|
@require_http_methods(["GET", "POST"])
|
|
def customer_login(request):
|
|
if response:=_authenticated_home(request): return response
|
|
form = MobileForm(request.POST or None)
|
|
if request.method == "POST" and form.is_valid():
|
|
challenge = None
|
|
try:
|
|
challenge, code = OTPChallenge.issue(form.cleaned_data["mobile"], request)
|
|
is_fixed_dev_otp = settings.DEBUG and challenge.mobile == settings.DEV_OTP_MOBILE and code == settings.DEV_OTP_CODE
|
|
result = SMSResult(True, "fixed-dev-otp") if is_fixed_dev_otp else get_sms_provider().send_otp(challenge.mobile, code)
|
|
if not result.success:
|
|
raise RuntimeError(result.error or "سرویس پیامک درخواست را نپذیرفت.")
|
|
request.session["otp_challenge_id"] = challenge.pk
|
|
return redirect("accounts:otp")
|
|
except ValueError as exc: form.add_error(None, str(exc))
|
|
except Exception:
|
|
if challenge is not None:
|
|
challenge.delete()
|
|
logger.exception("OTP SMS delivery failed")
|
|
form.add_error(None, "ارسال پیامک ورود در حال حاضر ممکن نیست؛ لطفاً کمی بعد دوباره تلاش کنید.")
|
|
return render(request, "accounts/customer_login.html", {"form": form})
|
|
|
|
@require_http_methods(["GET", "POST"])
|
|
def verify_otp(request):
|
|
if response:=_authenticated_home(request): return response
|
|
challenge = get_object_or_404(OTPChallenge, pk=request.session.get("otp_challenge_id"))
|
|
form = OTPForm(request.POST or None)
|
|
if request.method == "POST" and form.is_valid():
|
|
if challenge.verify(form.cleaned_data["code"]):
|
|
user, created = User.objects.get_or_create(mobile=challenge.mobile, defaults={"username": f"customer_{challenge.mobile[3:]}", "account_type": User.AccountType.CUSTOMER})
|
|
if created: user.set_unusable_password(); user.save(update_fields=["password"])
|
|
CustomerProfile.objects.get_or_create(user=user)
|
|
if not user.is_active: form.add_error(None, "حساب شما غیرفعال است.")
|
|
else:
|
|
login(request, user, backend="django.contrib.auth.backends.ModelBackend")
|
|
return redirect(request.session.pop("next", "tickets:list"))
|
|
else: form.add_error("code", "کد نامعتبر یا منقضی است.")
|
|
return render(request, "accounts/otp.html", {"form": form, "mobile": challenge.mobile})
|
|
|
|
@require_http_methods(["GET","POST"])
|
|
def customer_password_login(request):
|
|
if response:=_authenticated_home(request): return response
|
|
form=CustomerPasswordLoginForm(request.POST or None)
|
|
if request.method=="POST" and form.is_valid():
|
|
user=form.authenticate(request)
|
|
if user and user.is_active:
|
|
login(request,user); return redirect(request.session.pop("next","tickets:list"))
|
|
form.add_error(None,"شماره موبایل یا رمز عبور صحیح نیست.")
|
|
return render(request,"accounts/customer_password_login.html",{"form":form})
|
|
|
|
@login_required
|
|
@require_http_methods(["GET","POST"])
|
|
def customer_profile(request):
|
|
if request.user.account_type!=User.AccountType.CUSTOMER: return redirect("staff_tickets:list")
|
|
profile,_=CustomerProfile.objects.get_or_create(user=request.user)
|
|
form=CustomerProfileForm(request.POST or None,instance=profile)
|
|
if request.method=="POST" and form.is_valid():
|
|
form.save(); password=form.cleaned_data.get("password")
|
|
if password: request.user.set_password(password); request.user.save(update_fields=["password"]); login(request,request.user,backend="django.contrib.auth.backends.ModelBackend")
|
|
messages.success(request,"پروفایل شما ذخیره شد."); return redirect("accounts:profile")
|
|
return render(request,"accounts/profile.html",{"form":form})
|
|
|
|
@require_http_methods(["GET", "POST"])
|
|
def staff_login(request):
|
|
if response:=_authenticated_home(request): return response
|
|
form = StaffLoginForm(request.POST or None)
|
|
if request.method == "POST" and form.is_valid():
|
|
user = form.authenticate(request)
|
|
if user and user.is_active and user.account_type == User.AccountType.STAFF:
|
|
login(request, user); return redirect("staff_tickets:list")
|
|
form.add_error(None, "اطلاعات ورود صحیح نیست.")
|
|
return render(request, "accounts/staff_login.html", {"form": form})
|
|
|
|
@require_POST
|
|
def logout_view(request):
|
|
logout(request)
|
|
request.session.flush()
|
|
response=redirect("accounts:login")
|
|
response["Cache-Control"]="no-store"
|
|
return response
|
|
|
|
def consume_magic_link(request, token):
|
|
link = TicketMagicLink.objects.select_related("customer", "ticket").filter(token_hash=TicketMagicLink.digest(token)).first()
|
|
if not link or link.revoked_at or link.expires_at <= timezone.now():
|
|
request.session["next"] = f"/tickets/{link.ticket.public_id}/" if link else "/tickets/"
|
|
messages.error(request, "این لینک منقضی شده یا اشتباه است. لطفاً با شماره موبایل خود وارد شوید تا بتوانید پیامها را ببینید.")
|
|
return redirect("accounts:login")
|
|
if request.user.is_authenticated:
|
|
if request.user.pk != link.customer_id:
|
|
from django.core.exceptions import PermissionDenied
|
|
raise PermissionDenied
|
|
else:
|
|
grant_ticket_magic_access(request, link)
|
|
ip_address = request.META.get("REMOTE_ADDR")
|
|
user_agent = request.META.get("HTTP_USER_AGENT", "")[:500]
|
|
with transaction.atomic():
|
|
MagicLinkAccessLog.objects.create(
|
|
magic_link=link,
|
|
ip_address=ip_address,
|
|
user_agent=user_agent,
|
|
)
|
|
TicketMagicLink.objects.filter(pk=link.pk).update(
|
|
used_at=timezone.now(),
|
|
last_used_ip=ip_address,
|
|
last_user_agent=user_agent,
|
|
view_count=F("view_count") + 1,
|
|
)
|
|
response = redirect("tickets:detail", public_id=link.ticket.public_id); response["Referrer-Policy"] = "no-referrer"; return response
|