*: cleanup code, return errors in challenge_invalid, fixup rendering

This commit is contained in:
Jens Langhammer
2021-02-20 23:19:27 +01:00
parent 548b1ead2f
commit 511f94fc7f
25 changed files with 306 additions and 296 deletions

View File

@ -1,6 +1,36 @@
import { DefaultClient, AKResponse, QueryArguments, BaseInheritanceModel } from "./Client";
import { TypeCreate } from "./Providers";
export enum ChallengeTypes {
native = "native",
response = "response",
shell = "shell",
redirect = "redirect",
}
export interface Error {
code: string;
string: string;
}
export interface ErrorDict {
[key: string]: Error[];
}
export interface Challenge {
type: ChallengeTypes;
component?: string;
title?: string;
response_errors?: ErrorDict;
}
export interface ShellChallenge extends Challenge {
body: string;
}
export interface RedirectChallenge extends Challenge {
to: string;
}
export enum FlowDesignation {
Authentication = "authentication",
Authorization = "authorization",
@ -44,6 +74,11 @@ export class Flow {
return r.count;
});
}
static executor(slug: string): Promise<Challenge> {
return DefaultClient.fetch(["flows", "executor", slug]);
}
static adminUrl(rest: string): string {
return `/administration/flows/${rest}`;
}

View File

