Add proper email notifications
This commit is contained in:
@@ -7,9 +7,9 @@ app_name = "dashboard"
|
||||
urlpatterns = [
|
||||
path("", views.IndexView.as_view(), name="index"),
|
||||
path("my-prints/", views.MyPrintsView.as_view(), name="my_prints"),
|
||||
path("p/<slug:slug>/confirm/<str:token>/", views.ConfirmEmailView.as_view(), name="confirm"),
|
||||
# Routes to be added as features land (see plan.md Section 7):
|
||||
# path("p/<slug:slug>/", views.SubmissionDetailView.as_view(), name="detail"),
|
||||
# path("p/<slug:slug>/status/", views.SubmissionStatusFragment.as_view(), name="status"),
|
||||
# path("p/<slug:slug>/confirm/<str:token>/", views.ConfirmEmailView.as_view(), name="confirm"),
|
||||
# path("p/<slug:slug>/resend/", views.ResendConfirmationView.as_view(), name="resend"),
|
||||
]
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import secrets
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Count, Q
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.utils.html import format_html
|
||||
from django.views import View
|
||||
from django.views.generic import ListView
|
||||
|
||||
from apps.submissions.models import Submission
|
||||
@@ -54,6 +60,76 @@ class IndexView(ListView):
|
||||
return ctx
|
||||
|
||||
|
||||
class ConfirmEmailView(View):
|
||||
"""Email-confirmation landing page (plan.md §7.4 step 8).
|
||||
|
||||
The URL `/p/<slug>/confirm/<token>/` is what the welcome email's button
|
||||
points at. Hitting it does plan.md §7.3's `identifying -> processing`
|
||||
transition (`email_confirmed = True`, `confirmation_token` cleared),
|
||||
then bounces the user back to the dashboard with a green notice. The
|
||||
`processing` worker (plan.md §7.5) picks the row up within ~30 s and
|
||||
moves it to `verifying`, where it becomes visible on the public list.
|
||||
|
||||
Idempotent: hitting the link twice (or after expiry / approval / rejection)
|
||||
doesn't crash -- it just surfaces a neutral "already past the
|
||||
confirmation step" notice and redirects. Constant-time string compare
|
||||
on the token, so a stranger guessing tokens can't sniff prefix matches
|
||||
via response-time differences.
|
||||
"""
|
||||
|
||||
def get(self, request, slug, token):
|
||||
# 404 if the row doesn't exist -- including the case where
|
||||
# cleanup_stale has already nuked an unconfirmed submission.
|
||||
sub = get_object_or_404(Submission, slug=slug)
|
||||
|
||||
if sub.status != Submission.Status.IDENTIFYING:
|
||||
# Already past the confirmation step -- treat as a no-op rather
|
||||
# than an error so refreshing / re-clicking is harmless.
|
||||
messages.info(
|
||||
request,
|
||||
format_html(
|
||||
"Submission <strong class=\"font-mono\">{slug}</strong> is "
|
||||
"already past the confirmation step (current state: "
|
||||
"<em>{status}</em>). Nothing more to do.",
|
||||
slug=sub.slug,
|
||||
status=sub.get_status_display(),
|
||||
),
|
||||
)
|
||||
return redirect("dashboard:index")
|
||||
|
||||
# Constant-time compare on the token to make timing-side-channel
|
||||
# attacks against the 32-byte secret impractical.
|
||||
stored = sub.confirmation_token or ""
|
||||
if not stored or not secrets.compare_digest(stored, token):
|
||||
messages.error(
|
||||
request,
|
||||
format_html(
|
||||
"Couldn't confirm submission <strong class=\"font-mono\">{slug}</strong> — "
|
||||
"the link is invalid or has expired. If you submitted "
|
||||
"this print, please <a href=\"{submit_url}\" class=\"underline font-medium\">"
|
||||
"submit again</a>.",
|
||||
slug=sub.slug,
|
||||
submit_url="/submit/",
|
||||
),
|
||||
)
|
||||
return redirect("dashboard:index")
|
||||
|
||||
sub.status = Submission.Status.PROCESSING
|
||||
sub.email_confirmed = True
|
||||
sub.confirmation_token = ""
|
||||
sub.save()
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
format_html(
|
||||
"Submission <strong class=\"font-mono\">{slug}</strong> confirmed! "
|
||||
"It'll appear on the dashboard shortly, after validation.",
|
||||
slug=sub.slug,
|
||||
),
|
||||
)
|
||||
return redirect("dashboard:index")
|
||||
|
||||
|
||||
class MyPrintsView(LoginRequiredMixin, ListView):
|
||||
"""Private listing -- every submission the signed-in user has ever made.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user