web/flows: improve authenticator styling (#8560)

* fix empty state shifting when switching from loading to icon

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix static token setup misaligned

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add option to submit flow invisibly

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* remove lots of duplicate code and fix styling

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* put return button below submit button

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L
2024-02-19 11:20:47 +01:00
committed by GitHub
parent f62a470dfa
commit 848dae52ab
10 changed files with 300 additions and 295 deletions

View File

@ -6,3 +6,4 @@ dist
coverage
src/locale-codes.ts
storybook-static/
src/locales/**

View File

@ -1,7 +1,7 @@
import { AKElement } from "@goauthentik/elements/Base";
import { PFSize } from "@goauthentik/elements/Spinner";
import { CSSResult, TemplateResult, html } from "lit";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
@ -23,7 +23,17 @@ export class EmptyState extends AKElement {
header = "";
static get styles(): CSSResult[] {
return [PFBase, PFEmptyState, PFTitle];
return [
PFBase,
PFEmptyState,
PFTitle,
css`
i.pf-c-empty-state__icon {
height: var(--pf-global--icon--FontSize--2xl);
line-height: var(--pf-global--icon--FontSize--2xl);
}
`,
];
}
render(): TemplateResult {

View File

@ -15,7 +15,7 @@ import "@goauthentik/flow/sources/apple/AppleLoginInit";
import "@goauthentik/flow/sources/plex/PlexLoginInit";
import "@goauthentik/flow/stages/FlowErrorStage";
import "@goauthentik/flow/stages/RedirectStage";
import { StageHost } from "@goauthentik/flow/stages/base";
import { StageHost, SubmitOptions } from "@goauthentik/flow/stages/base";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
@ -189,12 +189,17 @@ export class FlowExecutor extends Interface implements StageHost {
return globalAK()?.brand.uiTheme || UiThemeEnum.Automatic;
}
async submit(payload?: FlowChallengeResponseRequest): Promise<boolean> {
async submit(
payload?: FlowChallengeResponseRequest,
options?: SubmitOptions,
): Promise<boolean> {
if (!payload) return Promise.reject();
if (!this.challenge) return Promise.reject();
// @ts-ignore
// @ts-expect-error
payload.component = this.challenge.component;
this.loading = true;
if (!options?.invisible) {
this.loading = true;
}
try {
const challenge = await new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({
flowSlug: this.flowSlug,

View File

@ -40,6 +40,7 @@ export class AuthenticatorStaticStage extends BaseStage<
columns: 2;
-webkit-columns: 2;
-moz-columns: 2;
column-width: 1em;
margin-left: var(--pf-global--spacer--xs);
}
ul li {

View File

@ -2,13 +2,12 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageCode";
import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageDuo";
import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageWebAuthn";
import { BaseStage, StageHost } from "@goauthentik/flow/stages/base";
import { BaseStage, StageHost, SubmitOptions } from "@goauthentik/flow/stages/base";
import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/IdentificationStage";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
@ -59,7 +58,7 @@ export class AuthenticatorValidateStage
// We don't use this.submit here, as we don't want to advance the flow.
// We just want to notify the backend which challenge has been selected.
new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({
flowSlug: this.host.flowSlug || "",
flowSlug: this.host?.flowSlug || "",
query: window.location.search.substring(1),
flowChallengeResponseRequest: {
// @ts-ignore
@ -73,8 +72,11 @@ export class AuthenticatorValidateStage
return this._selectedDeviceChallenge;
}
submit(payload: AuthenticatorValidationChallengeResponseRequest): Promise<boolean> {
return this.host?.submit(payload) || Promise.resolve();
submit(
payload: AuthenticatorValidationChallengeResponseRequest,
options?: SubmitOptions,
): Promise<boolean> {
return this.host?.submit(payload, options) || Promise.resolve();
}
static get styles(): CSSResult[] {
@ -253,23 +255,7 @@ export class AuthenticatorValidateStage
? this.renderDeviceChallenge()
: html`<div class="pf-c-login__main-body">
<form class="pf-c-form">
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${msg("Not you?")}</a
>
</div>
</ak-form-static>
<input
name="username"
autocomplete="username"
type="hidden"
value="${this.challenge.pendingUser}"
/>
${this.renderUserInfo()}
${this.selectedDeviceChallenge
? ""
: html`<p>${msg("Select an authentication method.")}</p>`}

View File

@ -1,59 +1,34 @@
import { BaseDeviceStage } from "@goauthentik/app/flow/stages/authenticator_validate/base";
import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/forms/FormElement";
import "@goauthentik/flow/FormStatic";
import { AuthenticatorValidateStage } from "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStage";
import { BaseStage } from "@goauthentik/flow/stages/base";
import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/IdentificationStage";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { customElement } from "lit/decorators.js";
import {
AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest,
DeviceChallenge,
DeviceClassesEnum,
} from "@goauthentik/api";
@customElement("ak-stage-authenticator-validate-code")
export class AuthenticatorValidateStageWebCode extends BaseStage<
export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest
> {
@property({ attribute: false })
deviceChallenge?: DeviceChallenge;
@property({ type: Boolean })
showBackButton = false;
static get styles(): CSSResult[] {
return [
PFBase,
PFLogin,
PFForm,
PFFormControl,
PFTitle,
PFButton,
css`
.icon-description {
display: flex;
}
.icon-description i {
font-size: 2em;
padding: 0.25em;
padding-right: 0.5em;
}
`,
];
return super.styles.concat(css`
.icon-description {
display: flex;
}
.icon-description i {
font-size: 2em;
padding: 0.25em;
padding-right: 0.5em;
}
`);
}
render(): TemplateResult {
@ -62,92 +37,62 @@ export class AuthenticatorValidateStageWebCode extends BaseStage<
</ak-empty-state>`;
}
return html`<div class="pf-c-login__main-body">
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
${this.renderUserInfo()}
<div class="icon-description">
<i
class="fa ${this.deviceChallenge?.deviceClass == DeviceClassesEnum.Sms
? "fa-key"
: "fa-mobile-alt"}"
aria-hidden="true"
></i>
${this.deviceChallenge?.deviceClass == DeviceClassesEnum.Sms
? html`<p>${msg("A code has been sent to you via SMS.")}</p>`
: html`<p>
${msg(
"Open your two-factor authenticator app to view your authentication code.",
)}
</p>`}
</div>
<ak-form-element
label="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
? msg("Static token")
: msg("Authentication code")}"
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})["code"]}
>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${msg("Not you?")}</a
>
</div>
</ak-form-static>
<div class="icon-description">
<i
class="fa ${this.deviceChallenge?.deviceClass == DeviceClassesEnum.Sms
? "fa-key"
: "fa-mobile-alt"}"
aria-hidden="true"
></i>
${this.deviceChallenge?.deviceClass == DeviceClassesEnum.Sms
? html`<p>${msg("A code has been sent to you via SMS.")}</p>`
: html`<p>
${msg(
"Open your two-factor authenticator app to view your authentication code.",
)}
</p>`}
</div>
<ak-form-element
label="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
? msg("Static token")
: msg("Authentication code")}"
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})["code"]}
>
<!-- @ts-ignore -->
<input
type="text"
name="code"
inputmode="${this.deviceChallenge?.deviceClass ===
DeviceClassesEnum.Static
? "text"
: "numeric"}"
pattern="${this.deviceChallenge?.deviceClass ===
DeviceClassesEnum.Static
? "[0-9a-zA-Z]*"
: "[0-9]*"}"
placeholder="${msg("Please enter your code")}"
autofocus=""
autocomplete="one-time-code"
class="pf-c-form-control"
value="${PasswordManagerPrefill.totp || ""}"
required
/>
</ak-form-element>
<!-- @ts-ignore -->
<input
type="text"
name="code"
inputmode="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
? "text"
: "numeric"}"
pattern="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
? "[0-9a-zA-Z]*"
: "[0-9]*"}"
placeholder="${msg("Please enter your code")}"
autofocus=""
autocomplete="one-time-code"
class="pf-c-form-control"
value="${PasswordManagerPrefill.totp || ""}"
required
/>
</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">
${msg("Continue")}
</button>
</div>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
${this.showBackButton
? html`<li class="pf-c-login__main-footer-links-item">
<button
class="pf-c-button pf-m-secondary pf-m-block"
@click=${() => {
if (!this.host) return;
(
this.host as AuthenticatorValidateStage
).selectedDeviceChallenge = undefined;
}}
>
${msg("Return to device picker")}
</button>
</li>`
: html``}
</ul>
</footer>`;
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${msg("Continue")}
</button>
${this.renderReturnToDevicePicker()}
</div>
</form>
</div>`;
}
}

View File

@ -1,20 +1,10 @@
import { BaseDeviceStage } from "@goauthentik/app/flow/stages/authenticator_validate/base";
import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/forms/FormElement";
import "@goauthentik/flow/FormStatic";
import { AuthenticatorValidateStage } from "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStage";
import { BaseStage } from "@goauthentik/flow/stages/base";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import {
AuthenticatorValidationChallenge,
@ -23,7 +13,7 @@ import {
} from "@goauthentik/api";
@customElement("ak-stage-authenticator-validate-duo")
export class AuthenticatorValidateStageWebDuo extends BaseStage<
export class AuthenticatorValidateStageWebDuo extends BaseDeviceStage<
AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest
> {
@ -33,14 +23,24 @@ export class AuthenticatorValidateStageWebDuo extends BaseStage<
@property({ type: Boolean })
showBackButton = false;
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton];
}
@state()
authenticating = false;
firstUpdated(): void {
this.host?.submit({
duo: this.deviceChallenge?.deviceUid,
});
this.authenticating = true;
this.host
?.submit(
{
duo: this.deviceChallenge?.deviceUid,
},
{ invisible: true },
)
.then(() => {
this.authenticating = false;
})
.catch(() => {
this.authenticating = false;
});
}
render(): TemplateResult {
@ -49,56 +49,25 @@ export class AuthenticatorValidateStageWebDuo extends BaseStage<
</ak-empty-state>`;
}
const errors = this.challenge.responseErrors?.duo || [];
const errorMessage = errors.map((err) => err.string);
return html`<div class="pf-c-login__main-body">
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
${this.renderUserInfo()}
<ak-empty-state
?loading="${this.authenticating}"
header=${this.authenticating
? msg("Sending Duo push notification...")
: errorMessage.join(", ") || msg("Failed to authenticate")}
icon="fas fa-times"
>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${msg("Not you?")}</a
>
</div>
</ak-form-static>
${errors.length > 0
? errors.map((err) => {
if (err.code === "denied") {
return html` <ak-stage-access-denied-icon
errorMessage=${err.string}
>
</ak-stage-access-denied-icon>`;
}
return html`<p>${err.string}</p>`;
})
: html`${msg("Sending Duo push notification")}`}
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
${this.showBackButton
? html`<li class="pf-c-login__main-footer-links-item">
<button
class="pf-c-button pf-m-secondary pf-m-block"
@click=${() => {
if (!this.host) return;
(
this.host as AuthenticatorValidateStage
).selectedDeviceChallenge = undefined;
}}
>
${msg("Return to device picker")}
</button>
</li>`
: html``}
</ul>
</footer>`;
</ak-empty-state>
<div class="pf-c-form__group pf-m-action">${this.renderReturnToDevicePicker()}</div>
</form>
</div>`;
}
}

View File

@ -1,23 +1,14 @@
import { BaseDeviceStage } from "@goauthentik/app/flow/stages/authenticator_validate/base";
import {
checkWebAuthnSupport,
transformAssertionForServer,
transformCredentialRequestOptions,
} from "@goauthentik/common/helpers/webauthn";
import { AuthenticatorValidateStage } from "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStage";
import { BaseStage } from "@goauthentik/flow/stages/base";
import "@goauthentik/elements/EmptyState";
import { msg, str } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBullseye from "@patternfly/patternfly/layouts/Bullseye/bullseye.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { msg } from "@lit/localize";
import { TemplateResult, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import {
AuthenticatorValidationChallenge,
@ -26,7 +17,7 @@ import {
} from "@goauthentik/api";
@customElement("ak-stage-authenticator-validate-webauthn")
export class AuthenticatorValidateStageWebAuthn extends BaseStage<
export class AuthenticatorValidateStageWebAuthn extends BaseDeviceStage<
AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest
> {
@ -34,25 +25,15 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<
deviceChallenge?: DeviceChallenge;
@property()
authenticateMessage?: string;
errorMessage?: string;
@property({ type: Boolean })
showBackButton = false;
transformedCredentialRequestOptions?: PublicKeyCredentialRequestOptions;
@state()
authenticating = false;
static get styles(): CSSResult[] {
return [
PFBase,
PFLogin,
PFEmptyState,
PFBullseye,
PFForm,
PFFormControl,
PFTitle,
PFButton,
];
}
transformedCredentialRequestOptions?: PublicKeyCredentialRequestOptions;
async authenticate(): Promise<void> {
// request the authenticator to create an assertion signature using the
@ -64,10 +45,10 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<
publicKey: this.transformedCredentialRequestOptions,
});
if (!assertion) {
throw new Error(msg("Assertions is empty"));
throw new Error("Assertions is empty");
}
} catch (err) {
throw new Error(msg(str`Error when creating credential: ${err}`));
throw new Error(`Error when creating credential: ${err}`);
}
// we now have an authentication assertion! encode the byte arrays contained
@ -78,11 +59,16 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<
// post the assertion to the server for verification.
try {
await this.host?.submit({
webauthn: transformedAssertionForServer,
});
await this.host?.submit(
{
webauthn: transformedAssertionForServer,
},
{
invisible: true,
},
);
} catch (err) {
throw new Error(msg(str`Error when validating assertion on server: ${err}`));
throw new Error(`Error when validating assertion on server: ${err}`);
}
}
@ -97,58 +83,46 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<
}
async authenticateWrapper(): Promise<void> {
if (this.host.loading) {
if (this.authenticating) {
return;
}
this.host.loading = true;
this.authenticating = true;
this.authenticate()
.catch((e) => {
console.error(e);
this.authenticateMessage = e.toString();
.catch((e: Error) => {
console.warn(`authentik/flows/authenticator_validate/webauthn: ${e.toString()}`);
this.errorMessage = msg("Authentication failed.");
})
.finally(() => {
this.host.loading = false;
this.authenticating = false;
});
}
render(): TemplateResult {
return html`<div class="pf-c-login__main-body">
${this.authenticateMessage
? html`<div class="pf-c-form__group pf-m-action">
<p class="pf-m-block">${this.authenticateMessage}</p>
<button
<form class="pf-c-form">
${this.renderUserInfo()}
<ak-empty-state
?loading="${this.authenticating}"
header=${this.authenticating
? msg("Authenticating...")
: this.errorMessage || msg("Failed to authenticate")}
icon="fa-times"
>
</ak-empty-state>
<div class="pf-c-form__group pf-m-action">
${this.errorMessage
? html` <button
class="pf-c-button pf-m-primary pf-m-block"
@click=${() => {
this.authenticateWrapper();
}}
>
${msg("Retry authentication")}
</button>
</div>`
: html`<div class="pf-c-form__group pf-m-action">
<p class="pf-m-block">&nbsp;</p>
<p class="pf-m-block">&nbsp;</p>
<p class="pf-m-block">&nbsp;</p>
</div> `}
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
${this.showBackButton
? html`<li class="pf-c-login__main-footer-links-item">
<button
class="pf-c-button pf-m-secondary pf-m-block"
@click=${() => {
if (!this.host) return;
(
this.host as AuthenticatorValidateStage
).selectedDeviceChallenge = undefined;
}}
>
${msg("Return to device picker")}
</button>
</li>`
: html``}
</ul>
</footer>`;
</button>`
: nothing}
${this.renderReturnToDevicePicker()}
</div>
</form>
</div>`;
}
}

View File

@ -0,0 +1,69 @@
import {
BaseStage,
FlowInfoChallenge,
PendingUserChallenge,
} from "@goauthentik/app/flow/stages/base";
import { AuthenticatorValidateStage } from "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStage";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { property } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { DeviceChallenge } from "@goauthentik/api";
export class BaseDeviceStage<
Tin extends FlowInfoChallenge & PendingUserChallenge,
Tout,
> extends BaseStage<Tin, Tout> {
@property({ attribute: false })
deviceChallenge?: DeviceChallenge;
@property({ type: Boolean })
showBackButton = false;
static get styles(): CSSResult[] {
return [
PFBase,
PFLogin,
PFForm,
PFFormControl,
PFTitle,
PFButton,
css`
.pf-c-form__group.pf-m-action {
display: flex;
gap: 16px;
margin-top: 0;
margin-bottom: calc(var(--pf-c-form__group--m-action--MarginTop) / 2);
flex-direction: column;
}
`,
];
}
submit(payload: Tin): Promise<boolean> {
return this.host?.submit(payload) || Promise.resolve();
}
renderReturnToDevicePicker(): TemplateResult {
if (!this.showBackButton) {
return html``;
}
return html`<button
class="pf-c-button pf-m-secondary pf-m-block"
@click=${() => {
if (!this.host) return;
(this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined;
}}
>
${msg("Return to device picker")}
</button>`;
}
}

View File

@ -1,16 +1,22 @@
import { AKElement } from "@goauthentik/elements/Base";
import { KeyUnknown } from "@goauthentik/elements/forms/Form";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { CurrentBrand, ErrorDetail } from "@goauthentik/api";
import { ContextualFlowInfo, CurrentBrand, ErrorDetail } from "@goauthentik/api";
export interface SubmitOptions {
invisible: boolean;
}
export interface StageHost {
challenge?: unknown;
flowSlug?: string;
loading: boolean;
submit(payload: unknown): Promise<boolean>;
submit(payload: unknown, options?: SubmitOptions): Promise<boolean>;
readonly brand?: CurrentBrand;
}
@ -26,7 +32,21 @@ export function readFileAsync(file: Blob) {
});
}
export class BaseStage<Tin, Tout> extends AKElement {
// Challenge which contains flow info
export interface FlowInfoChallenge {
flowInfo?: ContextualFlowInfo;
}
// Challenge which has a pending user
export interface PendingUserChallenge {
pendingUser?: string;
pendingUserAvatar?: string;
}
export class BaseStage<
Tin extends FlowInfoChallenge & PendingUserChallenge,
Tout,
> extends AKElement {
host!: StageHost;
@property({ attribute: false })
@ -68,6 +88,31 @@ export class BaseStage<Tin, Tout> extends AKElement {
</div>`;
}
renderUserInfo(): TemplateResult {
if (!this.challenge.pendingUser || !this.challenge.pendingUserAvatar) {
return html``;
}
return html`
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${msg("Not you?")}</a
>
</div>
</ak-form-static>
<input
name="username"
autocomplete="username"
type="hidden"
value="${this.challenge.pendingUser}"
/>
`;
}
cleanup(): void {
// Method that can be overridden by stages
return;