stages/email: check saved get params for token
This commit is contained in:
parent
de1be2df88
commit
491e507d49
|
@ -13,12 +13,15 @@ from structlog import get_logger
|
||||||
from passbook.core.models import Token
|
from passbook.core.models import Token
|
||||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
|
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from passbook.flows.stage import StageView
|
from passbook.flows.stage import StageView
|
||||||
|
from passbook.flows.views import SESSION_KEY_GET
|
||||||
from passbook.stages.email.forms import EmailStageSendForm
|
from passbook.stages.email.forms import EmailStageSendForm
|
||||||
|
from passbook.stages.email.models import EmailStage
|
||||||
from passbook.stages.email.tasks import send_mails
|
from passbook.stages.email.tasks import send_mails
|
||||||
from passbook.stages.email.utils import TemplateEmailMessage
|
from passbook.stages.email.utils import TemplateEmailMessage
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
QS_KEY_TOKEN = "token"
|
QS_KEY_TOKEN = "token"
|
||||||
|
PLAN_CONTEXT_EMAIL_SENT = "email_sent"
|
||||||
|
|
||||||
|
|
||||||
class EmailStageView(FormView, StageView):
|
class EmailStageView(FormView, StageView):
|
||||||
|
@ -30,34 +33,25 @@ class EmailStageView(FormView, StageView):
|
||||||
def get_full_url(self, **kwargs) -> str:
|
def get_full_url(self, **kwargs) -> str:
|
||||||
"""Get full URL to be used in template"""
|
"""Get full URL to be used in template"""
|
||||||
base_url = reverse(
|
base_url = reverse(
|
||||||
"passbook_flows:flow-executor",
|
"passbook_flows:flow-executor-shell",
|
||||||
kwargs={"flow_slug": self.executor.flow.slug},
|
kwargs={"flow_slug": self.executor.flow.slug},
|
||||||
)
|
)
|
||||||
relative_url = f"{base_url}?{urlencode(kwargs)}"
|
relative_url = f"{base_url}?{urlencode(kwargs)}"
|
||||||
return self.request.build_absolute_uri(relative_url)
|
return self.request.build_absolute_uri(relative_url)
|
||||||
|
|
||||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
def send_email(self):
|
||||||
if QS_KEY_TOKEN in request.GET:
|
"""Helper function that sends the actual email. Implies that you've
|
||||||
token = get_object_or_404(Token, pk=request.GET[QS_KEY_TOKEN])
|
already checked that there is a pending user."""
|
||||||
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = token.user
|
|
||||||
token.delete()
|
|
||||||
messages.success(request, _("Successfully verified Email."))
|
|
||||||
return self.executor.stage_ok()
|
|
||||||
return super().get(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def form_invalid(self, form: EmailStageSendForm) -> HttpResponse:
|
|
||||||
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
|
|
||||||
messages.error(self.request, _("No pending user."))
|
|
||||||
return super().form_invalid(form)
|
|
||||||
pending_user = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
pending_user = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
||||||
|
current_stage: EmailStage = self.executor.current_stage
|
||||||
valid_delta = timedelta(
|
valid_delta = timedelta(
|
||||||
minutes=self.executor.current_stage.token_expiry + 1
|
minutes=current_stage.token_expiry + 1
|
||||||
) # + 1 because django timesince always rounds down
|
) # + 1 because django timesince always rounds down
|
||||||
token = Token.objects.create(user=pending_user, expires=now() + valid_delta)
|
token = Token.objects.create(user=pending_user, expires=now() + valid_delta)
|
||||||
# Send mail to user
|
# Send mail to user
|
||||||
message = TemplateEmailMessage(
|
message = TemplateEmailMessage(
|
||||||
subject=_("passbook - Password Recovery"),
|
subject=_(current_stage.subject),
|
||||||
template_name=self.executor.current_stage.template,
|
template_name=current_stage.template,
|
||||||
to=[pending_user.email],
|
to=[pending_user.email],
|
||||||
template_context={
|
template_context={
|
||||||
"url": self.get_full_url(**{QS_KEY_TOKEN: token.pk.hex}),
|
"url": self.get_full_url(**{QS_KEY_TOKEN: token.pk.hex}),
|
||||||
|
@ -65,7 +59,32 @@ class EmailStageView(FormView, StageView):
|
||||||
"expires": token.expires,
|
"expires": token.expires,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
send_mails(self.executor.current_stage, message)
|
send_mails(current_stage, message)
|
||||||
|
|
||||||
|
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
|
# Check if the user came back from the email link to verify
|
||||||
|
if QS_KEY_TOKEN in request.session.get(SESSION_KEY_GET, {}):
|
||||||
|
token = get_object_or_404(
|
||||||
|
Token, pk=request.session[SESSION_KEY_GET][QS_KEY_TOKEN]
|
||||||
|
)
|
||||||
|
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = token.user
|
||||||
|
token.delete()
|
||||||
|
messages.success(request, _("Successfully verified Email."))
|
||||||
|
return self.executor.stage_ok()
|
||||||
|
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
|
||||||
|
messages.error(self.request, _("No pending user."))
|
||||||
|
return self.executor.stage_invalid()
|
||||||
|
# Check if we've already sent the initial e-mail
|
||||||
|
if PLAN_CONTEXT_EMAIL_SENT not in self.executor.plan.context:
|
||||||
|
self.send_email()
|
||||||
|
self.executor.plan.context[PLAN_CONTEXT_EMAIL_SENT] = True
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def form_invalid(self, form: EmailStageSendForm) -> HttpResponse:
|
||||||
|
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
|
||||||
|
messages.error(self.request, _("No pending user."))
|
||||||
|
return super().form_invalid(form)
|
||||||
|
self.send_email()
|
||||||
# We can't call stage_ok yet, as we're still waiting
|
# We can't call stage_ok yet, as we're still waiting
|
||||||
# for the user to click the link in the email
|
# for the user to click the link in the email
|
||||||
return super().form_invalid(form)
|
return super().form_invalid(form)
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
{% block beneath_form %}
|
{% block beneath_form %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<div class="pf-c-form__group pf-m-action">
|
<div class="pf-c-form__group pf-m-action">
|
||||||
<button class="pf-c-button pf-m-primary pf-m-block" type="submit">{% trans "Send Email." %}</button>
|
<button class="pf-c-button pf-m-block" type="submit">{% trans "Send Email again." %}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -83,7 +83,7 @@ class TestEmailStage(TestCase):
|
||||||
response = self.client.post(url)
|
response = self.client.post(url)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(len(mail.outbox), 1)
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
self.assertEqual(mail.outbox[0].subject, "passbook - Password Recovery")
|
self.assertEqual(mail.outbox[0].subject, "passbook")
|
||||||
|
|
||||||
def test_token(self):
|
def test_token(self):
|
||||||
"""Test with token"""
|
"""Test with token"""
|
||||||
|
@ -97,12 +97,20 @@ class TestEmailStage(TestCase):
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
with patch("passbook.flows.views.FlowExecutorView.cancel", MagicMock()):
|
with patch("passbook.flows.views.FlowExecutorView.cancel", MagicMock()):
|
||||||
|
# Call the executor shell to preseed the session
|
||||||
url = reverse(
|
url = reverse(
|
||||||
"passbook_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
"passbook_flows:flow-executor-shell",
|
||||||
|
kwargs={"flow_slug": self.flow.slug},
|
||||||
)
|
)
|
||||||
token = Token.objects.get(user=self.user)
|
token = Token.objects.get(user=self.user)
|
||||||
url += f"?{QS_KEY_TOKEN}={token.pk.hex}"
|
url += f"?{QS_KEY_TOKEN}={token.pk.hex}"
|
||||||
response = self.client.get(url)
|
self.client.get(url)
|
||||||
|
# Call the actual executor to get the JSON Response
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"passbook_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertJSONEqual(
|
self.assertJSONEqual(
|
||||||
|
|
Reference in New Issue