@ -1,13 +1,14 @@
import { gettext } from "django";
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { Challenge, Error } from "../../../api/Flows";
import { COMMON_STYLES } from "../../../common/styles";
import { BaseStage } from "../base";
export interface IdentificationStageArgs {
export interface IdentificationChallenge extends Challenge {
input_type: string;
primary_action: string;
sources: UILoginButton[];
sources?: UILoginButton[];
application_pre?: string;
@ -22,11 +23,42 @@ export interface UILoginButton {
icon_url?: string;
}
@customElement("ak-form-element")
export class FormElement extends LitElement {
static get styles(): CSSResult[] {
return COMMON_STYLES;
}
@property()
label?: string;
@property({type: Boolean})
required = false;
@property({attribute: false})
errors?: Error[];
render(): TemplateResult {
return html`<div class="pf-c-form__group">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text">${this.label}</span>
${this.required ? html`<span class="pf-c-form__label-required" aria-hidden="true">*</span>` : html``}
</label>
<slot></slot>
${(this.errors || []).map((error) => {
return html`<p class="pf-c-form__helper-text pf-m-error">${error.string}</p>`;
})}
</div>`;
}
}
@customElement("ak-stage-identification")
export class IdentificationStage extends BaseStage {
@property({attribute: false})
args?: IdentificationStageArgs;
challenge?: IdentificationChallenge;
static get styles(): CSSResult[] {
return COMMON_STYLES;
@ -51,58 +83,58 @@ export class IdentificationStage extends BaseStage {
}
renderFooter(): TemplateResult {
if (!(this.args?.enroll_url && this.args.recovery_url)) {
if (!(this.challenge?.enroll_url && this.challenge.recovery_url)) {
return html``;
}
return html`<div class="pf-c-login__main-footer-band">
${this.args.enroll_url ? html`
${this.challenge.enroll_url ? html`
<p class="pf-c-login__main-footer-band-item">
${gettext("Need an account?")}
<a id="enroll" href="${this.args.enroll_url}">${gettext("Sign up.")}</a>
<a id="enroll" href="${this.challenge.enroll_url}">${gettext("Sign up.")}</a>
</p>` : html``}
${this.args.recovery_url ? html`
${this.challenge.recovery_url ? html`
<p class="pf-c-login__main-footer-band-item">
${gettext("Need an account?")}
<a id="recovery" href="${this.args.recovery_url}">${gettext("Forgot username or password?")}</a>
<a id="recovery" href="${this.challenge.recovery_url}">${gettext("Forgot username or password?")}</a>
</p>` : html``}
</div>`;
}
render(): TemplateResult {
if (!this.args) {
if (!this.challenge) {
return html`<ak-loading-state></ak-loading-state>`;
}
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${gettext("Log in to your account")}
${this.challenge.title}
</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e) => {this.submit(e);}}>
${this.args.application_pre ?
<form class="pf-c-form" @submit=${(e: Event) => {this.submit(e);}}>
${this.challenge.application_pre ?
html`<p>
${gettext(`Login to continue to ${this.args.application_pre}.`)}
${gettext(`Login to continue to ${this.challenge.application_pre}.`)}
</p>`:
html``}
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text">${gettext("Email or Username")}</span>
<span class="pf-c-form__label-required" aria-hidden="true">*</span>
</label>
<ak-form-element
label="${gettext("Email or Username")}"
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge?.response_errors || {})["uid_field"]}>
<input type="text" name="uid_field" placeholder="Email or Username" autofocus autocomplete="username" class="pf-c-form-control" required="">
</div>
</ak-form-element>
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${this.args.primary_action}
${this.challenge.primary_action}
</button>
</div>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
${this.args.sources.map((source) => {
${(this.challenge.sources || []).map((source) => {
return this.renderSource(source);
})}
</ul>

View File

@ -1,32 +1,19 @@
import { gettext } from "django";
import { LitElement, html, customElement, property, TemplateResult } from "lit-element";
import { unsafeHTML } from "lit-html/directives/unsafe-html";
import { SentryIgnoredError } from "../../common/errors";
import { getCookie } from "../../utils";
import "../../elements/stages/identification/IdentificationStage";
enum ChallengeTypes {
native = "native",
response = "response",
shell = "shell",
redirect = "redirect",
}
interface Challenge {
type: ChallengeTypes;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args: any;
component?: string;
title?: string;
}
import { ShellChallenge, Challenge, ChallengeTypes, Flow, RedirectChallenge } from "../../api/Flows";
import { DefaultClient } from "../../api/Client";
import { IdentificationChallenge } from "../../elements/stages/identification/IdentificationStage";
@customElement("ak-flow-executor")
export class FlowExecutor extends LitElement {
@property()
flowBodyUrl = "";
flowSlug = "";
@property({attribute: false})
flowBody?: TemplateResult;
challenge?: Challenge;
createRenderRoot(): Element | ShadowRoot {
return this;
@ -41,7 +28,7 @@ export class FlowExecutor extends LitElement {
submit(formData?: FormData): void {
const csrftoken = getCookie("authentik_csrf");
const request = new Request(this.flowBodyUrl, {
const request = new Request(DefaultClient.makeUrl(["flows", "executor", this.flowSlug]), {
headers: {
"X-CSRFToken": csrftoken,
},
@ -55,7 +42,7 @@ export class FlowExecutor extends LitElement {
return response.json();
})
.then((data) => {
this.updateCard(data);
this.challenge = data;
})
.catch((e) => {
this.errorMessage(e);
@ -63,126 +50,33 @@ export class FlowExecutor extends LitElement {
}
firstUpdated(): void {
fetch(this.flowBodyUrl)
.then((r) => {
if (r.status === 404) {
// Fallback when the flow does not exist, just redirect to the root
window.location.pathname = "/";
} else if (!r.ok) {
throw new SentryIgnoredError(r.statusText);
}
return r;
})
.then((r) => {
return r.json();
})
.then((r) => {
this.updateCard(r);
})
.catch((e) => {
// Catch JSON or Update errors
this.errorMessage(e);
});
}
async updateCard(data: Challenge): Promise<void> {
switch (data.type) {
case ChallengeTypes.redirect:
console.debug(`authentik/flows: redirecting to ${data.args.to}`);
window.location.assign(data.args.to || "");
break;
case ChallengeTypes.shell:
this.flowBody = html`${unsafeHTML(data.args.body)}`;
await this.requestUpdate();
this.checkAutofocus();
this.loadFormCode();
this.setFormSubmitHandlers();
break;
case ChallengeTypes.native:
switch (data.component) {
case "ak-stage-identification":
this.flowBody = html`<ak-stage-identification .host=${this} .args=${data.args}></ak-stage-identification>`;
break;
default:
break;
}
break;
default:
console.debug(`authentik/flows: unexpected data type ${data.type}`);
break;
}
}
loadFormCode(): void {
this.querySelectorAll("script").forEach((script) => {
const newScript = document.createElement("script");
newScript.src = script.src;
document.head.appendChild(newScript);
});
}
checkAutofocus(): void {
const autofocusElement = <HTMLElement>this.querySelector("[autofocus]");
if (autofocusElement !== null) {
autofocusElement.focus();
}
}
updateFormAction(form: HTMLFormElement): boolean {
for (let index = 0; index < form.elements.length; index++) {
const element = <HTMLInputElement>form.elements[index];
if (element.value === form.action) {
console.debug(
"authentik/flows: Found Form action URL in form elements, not changing form action."
);
return false;
}
}
form.action = this.flowBodyUrl;
console.debug(`authentik/flows: updated form.action ${this.flowBodyUrl}`);
return true;
}
checkAutosubmit(form: HTMLFormElement): void {
if ("autosubmit" in form.attributes) {
return form.submit();
}
}
setFormSubmitHandlers(): void {
this.querySelectorAll("form").forEach((form) => {
console.debug(`authentik/flows: Checking for autosubmit attribute ${form}`);
this.checkAutosubmit(form);
console.debug(`authentik/flows: Setting action for form ${form}`);
this.updateFormAction(form);
console.debug(`authentik/flows: Adding handler for form ${form}`);
form.addEventListener("submit", (e) => {
e.preventDefault();
const formData = new FormData(form);
this.flowBody = undefined;
this.submit(formData);
});
form.classList.add("ak-flow-wrapped");
Flow.executor(this.flowSlug).then((challenge) => {
this.challenge = challenge;
}).catch((e) => {
// Catch JSON or Update errors
this.errorMessage(e);
});
}
errorMessage(error: string): void {
this.flowBody = html`
<style>
.ak-exception {
font-family: monospace;
overflow-x: scroll;
}
</style>
<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${gettext("Whoops!")}
</h1>
</header>
<div class="pf-c-login__main-body">
<h3>${gettext("Something went wrong! Please try again later.")}</h3>
<pre class="ak-exception">${error}</pre>
</div>`;
this.challenge = <ShellChallenge>{
type: ChallengeTypes.shell,
body: `<style>
.ak-exception {
font-family: monospace;
overflow-x: scroll;
}
</style>
<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${gettext("Whoops!")}
</h1>
</header>
<div class="pf-c-login__main-body">
<h3>${gettext("Something went wrong! Please try again later.")}</h3>
<pre class="ak-exception">${error}</pre>
</div>`
};
}
loading(): TemplateResult {
@ -196,9 +90,28 @@ export class FlowExecutor extends LitElement {
}
render(): TemplateResult {
if (this.flowBody) {
return this.flowBody;
if (!this.challenge) {
return this.loading();
}
return this.loading();
switch(this.challenge.type) {
case ChallengeTypes.redirect:
console.debug(`authentik/flows: redirecting to ${(this.challenge as RedirectChallenge).to}`);
window.location.assign((this.challenge as RedirectChallenge).to);
break;
case ChallengeTypes.shell:
return html`${unsafeHTML((this.challenge as ShellChallenge).body)}`;
case ChallengeTypes.native:
switch (this.challenge.component) {
case "ak-stage-identification":
return html`<ak-stage-identification .host=${this} .challenge=${this.challenge as IdentificationChallenge}></ak-stage-identification>`;
default:
break;
}
break;
default:
console.debug(`authentik/flows: unexpected data type ${this.challenge.type}`);
break;
}
return html``;
}
}