mirror of
https://github.com/goauthentik/authentik
synced 2026-04-30 03:17:46 +02:00
core: customisable user settings (#2397)
* tenants: add user_settings flow, add basic flow and basic new executor Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/user: use flow PromptStage instead of custom stage Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/flows: add tenant to StageHost interface Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/user: fix form missing component Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/user: re-add success message Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/user: improve support for multiple error messages Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * stages/prompt: allow expressions in prompt placeholders Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * stages/prompt: add tests Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * flows: always set pending user Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * flows: never cache stage configuration flow plans Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * stages/user_write: fix error when pending user is anonymous user Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/admin: add checkbox for prompt placeholder expression Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * website/docs: add prompt expression docs Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * stages/prompt: add ak-locale field type Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * tenants: fix default policy Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/user: add function to do global refresh Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/flows: fix rendering of ak-locale Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * tenants: fix default policy, add error handling to placeholder, fix locale attribute Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add tests Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
231
web/src/user/user-settings/details/UserSettingsFlowExecutor.ts
Normal file
231
web/src/user/user-settings/details/UserSettingsFlowExecutor.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import { t } from "@lingui/macro";
|
||||
|
||||
import { CSSResult, LitElement, TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
|
||||
import AKGlobal from "../../../authentik.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import {
|
||||
ChallengeChoices,
|
||||
ChallengeTypes,
|
||||
CurrentTenant,
|
||||
FlowChallengeResponseRequest,
|
||||
FlowsApi,
|
||||
RedirectChallenge,
|
||||
ShellChallenge,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { DEFAULT_CONFIG, tenant } from "../../../api/Config";
|
||||
import { refreshMe } from "../../../api/Users";
|
||||
import { EVENT_REFRESH } from "../../../constants";
|
||||
import "../../../elements/LoadingOverlay";
|
||||
import { MessageLevel } from "../../../elements/messages/Message";
|
||||
import { showMessage } from "../../../elements/messages/MessageContainer";
|
||||
import { StageHost } from "../../../flows/stages/base";
|
||||
import "./stages/prompt/PromptStage";
|
||||
|
||||
@customElement("ak-user-settings-flow-executor")
|
||||
export class UserSettingsFlowExecutor extends LitElement implements StageHost {
|
||||
@property()
|
||||
flowSlug?: string;
|
||||
|
||||
private _challenge?: ChallengeTypes;
|
||||
|
||||
@property({ attribute: false })
|
||||
set challenge(value: ChallengeTypes | undefined) {
|
||||
this._challenge = value;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
get challenge(): ChallengeTypes | undefined {
|
||||
return this._challenge;
|
||||
}
|
||||
|
||||
@property({ type: Boolean })
|
||||
loading = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
tenant!: CurrentTenant;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFCard, PFPage, PFButton, PFContent, AKGlobal];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
tenant().then((tenant) => (this.tenant = tenant));
|
||||
}
|
||||
|
||||
submit(payload?: FlowChallengeResponseRequest): Promise<boolean> {
|
||||
if (!payload) return Promise.reject();
|
||||
if (!this.challenge) return Promise.reject();
|
||||
// @ts-ignore
|
||||
payload.component = this.challenge.component;
|
||||
this.loading = true;
|
||||
return new FlowsApi(DEFAULT_CONFIG)
|
||||
.flowsExecutorSolve({
|
||||
flowSlug: this.flowSlug || "",
|
||||
query: window.location.search.substring(1),
|
||||
flowChallengeResponseRequest: payload,
|
||||
})
|
||||
.then((data) => {
|
||||
showMessage({
|
||||
level: MessageLevel.success,
|
||||
message: t`Successfully updated details`,
|
||||
});
|
||||
this.challenge = data;
|
||||
if (this.challenge.responseErrors) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.catch((e: Error | Response) => {
|
||||
this.errorMessage(e);
|
||||
return false;
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
firstUpdated(): void {
|
||||
tenant().then((tenant) => {
|
||||
this.flowSlug = tenant.flowUserSettings;
|
||||
if (!this.flowSlug) {
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
new FlowsApi(DEFAULT_CONFIG)
|
||||
.flowsExecutorGet({
|
||||
flowSlug: this.flowSlug,
|
||||
query: window.location.search.substring(1),
|
||||
})
|
||||
.then((challenge) => {
|
||||
this.challenge = challenge;
|
||||
})
|
||||
.catch((e: Error | Response) => {
|
||||
// Catch JSON or Update errors
|
||||
this.errorMessage(e);
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async errorMessage(error: Error | Response): Promise<void> {
|
||||
let body = "";
|
||||
if (error instanceof Error) {
|
||||
body = error.message;
|
||||
}
|
||||
this.challenge = {
|
||||
type: ChallengeChoices.Shell,
|
||||
body: `<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
${t`Whoops!`}
|
||||
</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<h3>${t`Something went wrong! Please try again later.`}</h3>
|
||||
<pre class="ak-exception">${body}</pre>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links">
|
||||
<li class="pf-c-login__main-footer-links-item">
|
||||
<a class="pf-c-button pf-m-primary pf-m-block" href="/">
|
||||
${t`Return`}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</footer>`,
|
||||
} as ChallengeTypes;
|
||||
}
|
||||
|
||||
globalRefresh(): void {
|
||||
refreshMe().then(() => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
try {
|
||||
document.querySelectorAll("ak-interface-user").forEach((int) => {
|
||||
(int as LitElement).requestUpdate();
|
||||
});
|
||||
} catch {
|
||||
console.debug("authentik/user/flows: failed to find interface to refresh");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderChallenge(): TemplateResult {
|
||||
if (!this.challenge) {
|
||||
return html``;
|
||||
}
|
||||
switch (this.challenge.type) {
|
||||
case ChallengeChoices.Redirect:
|
||||
if ((this.challenge as RedirectChallenge).to !== "/") {
|
||||
return html`<a
|
||||
href="${(this.challenge as RedirectChallenge).to}"
|
||||
class="pf-c-button pf-m-primary"
|
||||
>${"Edit settings"}</a
|
||||
>`;
|
||||
}
|
||||
// Flow has finished, so let's load while in the background we can restart the flow
|
||||
this.loading = true;
|
||||
console.debug("authentik/user/flows: redirect to '/', restarting flow.");
|
||||
this.firstUpdated();
|
||||
this.globalRefresh();
|
||||
return html``;
|
||||
case ChallengeChoices.Shell:
|
||||
return html`${unsafeHTML((this.challenge as ShellChallenge).body)}`;
|
||||
case ChallengeChoices.Native:
|
||||
switch (this.challenge.component) {
|
||||
case "ak-stage-prompt":
|
||||
return html`<ak-user-stage-prompt
|
||||
.host=${this as StageHost}
|
||||
.challenge=${this.challenge}
|
||||
></ak-user-stage-prompt>`;
|
||||
default:
|
||||
console.log(
|
||||
`authentik/user/flows: unsupported stage type ${this.challenge.component}`,
|
||||
);
|
||||
return html`
|
||||
<a href="/if/flow/${this.flowSlug}" class="pf-c-button pf-m-primary">
|
||||
${t`Open settings`}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
default:
|
||||
console.debug(`authentik/user/flows: unexpected data type ${this.challenge.type}`);
|
||||
break;
|
||||
}
|
||||
return html``;
|
||||
}
|
||||
|
||||
renderChallengeWrapper(): TemplateResult {
|
||||
if (!this.flowSlug) {
|
||||
return html`<p>${t`No settings flow configured.`}</p> `;
|
||||
}
|
||||
if (!this.challenge) {
|
||||
return html`<ak-empty-state ?loading=${true} header=${t`Loading`}> </ak-empty-state>`;
|
||||
}
|
||||
return html` ${this.renderChallenge()} `;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`${this.loading ? html`<ak-loading-overlay></ak-loading-overlay>` : html``}
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__title">${t`Update details`}</div>
|
||||
<div class="pf-c-card__body">${this.renderChallengeWrapper()}</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user