From 14962eb6ccf627f7fb44f2099d689ac44b3f7f5a Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sun, 21 Feb 2021 13:42:45 +0100 Subject: [PATCH] stages/email: migrate to SPA --- authentik/stages/email/forms.py | 6 --- authentik/stages/email/stage.py | 38 ++++++++++---- .../stages/email/waiting_message.html | 21 -------- web/src/elements/stages/email/EmailStage.ts | 49 +++++++++++++++++++ web/src/pages/generic/FlowExecutor.ts | 4 ++ 5 files changed, 81 insertions(+), 37 deletions(-) delete mode 100644 authentik/stages/email/templates/stages/email/waiting_message.html create mode 100644 web/src/elements/stages/email/EmailStage.ts diff --git a/authentik/stages/email/forms.py b/authentik/stages/email/forms.py index 303d7834b..5e87b6544 100644 --- a/authentik/stages/email/forms.py +++ b/authentik/stages/email/forms.py @@ -5,12 +5,6 @@ from django.utils.translation import gettext_lazy as _ from authentik.stages.email.models import EmailStage, get_template_choices -class EmailStageSendForm(forms.Form): - """Form used when sending the email to prevent multiple emails being sent""" - - invalid = forms.CharField(widget=forms.HiddenInput, required=True) - - class EmailStageForm(forms.ModelForm): """Form to create/edit Email Stage""" diff --git a/authentik/stages/email/stage.py b/authentik/stages/email/stage.py index 5d2ef8991..f6cea9422 100644 --- a/authentik/stages/email/stage.py +++ b/authentik/stages/email/stage.py @@ -3,18 +3,19 @@ from datetime import timedelta from django.contrib import messages from django.http import HttpRequest, HttpResponse -from django.shortcuts import get_object_or_404, reverse +from django.shortcuts import get_object_or_404 +from django.urls import reverse from django.utils.http import urlencode from django.utils.timezone import now from django.utils.translation import gettext as _ -from django.views.generic import FormView +from rest_framework.serializers import ValidationError from structlog.stdlib import get_logger from authentik.core.models import Token +from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER -from authentik.flows.stage import StageView +from authentik.flows.stage import ChallengeStageView from authentik.flows.views import SESSION_KEY_GET -from authentik.stages.email.forms import EmailStageSendForm from authentik.stages.email.models import EmailStage from authentik.stages.email.tasks import send_mails from authentik.stages.email.utils import TemplateEmailMessage @@ -24,11 +25,22 @@ QS_KEY_TOKEN = "token" # nosec PLAN_CONTEXT_EMAIL_SENT = "email_sent" -class EmailStageView(FormView, StageView): +class EmailChallenge(Challenge): + """Email challenge""" + + +class EmailChallengeResponse(ChallengeResponse): + """Email challenge resposen. No fields. This challenge is + always declared invalid to give the user a chance to retry""" + + def validate(self, data): + raise ValidationError("") + + +class EmailStageView(ChallengeStageView): """Email stage which sends Email for verification""" - form_class = EmailStageSendForm - template_name = "stages/email/waiting_message.html" + response_class = EmailChallengeResponse def get_full_url(self, **kwargs) -> str: """Get full URL to be used in template""" @@ -80,11 +92,17 @@ class EmailStageView(FormView, StageView): self.executor.plan.context[PLAN_CONTEXT_EMAIL_SENT] = True return super().get(request, *args, **kwargs) - def form_invalid(self, form: EmailStageSendForm) -> HttpResponse: + def get_challenge(self) -> Challenge: + challenge = EmailChallenge( + data={"type": ChallengeTypes.native, "component": "ak-stage-email"} + ) + return challenge + + def challenge_invalid(self, response: ChallengeResponse) -> HttpResponse: if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context: messages.error(self.request, _("No pending user.")) - return super().form_invalid(form) + return super().challenge_invalid(response) self.send_email() # We can't call stage_ok yet, as we're still waiting # for the user to click the link in the email - return super().form_invalid(form) + return super().challenge_invalid(response) diff --git a/authentik/stages/email/templates/stages/email/waiting_message.html b/authentik/stages/email/templates/stages/email/waiting_message.html deleted file mode 100644 index 5f1762624..000000000 --- a/authentik/stages/email/templates/stages/email/waiting_message.html +++ /dev/null @@ -1,21 +0,0 @@ -{% extends 'login/base.html' %} - -{% load static %} -{% load i18n %} - -{% block card %} -
-

- {% blocktrans %} - Check your Emails for a password reset link. - {% endblocktrans %} -

- {% csrf_token %} - - {% block beneath_form %} - {% endblock %} -
- -
-
-{% endblock %} diff --git a/web/src/elements/stages/email/EmailStage.ts b/web/src/elements/stages/email/EmailStage.ts new file mode 100644 index 000000000..cb01ebe07 --- /dev/null +++ b/web/src/elements/stages/email/EmailStage.ts @@ -0,0 +1,49 @@ +import { gettext } from "django"; +import { CSSResult, customElement, html, property, TemplateResult } from "lit-element"; +import { Challenge } from "../../../api/Flows"; +import { COMMON_STYLES } from "../../../common/styles"; +import { BaseStage } from "../base"; + +export type EmailChallenge = Challenge + +@customElement("ak-stage-email") +export class EmailStage extends BaseStage { + + @property({ attribute: false }) + challenge?: EmailChallenge; + + static get styles(): CSSResult[] { + return COMMON_STYLES; + } + + render(): TemplateResult { + if (!this.challenge) { + return html``; + } + return html`
+

+ ${this.challenge.title} +

+
+
+
{ this.submit(e); }}> +
+

+ ${gettext("Check your Emails for a password reset link.")} +

+
+ +
+ +
+
+
+ `; + } + +} diff --git a/web/src/pages/generic/FlowExecutor.ts b/web/src/pages/generic/FlowExecutor.ts index 6dab191b0..e9c969ede 100644 --- a/web/src/pages/generic/FlowExecutor.ts +++ b/web/src/pages/generic/FlowExecutor.ts @@ -5,11 +5,13 @@ import { getCookie } from "../../utils"; import "../../elements/stages/identification/IdentificationStage"; import "../../elements/stages/password/PasswordStage"; import "../../elements/stages/consent/ConsentStage"; +import "../../elements/stages/email/EmailStage"; import { ShellChallenge, Challenge, ChallengeTypes, Flow, RedirectChallenge } from "../../api/Flows"; import { DefaultClient } from "../../api/Client"; import { IdentificationChallenge } from "../../elements/stages/identification/IdentificationStage"; import { PasswordChallenge } from "../../elements/stages/password/PasswordStage"; import { ConsentChallenge } from "../../elements/stages/consent/ConsentStage"; +import { EmailChallenge } from "../../elements/stages/email/EmailStage"; @customElement("ak-flow-executor") export class FlowExecutor extends LitElement { @@ -112,6 +114,8 @@ export class FlowExecutor extends LitElement { return html``; case "ak-stage-consent": return html``; + case "ak-stage-email": + return html``; default: break; }