flows: mount executor under api, implement initial challenge design

This commit is contained in:
Jens Langhammer
2021-02-17 23:52:49 +01:00
parent 8708e487ae
commit eb01b42425
33 changed files with 482 additions and 218 deletions

View File

@ -0,0 +1,10 @@
import { customElement, html, LitElement, TemplateResult } from "lit-element";
@customElement("ak-stage-authenticator-validate")
export class AuthenticatorValidateStage extends LitElement {
render(): TemplateResult {
return html`ak-stage-authenticator-validate`;
}
}

View File

@ -0,0 +1,10 @@
import { LitElement } from "lit-element";
import { FlowExecutor } from "../../pages/generic/FlowExecutor";
export class BaseStage extends LitElement {
// submit()
host?: FlowExecutor;
}

View File

@ -0,0 +1,101 @@
import { gettext } from "django";
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
import { COMMON_STYLES } from "../../../common/styles";
import { BaseStage } from "../base";
export interface IdentificationStageArgs {
input_type: string;
primary_action: string;
sources: string[];
application_pre?: string;
}
@customElement("ak-stage-identification")
export class IdentificationStage extends BaseStage {
@property({attribute: false})
args?: IdentificationStageArgs;
static get styles(): CSSResult[] {
return COMMON_STYLES;
}
render(): TemplateResult {
if (!this.args) {
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")}
</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form">
${this.args.application_pre ?
html`<p>
${gettext(`Login to continue to ${this.args.application_pre}.`)}
</p>`:
html``}
<div class="pf-c-form__group">
<label class="pf-c-form__label" for="uid_field-0">
<span class="pf-c-form__label-text">${gettext("Email or Username")}</span>
<span class="pf-c-form__label-required" aria-hidden="true">*</span>
</label>
<input type="text" name="uid_field" placeholder="Email or Username" autofocus autocomplete="username" class="pf-c-form-control" required="" id="id_uid_field">
</div>
<div class="pf-c-form__group pf-m-action">
<button class="pf-c-button pf-m-primary pf-m-block" @click=${(e: Event) => {
e.preventDefault();
const form = new FormData(this.shadowRoot.querySelector("form"));
this.host?.submit(form);
}}>
${this.args.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(() => {
// TODO: source testing
// TODO: Placeholder and label for input above
return html``;
// {% for source in sources %}
// <li class="pf-c-login__main-footer-links-item">
// <a href="{{ source.url }}" class="pf-c-login__main-footer-links-item-link">
// {% if source.icon_path %}
// <img src="{% static source.icon_path %}" style="width:24px;" alt="{{ source.name }}">
// {% elif source.icon_url %}
// <img src="icon_url" alt="{{ source.name }}">
// {% else %}
// <i class="pf-icon pf-icon-arrow" title="{{ source.name }}"></i>
// {% endif %}
// </a>
// </li>
// {% endfor %}
})}
</ul>
{% if enroll_url or recovery_url %}
<div class="pf-c-login__main-footer-band">
{% if enroll_url %}
<p class="pf-c-login__main-footer-band-item">
{% trans 'Need an account?' %}
<a role="enroll" href="{{ enroll_url }}">{% trans 'Sign up.' %}</a>
</p>
{% endif %}
{% if recovery_url %}
<p class="pf-c-login__main-footer-band-item">
<a role="recovery" href="{{ recovery_url }}">{% trans 'Forgot username or password?' %}</a>
</p>
{% endif %}
</div>
{% endif %}
</footer>`;
}
}

3
web/src/flow.ts Normal file
View File

@ -0,0 +1,3 @@
import "construct-style-sheets-polyfill";
import "./pages/generic/FlowExecutor";

View File

@ -22,7 +22,6 @@ import "./elements/Spinner";
import "./elements/Tabs";
import "./elements/router/RouterOutlet";
import "./pages/generic/FlowShellCard";
import "./pages/generic/SiteShell";
import "./pages/admin-overview/AdminOverviewPage";
@ -33,5 +32,7 @@ import "./pages/LibraryPage";
import "./elements/stages/authenticator_webauthn/WebAuthnRegister";
import "./elements/stages/authenticator_webauthn/WebAuthnAuth";
import "./elements/stages/authenticator_validate/AuthenticatorValidateStage";
import "./elements/stages/identification/IdentificationStage";
import "./interfaces/AdminInterface";

View File

@ -7,6 +7,7 @@ import "../../elements/Tabs";
import "../../elements/AdminLoginsChart";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton";
import "../../elements/buttons/Dropdown";
import "../../elements/policies/BoundPoliciesList";
import { FlowStageBinding, Stage } from "../../api/Flows";
import { until } from "lit-html/directives/until";

View File

@ -1,25 +1,30 @@
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 ResponseType {
enum ChallengeTypes {
native = "native",
response = "response",
shell = "shell",
redirect = "redirect",
template = "template",
}
interface Response {
type: ResponseType;
to?: string;
body?: string;
interface Challenge {
type: ChallengeTypes;
args: { [key: string]: string };
component: string;
}
@customElement("ak-flow-shell-card")
export class FlowShellCard extends LitElement {
@customElement("ak-flow-executor")
export class FlowExecutor extends LitElement {
@property()
flowBodyUrl = "";
@property()
flowBody?: string;
@property({attribute: false})
flowBody?: TemplateResult;
createRenderRoot(): Element | ShadowRoot {
return this;
@ -28,28 +33,33 @@ export class FlowShellCard extends LitElement {
constructor() {
super();
this.addEventListener("ak-flow-submit", () => {
const csrftoken = getCookie("authentik_csrf");
const request = new Request(this.flowBodyUrl, {
headers: {
"X-CSRFToken": csrftoken,
},
});
fetch(request, {
method: "POST",
mode: "same-origin"
})
.then((response) => {
return response.json();
})
.then((data) => {
this.updateCard(data);
})
.catch((e) => {
this.errorMessage(e);
});
this.submit();
});
}
submit(formData?: FormData): void {
const csrftoken = getCookie("authentik_csrf");
const request = new Request(this.flowBodyUrl, {
headers: {
"X-CSRFToken": csrftoken,
},
});
fetch(request, {
method: "POST",
mode: "same-origin",
body: formData,
})
.then((response) => {
return response.json();
})
.then((data) => {
this.updateCard(data);
})
.catch((e) => {
this.errorMessage(e);
});
}
firstUpdated(): void {
fetch(this.flowBodyUrl)
.then((r) => {
@ -73,19 +83,29 @@ export class FlowShellCard extends LitElement {
});
}
async updateCard(data: Response): Promise<void> {
async updateCard(data: Challenge): Promise<void> {
switch (data.type) {
case ResponseType.redirect:
console.debug(`authentik/flows: redirecting to ${data.to}`);
window.location.assign(data.to || "");
case ChallengeTypes.redirect:
console.debug(`authentik/flows: redirecting to ${data.args.to}`);
window.location.assign(data.args.to || "");
break;
case ResponseType.template:
this.flowBody = data.body;
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;
}
// this.flowBody = html`${unsafeHTML(`<${data.component} .args="${data.args}"></${data.component}>`)}`;
break;
default:
console.debug(`authentik/flows: unexpected data type ${data.type}`);
break;
@ -139,26 +159,14 @@ export class FlowShellCard extends LitElement {
e.preventDefault();
const formData = new FormData(form);
this.flowBody = undefined;
fetch(this.flowBodyUrl, {
method: "post",
body: formData,
})
.then((response) => {
return response.json();
})
.then((data) => {
this.updateCard(data);
})
.catch((e) => {
this.errorMessage(e);
});
this.submit(formData);
});
form.classList.add("ak-flow-wrapped");
});
}
errorMessage(error: string): void {
this.flowBody = `
this.flowBody = html`
<style>
.ak-exception {
font-family: monospace;
@ -167,13 +175,11 @@ export class FlowShellCard extends LitElement {
</style>
<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
Whoops!
${gettext("Whoops!")}
</h1>
</header>
<div class="pf-c-login__main-body">
<h3>
Something went wrong! Please try again later.
</h3>
<h3>${gettext("Something went wrong! Please try again later.")}</h3>
<pre class="ak-exception">${error}</pre>
</div>`;
}
@ -190,7 +196,7 @@ export class FlowShellCard extends LitElement {
render(): TemplateResult {
if (this.flowBody) {
return html(<TemplateStringsArray>(<unknown>[this.flowBody]));
return this.flowBody;
}
return this.loading();
}