*: cleanup code, return errors in challenge_invalid, fixup rendering
This commit is contained in:
@ -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}`;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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``;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user