From 504338ea66dfe7045bbe9cd8ad1619e9a6536944 Mon Sep 17 00:00:00 2001 From: Jens L Date: Sun, 26 Jun 2022 00:46:40 +0200 Subject: [PATCH] web/admin: application wizard (part 1) (#2745) * initial Signed-off-by: Jens Langhammer * remove log Signed-off-by: Jens Langhammer * start oauth Signed-off-by: Jens Langhammer * use form for all type wizard pages Signed-off-by: Jens Langhammer * more oauth Signed-off-by: Jens Langhammer * basic wizard actions Signed-off-by: Jens Langhammer * make resets work Signed-off-by: Jens Langhammer * add hint in provider wizard Signed-off-by: Jens Langhammer * render correct icon in empty state in table page Signed-off-by: Jens Langhammer * improve empty state Signed-off-by: Jens Langhammer * more Signed-off-by: Jens Langhammer * add more pages Signed-off-by: Jens Langhammer * fix Signed-off-by: Jens Langhammer * add group PK to service account creation response Signed-off-by: Jens Langhammer * use wizard-level isValid prop Signed-off-by: Jens Langhammer * re-add old buttons Signed-off-by: Jens Langhammer --- Makefile | 3 + authentik/core/api/applications.py | 8 + authentik/core/api/users.py | 20 ++- schema.yml | 8 + web/src/common/ws.ts | 2 +- web/src/elements/forms/Form.ts | 36 ++-- web/src/elements/wizard/ActionWizardPage.ts | 147 ++++++++++++++++ web/src/elements/wizard/FormWizardPage.ts | 6 - web/src/elements/wizard/Wizard.ts | 166 +++++++++++++----- web/src/elements/wizard/WizardFormPage.ts | 85 +++++++++ web/src/elements/wizard/WizardPage.ts | 17 +- web/src/pages/applications/ApplicationForm.ts | 2 +- .../pages/applications/ApplicationListPage.ts | 37 ++-- .../applications/wizard/ApplicationWizard.ts | 65 +++++++ .../wizard/InitialApplicationWizardPage.ts | 73 ++++++++ .../wizard/TypeApplicationWizardPage.ts | 81 +++++++++ .../ldap/TypeLDAPApplicationWizardPage.ts | 74 ++++++++ .../link/TypeLinkApplicationWizardPage.ts | 31 ++++ .../TypeOAuthAPIApplicationWizardPage.ts | 33 ++++ .../oauth/TypeOAuthApplicationWizardPage.ts | 78 ++++++++ .../TypeOAuthCodeApplicationWizardPage.ts | 72 ++++++++ .../TypeOAuthImplicitApplicationWizardPage.ts | 16 ++ .../proxy/TypeProxyApplicationWizardPage.ts | 65 +++++++ .../saml/TypeSAMLApplicationWizardPage.ts | 66 +++++++ .../TypeSAMLConfigApplicationWizardPage.ts | 56 ++++++ .../TypeSAMLImportApplicationWizardPage.ts | 58 ++++++ web/src/pages/flows/FlowForm.ts | 2 +- web/src/pages/flows/FlowImportForm.ts | 2 +- .../pages/outposts/ServiceConnectionWizard.ts | 6 +- web/src/pages/policies/PolicyWizard.ts | 11 +- .../PropertyMappingWizard.ts | 6 +- web/src/pages/providers/ProviderWizard.ts | 62 ++++--- .../providers/saml/SAMLProviderImportForm.ts | 4 +- web/src/pages/sources/SourceWizard.ts | 11 +- web/src/pages/stages/StageWizard.ts | 11 +- .../details/UserSettingsFlowExecutor.ts | 2 +- 36 files changed, 1275 insertions(+), 147 deletions(-) create mode 100644 web/src/elements/wizard/ActionWizardPage.ts create mode 100644 web/src/elements/wizard/WizardFormPage.ts create mode 100644 web/src/pages/applications/wizard/ApplicationWizard.ts create mode 100644 web/src/pages/applications/wizard/InitialApplicationWizardPage.ts create mode 100644 web/src/pages/applications/wizard/TypeApplicationWizardPage.ts create mode 100644 web/src/pages/applications/wizard/ldap/TypeLDAPApplicationWizardPage.ts create mode 100644 web/src/pages/applications/wizard/link/TypeLinkApplicationWizardPage.ts create mode 100644 web/src/pages/applications/wizard/oauth/TypeOAuthAPIApplicationWizardPage.ts create mode 100644 web/src/pages/applications/wizard/oauth/TypeOAuthApplicationWizardPage.ts create mode 100644 web/src/pages/applications/wizard/oauth/TypeOAuthCodeApplicationWizardPage.ts create mode 100644 web/src/pages/applications/wizard/oauth/TypeOAuthImplicitApplicationWizardPage.ts create mode 100644 web/src/pages/applications/wizard/proxy/TypeProxyApplicationWizardPage.ts create mode 100644 web/src/pages/applications/wizard/saml/TypeSAMLApplicationWizardPage.ts create mode 100644 web/src/pages/applications/wizard/saml/TypeSAMLConfigApplicationWizardPage.ts create mode 100644 web/src/pages/applications/wizard/saml/TypeSAMLImportApplicationWizardPage.ts diff --git a/Makefile b/Makefile index 9066c92fa..3cb2feaba 100644 --- a/Makefile +++ b/Makefile @@ -112,6 +112,9 @@ web-install: cd web && npm ci web-watch: + rm -rf web/dist/ + mkdir web/dist/ + touch web/dist/.gitkeep cd web && npm run watch web-lint-fix: diff --git a/authentik/core/api/applications.py b/authentik/core/api/applications.py index 3ff126154..1d7869a9f 100644 --- a/authentik/core/api/applications.py +++ b/authentik/core/api/applications.py @@ -89,6 +89,14 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet): "meta_publisher", "group", ] + filterset_fields = [ + "name", + "slug", + "meta_launch_url", + "meta_description", + "meta_publisher", + "group", + ] lookup_field = "slug" filterset_fields = ["name", "slug"] ordering = ["name"] diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index 723696546..62fe17ef8 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -24,7 +24,13 @@ from drf_spectacular.utils import ( ) from guardian.shortcuts import get_anonymous_user, get_objects_for_user from rest_framework.decorators import action -from rest_framework.fields import CharField, JSONField, ListField, SerializerMethodField +from rest_framework.fields import ( + CharField, + IntegerField, + JSONField, + ListField, + SerializerMethodField, +) from rest_framework.request import Request from rest_framework.response import Response from rest_framework.serializers import ( @@ -315,6 +321,9 @@ class UserViewSet(UsedByMixin, ModelViewSet): { "username": CharField(required=True), "token": CharField(required=True), + "user_uid": CharField(required=True), + "user_pk": IntegerField(required=True), + "group_pk": CharField(required=False), }, ) }, @@ -332,18 +341,25 @@ class UserViewSet(UsedByMixin, ModelViewSet): attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: False}, path=USER_PATH_SERVICE_ACCOUNT, ) + response = { + "username": user.username, + "user_uid": user.uid, + "user_pk": user.pk, + } if create_group and self.request.user.has_perm("authentik_core.add_group"): group = Group.objects.create( name=username, ) group.users.add(user) + response["group_pk"] = str(group.pk) token = Token.objects.create( identifier=slugify(f"service-account-{username}-password"), intent=TokenIntents.INTENT_APP_PASSWORD, user=user, expires=now() + timedelta(days=360), ) - return Response({"username": user.username, "token": token.key}) + response["token"] = token.key + return Response(response) except (IntegrityError) as exc: return Response(data={"non_field_errors": [str(exc)]}, status=400) diff --git a/schema.yml b/schema.yml index 40d4cb5cc..605369ced 100644 --- a/schema.yml +++ b/schema.yml @@ -31589,8 +31589,16 @@ components: type: string token: type: string + user_uid: + type: string + user_pk: + type: integer + group_pk: + type: string required: - token + - user_pk + - user_uid - username UserSetting: type: object diff --git a/web/src/common/ws.ts b/web/src/common/ws.ts index 98e011ce4..893f16cc4 100644 --- a/web/src/common/ws.ts +++ b/web/src/common/ws.ts @@ -32,7 +32,7 @@ export class WebsocketClient { }); this.messageSocket.addEventListener("close", (e) => { console.debug(`authentik/ws: closed ws connection: ${e}`); - if (this.retryDelay > 3000) { + if (this.retryDelay > 6000) { showMessage( { level: MessageLevel.error, diff --git a/web/src/elements/forms/Form.ts b/web/src/elements/forms/Form.ts index c67a72364..78f1eccaa 100644 --- a/web/src/elements/forms/Form.ts +++ b/web/src/elements/forms/Form.ts @@ -29,6 +29,10 @@ export class APIError extends Error { } } +export interface KeyUnknown { + [key: string]: unknown; +} + @customElement("ak-form") export class Form extends LitElement { viewportCheck = true; @@ -101,15 +105,11 @@ export class Form extends LitElement { ironForm?.reset(); } - /** - * If this form contains a file input, and the input as been filled, this function returns - * said file. - * @returns File object or undefined - */ - getFormFile(): File | undefined { + getFormFiles(): { [key: string]: File } { const ironForm = this.shadowRoot?.querySelector("iron-form"); + const files: { [key: string]: File } = {}; if (!ironForm) { - return; + return files; } const elements = ironForm._getSubmittableElements(); for (let i = 0; i < elements.length; i++) { @@ -118,13 +118,18 @@ export class Form extends LitElement { if ((element.files || []).length < 1) { continue; } - // We already checked the length - return (element.files || [])[0]; + files[element.name] = (element.files || [])[0]; } } + return files; } - serializeForm(form: IronFormElement): T { + serializeForm(): T | undefined { + const form = this.shadowRoot?.querySelector("iron-form"); + if (!form) { + console.warn("authentik/forms: failed to find iron-form"); + return; + } const elements: HTMLInputElement[] = form._getSubmittableElements(); const json: { [key: string]: unknown } = {}; elements.forEach((element) => { @@ -189,12 +194,15 @@ export class Form extends LitElement { submit(ev: Event): Promise | undefined { ev.preventDefault(); - const ironForm = this.shadowRoot?.querySelector("iron-form"); - if (!ironForm) { + const data = this.serializeForm(); + if (!data) { + return; + } + const form = this.shadowRoot?.querySelector("iron-form"); + if (!form) { console.warn("authentik/forms: failed to find iron-form"); return; } - const data = this.serializeForm(ironForm); return this.send(data) .then((r) => { showMessage({ @@ -221,7 +229,7 @@ export class Form extends LitElement { throw errorMessage; } // assign all input-related errors to their elements - const elements: HorizontalFormElement[] = ironForm._getSubmittableElements(); + const elements: HorizontalFormElement[] = form._getSubmittableElements(); elements.forEach((element) => { const elementName = element.name; if (!elementName) return; diff --git a/web/src/elements/wizard/ActionWizardPage.ts b/web/src/elements/wizard/ActionWizardPage.ts new file mode 100644 index 000000000..dfeeae051 --- /dev/null +++ b/web/src/elements/wizard/ActionWizardPage.ts @@ -0,0 +1,147 @@ +import { t } from "@lingui/macro"; + +import { CSSResult, TemplateResult, html } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +import AKGlobal from "../../authentik.css"; +import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css"; +import PFProgressStepper from "@patternfly/patternfly/components/ProgressStepper/progress-stepper.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 { ResponseError } from "@goauthentik/api"; + +import { EVENT_REFRESH } from "../../constants"; +import { WizardAction } from "./Wizard"; +import { WizardPage } from "./WizardPage"; + +export enum ActionState { + pending = "pending", + running = "running", + done = "done", + failed = "failed", +} + +export interface ActionStateBundle { + action: WizardAction; + state: ActionState; + idx: number; +} + +@customElement("ak-wizard-page-action") +export class ActionWizardPage extends WizardPage { + static get styles(): CSSResult[] { + return [PFBase, PFBullseye, PFEmptyState, PFTitle, PFProgressStepper, AKGlobal]; + } + + @property({ attribute: false }) + states: ActionStateBundle[] = []; + + @property({ attribute: false }) + currentStep?: ActionStateBundle; + + activeCallback = async (): Promise => { + this.states = []; + this.host.actions.map((act, idx) => { + this.states.push({ + action: act, + state: ActionState.pending, + idx: idx, + }); + }); + this.host.canBack = false; + this.host.canCancel = false; + await this.run(); + // Ensure wizard is closable, even when run() failed + this.host.isValid = true; + }; + + sidebarLabel = () => t`Apply changes`; + + async run(): Promise { + this.currentStep = this.states[0]; + await new Promise((r) => setTimeout(r, 500)); + for await (const bundle of this.states) { + this.currentStep = bundle; + this.currentStep.state = ActionState.running; + this.requestUpdate(); + try { + await bundle.action.run(); + await new Promise((r) => setTimeout(r, 500)); + this.currentStep.state = ActionState.done; + this.requestUpdate(); + } catch (exc) { + if (exc instanceof ResponseError) { + this.currentStep.action.subText = await exc.response.text(); + } else { + this.currentStep.action.subText = (exc as Error).toString(); + } + this.currentStep.state = ActionState.failed; + this.requestUpdate(); + return; + } + } + this.host.isValid = true; + this.dispatchEvent( + new CustomEvent(EVENT_REFRESH, { + bubbles: true, + composed: true, + }), + ); + } + + render(): TemplateResult { + return html`
+
+
+ +

${this.currentStep?.action.displayName}

+
+
    + ${this.states.map((state) => { + let cls = ""; + switch (state.state) { + case ActionState.pending: + cls = "pf-m-pending"; + break; + case ActionState.done: + cls = "pf-m-success"; + break; + case ActionState.running: + cls = "pf-m-info"; + break; + case ActionState.failed: + cls = "pf-m-danger"; + break; + } + if (state.idx === this.currentStep?.idx) { + cls += " pf-m-current"; + } + return html`
  1. +
    + + + +
    +
    +
    + ${state.action.displayName} +
    + ${state.action.subText + ? html`
    + ${state.action.subText} +
    ` + : html``} +
    +
  2. `; + })} +
+
+
+
+
`; + } +} diff --git a/web/src/elements/wizard/FormWizardPage.ts b/web/src/elements/wizard/FormWizardPage.ts index b4a92468a..ef3544178 100644 --- a/web/src/elements/wizard/FormWizardPage.ts +++ b/web/src/elements/wizard/FormWizardPage.ts @@ -8,12 +8,6 @@ import { WizardPage } from "./WizardPage"; @customElement("ak-wizard-page-form") export class FormWizardPage extends WizardPage { - _isValid = true; - - isValid(): boolean { - return this._isValid; - } - nextCallback = async () => { const form = this.querySelector>("*"); if (!form) { diff --git a/web/src/elements/wizard/Wizard.ts b/web/src/elements/wizard/Wizard.ts index d1df155c7..b15c46e01 100644 --- a/web/src/elements/wizard/Wizard.ts +++ b/web/src/elements/wizard/Wizard.ts @@ -1,4 +1,6 @@ import { ModalButton } from "@goauthentik/web/elements/buttons/ModalButton"; +import "@goauthentik/web/elements/wizard/ActionWizardPage"; +import { WizardPage } from "@goauthentik/web/elements/wizard/WizardPage"; import { t } from "@lingui/macro"; @@ -9,22 +11,64 @@ import { state } from "lit/decorators.js"; import PFWizard from "@patternfly/patternfly/components/Wizard/wizard.css"; -import { WizardPage } from "./WizardPage"; +export interface WizardAction { + displayName: string; + subText?: string; + run: () => Promise; +} + +export const ApplyActionsSlot = "apply-actions"; @customElement("ak-wizard") export class Wizard extends ModalButton { + @property({ type: Boolean }) + canCancel = true; + + @property({ type: Boolean }) + canBack = true; + @property() header?: string; @property() description?: string; + @property({ type: Boolean }) + isValid = false; + static get styles(): CSSResult[] { return super.styles.concat(PFWizard); } + @state() + _steps: string[] = []; + + get steps(): string[] { + return this._steps; + } + + set steps(steps: string[]) { + const addApplyActionsSlot = this.steps.includes(ApplyActionsSlot); + this._steps = steps; + if (addApplyActionsSlot) { + this.steps.push(ApplyActionsSlot); + } + this.steps.forEach((step) => { + const exists = this.querySelector(`[slot=${step}]`) !== null; + if (!exists) { + const el = document.createElement(step); + el.slot = step; + el.dataset["wizardmanaged"] = "true"; + this.appendChild(el); + } + }); + this.requestUpdate(); + } + + _initialSteps: string[] = []; + @property({ attribute: false }) - steps: string[] = []; + actions: WizardAction[] = []; @state() _currentStep?: WizardPage; @@ -41,39 +85,63 @@ export class Wizard extends ModalButton { return this._currentStep; } - setSteps(...steps: string[]): void { - this.steps = steps; - this.requestUpdate(); - } - @property({ attribute: false }) finalHandler: () => Promise = () => { return Promise.resolve(); }; + @property({ attribute: false }) + state: { [key: string]: unknown } = {}; + + firstUpdated(): void { + this._initialSteps = this._steps; + } + + /** + * Add action to the beginning of the list + */ + addActionBefore(displayName: string, run: () => Promise): void { + this.actions.unshift({ + displayName, + run, + }); + } + + /** + * Add action at the end of the list + */ + addActionAfter(displayName: string, run: () => Promise): void { + this.actions.push({ + displayName, + run, + }); + } + renderModalInner(): TemplateResult { const firstPage = this.querySelector(`[slot=${this.steps[0]}]`); if (!this.currentStep && firstPage) { this.currentStep = firstPage; } - this.currentStep?.requestUpdate(); const currentIndex = this.currentStep ? this.steps.indexOf(this.currentStep.slot) : 0; + let lastPage = currentIndex === this.steps.length - 1; + if (lastPage && !this.steps.includes("ak-wizard-page-action") && this.actions.length > 0) { + this.steps = this.steps.concat("ak-wizard-page-action"); + lastPage = currentIndex === this.steps.length - 1; + } return html`
- + ${this.canCancel + ? html`` + : html``}

${this.header}

${this.description}

@@ -120,15 +188,15 @@ export class Wizard extends ModalButton { - ${(this.currentStep ? this.steps.indexOf(this.currentStep.slot) : 0) > 0 + ${(this.currentStep ? this.steps.indexOf(this.currentStep.slot) : 0) > 0 && + this.canBack ? html` -
+ ${this.canCancel + ? html`` + : html``} `; } + + reset(): void { + this.open = false; + this.querySelectorAll("[data-wizardmanaged=true]").forEach((el) => { + el.remove(); + }); + this.steps = this._initialSteps; + this.actions = []; + this.state = {}; + this.currentStep = undefined; + this.canBack = true; + this.canCancel = true; + } } diff --git a/web/src/elements/wizard/WizardFormPage.ts b/web/src/elements/wizard/WizardFormPage.ts new file mode 100644 index 000000000..5da35439d --- /dev/null +++ b/web/src/elements/wizard/WizardFormPage.ts @@ -0,0 +1,85 @@ +import { CSSResult, TemplateResult, html } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +import AKGlobal from "../../authentik.css"; +import PFAlert from "@patternfly/patternfly/components/Alert/alert.css"; +import PFButton from "@patternfly/patternfly/components/Button/button.css"; +import PFCard from "@patternfly/patternfly/components/Card/card.css"; +import PFForm from "@patternfly/patternfly/components/Form/form.css"; +import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; +import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; + +import { Form, KeyUnknown } from "../forms/Form"; +import { WizardPage } from "./WizardPage"; + +@customElement("ak-wizard-form") +export class WizardForm extends Form { + viewportCheck = false; + + @property({ attribute: false }) + nextDataCallback!: (data: KeyUnknown) => Promise; + + submit(): Promise | undefined { + const data = this.serializeForm(); + if (!data) { + return; + } + const files = this.getFormFiles(); + const finalData = Object.assign({}, data, files); + return this.nextDataCallback(finalData); + } +} + +export class WizardFormPage extends WizardPage { + static get styles(): CSSResult[] { + return [PFBase, PFCard, PFButton, PFForm, PFAlert, PFInputGroup, PFFormControl, AKGlobal]; + } + + inputCallback(): void { + const form = this.shadowRoot?.querySelector("form"); + if (!form) { + return; + } + const state = form.checkValidity(); + this.host.isValid = state; + } + + nextCallback = async (): Promise => { + const form = this.shadowRoot?.querySelector("ak-wizard-form"); + if (!form) { + console.warn("authentik/wizard: could not find form element"); + return false; + } + const response = await form.submit(); + if (response === undefined) { + return false; + } + return response; + }; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + nextDataCallback: (data: KeyUnknown) => Promise = async (data): Promise => { + return false; + }; + + renderForm(): TemplateResult { + return html``; + } + + firstUpdated(): void { + this.inputCallback(); + this.host.isValid = false; + } + + render(): TemplateResult { + return html` + this.inputCallback()} + > + ${this.renderForm()} + + `; + } +} diff --git a/web/src/elements/wizard/WizardPage.ts b/web/src/elements/wizard/WizardPage.ts index 2f07ff6df..ef437517e 100644 --- a/web/src/elements/wizard/WizardPage.ts +++ b/web/src/elements/wizard/WizardPage.ts @@ -1,28 +1,31 @@ -import { LitElement, PropertyDeclaration, TemplateResult, html } from "lit"; +import { CSSResult, LitElement, PropertyDeclaration, TemplateResult, html } from "lit"; import { customElement, property } from "lit/decorators.js"; +import AKGlobal from "../../authentik.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; + import { Wizard } from "./Wizard"; @customElement("ak-wizard-page") export class WizardPage extends LitElement { + static get styles(): CSSResult[] { + return [PFBase, AKGlobal]; + } + @property() sidebarLabel: () => string = () => { return "UNNAMED"; }; - isValid(): boolean { - return this._isValid; - } - get host(): Wizard { return this.parentElement as Wizard; } - _isValid = false; - activeCallback: () => Promise = () => { + this.host.isValid = false; return Promise.resolve(); }; + nextCallback: () => Promise = async () => { return true; }; diff --git a/web/src/pages/applications/ApplicationForm.ts b/web/src/pages/applications/ApplicationForm.ts index 11b71db53..3d9e15d5b 100644 --- a/web/src/pages/applications/ApplicationForm.ts +++ b/web/src/pages/applications/ApplicationForm.ts @@ -59,7 +59,7 @@ export class ApplicationForm extends ModelForm { } const c = await config(); if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) { - const icon = this.getFormFile(); + const icon = this.getFormFiles()["metaIcon"]; if (icon || this.clearIcon) { await new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconCreate({ slug: app.slug, diff --git a/web/src/pages/applications/ApplicationListPage.ts b/web/src/pages/applications/ApplicationListPage.ts index a7c9776a3..d7ae543e7 100644 --- a/web/src/pages/applications/ApplicationListPage.ts +++ b/web/src/pages/applications/ApplicationListPage.ts @@ -10,6 +10,7 @@ import { getURLParam } from "@goauthentik/web/elements/router/RouteMatch"; import { TableColumn } from "@goauthentik/web/elements/table/Table"; import { TablePage } from "@goauthentik/web/elements/table/TablePage"; import "@goauthentik/web/pages/applications/ApplicationForm"; +import "@goauthentik/web/pages/applications/wizard/ApplicationWizard"; import { t } from "@lingui/macro"; @@ -81,14 +82,20 @@ export class ApplicationListPage extends TablePage { } renderSidebarAfter(): TemplateResult { - return html`
-
-
${t`About applications`}
-
- + // Rendering the wizard with .open here, as if we set the attribute in + // renderObjectCreate() it'll open two wizards, since that function gets called twice + return html` +
+
+
${t`About applications`}
+
+ +
-
-
`; +
`; } renderToolbarSelected(): TemplateResult { @@ -142,7 +149,7 @@ export class ApplicationListPage extends TablePage { ` : html`-`, html`${item.providerObj?.verboseName || t`-`}`, - html` + html` ${t`Update`} ${t`Update Application`} @@ -160,13 +167,11 @@ export class ApplicationListPage extends TablePage { } renderObjectCreate(): TemplateResult { - return html` - - ${t`Create`} - ${t`Create Application`} - - - - `; + return html` + ${t`Create`} + ${t`Create Application`} + + + `; } } diff --git a/web/src/pages/applications/wizard/ApplicationWizard.ts b/web/src/pages/applications/wizard/ApplicationWizard.ts new file mode 100644 index 000000000..02d3c3226 --- /dev/null +++ b/web/src/pages/applications/wizard/ApplicationWizard.ts @@ -0,0 +1,65 @@ +import { t } from "@lingui/macro"; + +import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; +import { CSSResult, LitElement, TemplateResult, html } from "lit"; +import { property } from "lit/decorators.js"; + +import AKGlobal from "../../../authentik.css"; +import PFButton from "@patternfly/patternfly/components/Button/button.css"; +import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; + +import "../../../elements/wizard/Wizard"; +import "./InitialApplicationWizardPage"; +import "./TypeApplicationWizardPage"; +import "./ldap/TypeLDAPApplicationWizardPage"; +import "./link/TypeLinkApplicationWizardPage"; +import "./oauth/TypeOAuthAPIApplicationWizardPage"; +import "./oauth/TypeOAuthApplicationWizardPage"; +import "./oauth/TypeOAuthCodeApplicationWizardPage"; +import "./oauth/TypeOAuthImplicitApplicationWizardPage"; +import "./proxy/TypeProxyApplicationWizardPage"; +import "./saml/TypeSAMLApplicationWizardPage"; +import "./saml/TypeSAMLConfigApplicationWizardPage"; +import "./saml/TypeSAMLImportApplicationWizardPage"; + +@customElement("ak-application-wizard") +export class ApplicationWizard extends LitElement { + static get styles(): CSSResult[] { + return [PFBase, PFButton, AKGlobal, PFRadio]; + } + + @property({ type: Boolean }) + open = false; + + @property() + createText = t`Create`; + + @property({ type: Boolean }) + showButton = true; + + @property({ attribute: false }) + finalHandler: () => Promise = () => { + return Promise.resolve(); + }; + + render(): TemplateResult { + return html` + { + return this.finalHandler(); + }} + > + ${this.showButton + ? html`` + : html``} + + `; + } +} diff --git a/web/src/pages/applications/wizard/InitialApplicationWizardPage.ts b/web/src/pages/applications/wizard/InitialApplicationWizardPage.ts new file mode 100644 index 000000000..84cc16b8c --- /dev/null +++ b/web/src/pages/applications/wizard/InitialApplicationWizardPage.ts @@ -0,0 +1,73 @@ +import { t } from "@lingui/macro"; + +import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; +import { TemplateResult, html } from "lit"; + +import { ApplicationRequest, CoreApi, Provider } from "@goauthentik/api"; + +import { DEFAULT_CONFIG } from "../../../api/Config"; +import { KeyUnknown } from "../../../elements/forms/Form"; +import "../../../elements/forms/FormGroup"; +import "../../../elements/forms/HorizontalFormElement"; +import { WizardFormPage } from "../../../elements/wizard/WizardFormPage"; +import { convertToSlug } from "../../../utils"; + +@customElement("ak-application-wizard-initial") +export class InitialApplicationWizardPage extends WizardFormPage { + sidebarLabel = () => t`Application details`; + + nextDataCallback = async (data: KeyUnknown): Promise => { + const name = data.name as string; + let slug = convertToSlug(name || ""); + // Check if an application with the generated slug already exists + const apps = await new CoreApi(DEFAULT_CONFIG).coreApplicationsList({ + search: slug, + }); + if (apps.results.filter((app) => app.slug == slug)) { + slug += "-1"; + } + this.host.state["slug"] = slug; + this.host.state["name"] = name; + this.host.addActionBefore(t`Create application`, async (): Promise => { + const req: ApplicationRequest = { + name: name || "", + slug: slug, + metaPublisher: data.metaPublisher as string, + metaDescription: data.metaDescription as string, + }; + if ("provider" in this.host.state) { + req.provider = (this.host.state["provider"] as Provider).pk; + } + if ("link" in this.host.state) { + req.metaLaunchUrl = this.host.state["link"] as string; + } + this.host.state["app"] = await new CoreApi(DEFAULT_CONFIG).coreApplicationsCreate({ + applicationRequest: req, + }); + return true; + }); + return true; + }; + + renderForm(): TemplateResult { + return html` +
+ + +

${t`Application's display Name.`}

+
+ + ${t`Additional UI settings`} +
+ + + + + + +
+
+
+ `; + } +} diff --git a/web/src/pages/applications/wizard/TypeApplicationWizardPage.ts b/web/src/pages/applications/wizard/TypeApplicationWizardPage.ts new file mode 100644 index 000000000..650fc960b --- /dev/null +++ b/web/src/pages/applications/wizard/TypeApplicationWizardPage.ts @@ -0,0 +1,81 @@ +import { t } from "@lingui/macro"; + +import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; +import { CSSResult, TemplateResult, html } from "lit"; + +import AKGlobal from "../../../authentik.css"; +import PFButton from "@patternfly/patternfly/components/Button/button.css"; +import PFForm from "@patternfly/patternfly/components/Form/form.css"; +import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; + +import { TypeCreate } from "@goauthentik/api"; + +import { WizardPage } from "../../../elements/wizard/WizardPage"; + +@customElement("ak-application-wizard-type") +export class TypeApplicationWizardPage extends WizardPage { + applicationTypes: TypeCreate[] = [ + { + component: "ak-application-wizard-type-oauth", + name: t`OAuth2/OIDC`, + description: t`Modern applications, APIs and Single-page applications.`, + modelName: "", + }, + { + component: "ak-application-wizard-type-saml", + name: t`SAML`, + description: t`XML-based SSO standard. Use this if your application only supports SAML.`, + modelName: "", + }, + { + component: "ak-application-wizard-type-proxy", + name: t`Proxy`, + description: t`Legacy applications which don't natively support SSO.`, + modelName: "", + }, + { + component: "ak-application-wizard-type-ldap", + name: t`LDAP`, + description: t`Provide an LDAP interface for applications and users to authenticate against.`, + modelName: "", + }, + { + component: "ak-application-wizard-type-link", + name: t`Link`, + description: t`Provide an LDAP interface for applications and users to authenticate against.`, + modelName: "", + }, + ]; + + sidebarLabel = () => t`Authentication method`; + + static get styles(): CSSResult[] { + return [PFBase, PFButton, PFForm, PFRadio, AKGlobal]; + } + + render(): TemplateResult { + return html`
+ ${this.applicationTypes.map((type) => { + return html`
+ { + this.host.steps = [ + "ak-application-wizard-initial", + "ak-application-wizard-type", + type.component, + ]; + this.host.isValid = true; + }} + /> + + ${type.description} +
`; + })} +
`; + } +} diff --git a/web/src/pages/applications/wizard/ldap/TypeLDAPApplicationWizardPage.ts b/web/src/pages/applications/wizard/ldap/TypeLDAPApplicationWizardPage.ts new file mode 100644 index 000000000..86e5174cd --- /dev/null +++ b/web/src/pages/applications/wizard/ldap/TypeLDAPApplicationWizardPage.ts @@ -0,0 +1,74 @@ +import { t } from "@lingui/macro"; + +import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; +import { TemplateResult, html } from "lit"; + +import { + CoreApi, + FlowDesignationEnum, + FlowsApi, + LDAPProviderRequest, + ProvidersApi, + UserServiceAccountResponse, +} from "@goauthentik/api"; + +import { DEFAULT_CONFIG } from "../../../../api/Config"; +import { KeyUnknown } from "../../../../elements/forms/Form"; +import "../../../../elements/forms/HorizontalFormElement"; +import { WizardFormPage } from "../../../../elements/wizard/WizardFormPage"; + +@customElement("ak-application-wizard-type-ldap") +export class TypeLDAPApplicationWizardPage extends WizardFormPage { + sidebarLabel = () => t`LDAP details`; + + nextDataCallback = async (data: KeyUnknown): Promise => { + let name = this.host.state["name"] as string; + // Check if a provider with the name already exists + const providers = await new ProvidersApi(DEFAULT_CONFIG).providersAllList({ + search: name, + }); + if (providers.results.filter((provider) => provider.name == name)) { + name += "-1"; + } + this.host.addActionBefore(t`Create service account`, async (): Promise => { + const serviceAccount = await new CoreApi(DEFAULT_CONFIG).coreUsersServiceAccountCreate({ + userServiceAccountRequest: { + name: name, + createGroup: true, + }, + }); + this.host.state["serviceAccount"] = serviceAccount; + return true; + }); + this.host.addActionBefore(t`Create provider`, async (): Promise => { + // Get all flows and default to the implicit authorization + const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({ + designation: FlowDesignationEnum.Authorization, + ordering: "slug", + }); + const serviceAccount = this.host.state["serviceAccount"] as UserServiceAccountResponse; + const req: LDAPProviderRequest = { + name: name, + authorizationFlow: flows.results[0].pk, + baseDn: data.baseDN as string, + searchGroup: serviceAccount.groupPk, + }; + const provider = await new ProvidersApi(DEFAULT_CONFIG).providersLdapCreate({ + lDAPProviderRequest: req, + }); + this.host.state["provider"] = provider; + return true; + }); + return true; + }; + + renderForm(): TemplateResult { + const domainParts = window.location.hostname.split("."); + const defaultBaseDN = domainParts.map((part) => `dc=${part}`).join(","); + return html`
+ + + +
`; + } +} diff --git a/web/src/pages/applications/wizard/link/TypeLinkApplicationWizardPage.ts b/web/src/pages/applications/wizard/link/TypeLinkApplicationWizardPage.ts new file mode 100644 index 000000000..63daa7b0d --- /dev/null +++ b/web/src/pages/applications/wizard/link/TypeLinkApplicationWizardPage.ts @@ -0,0 +1,31 @@ +import { t } from "@lingui/macro"; + +import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; +import { TemplateResult, html } from "lit"; + +import { KeyUnknown } from "../../../../elements/forms/Form"; +import "../../../../elements/forms/HorizontalFormElement"; +import { WizardFormPage } from "../../../../elements/wizard/WizardFormPage"; + +@customElement("ak-application-wizard-type-link") +export class TypeLinkApplicationWizardPage extends WizardFormPage { + sidebarLabel = () => t`Application Link`; + + nextDataCallback = async (data: KeyUnknown): Promise => { + this.host.state["link"] = data.link; + return true; + }; + + renderForm(): TemplateResult { + return html` +
+ + +

+ ${t`URL which will be opened when a user clicks on the application.`} +

+
+
+ `; + } +} diff --git a/web/src/pages/applications/wizard/oauth/TypeOAuthAPIApplicationWizardPage.ts b/web/src/pages/applications/wizard/oauth/TypeOAuthAPIApplicationWizardPage.ts new file mode 100644 index 000000000..5228b857f --- /dev/null +++ b/web/src/pages/applications/wizard/oauth/TypeOAuthAPIApplicationWizardPage.ts @@ -0,0 +1,33 @@ +import { t } from "@lingui/macro"; + +import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; +import { CSSResult, TemplateResult, html } from "lit"; + +import AKGlobal from "../../../../authentik.css"; +import PFButton from "@patternfly/patternfly/components/Button/button.css"; +import PFForm from "@patternfly/patternfly/components/Form/form.css"; +import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; + +import "../../../../elements/forms/HorizontalFormElement"; +import { WizardPage } from "../../../../elements/wizard/WizardPage"; + +@customElement("ak-application-wizard-type-oauth-api") +export class TypeOAuthAPIApplicationWizardPage extends WizardPage { + static get styles(): CSSResult[] { + return [PFBase, PFButton, PFForm, PFRadio, AKGlobal]; + } + + sidebarLabel = () => t`Method details`; + + render(): TemplateResult { + return html`
+

+ ${t`This configuration can be used to authenticate to authentik with other APIs other otherwise programmatically.`} +

+

+ ${t`By default, all service accounts can authenticate as this application, as long as they have a valid token of the type app-password.`} +

+
`; + } +} diff --git a/web/src/pages/applications/wizard/oauth/TypeOAuthApplicationWizardPage.ts b/web/src/pages/applications/wizard/oauth/TypeOAuthApplicationWizardPage.ts new file mode 100644 index 000000000..fd3f9d088 --- /dev/null +++ b/web/src/pages/applications/wizard/oauth/TypeOAuthApplicationWizardPage.ts @@ -0,0 +1,78 @@ +import { t } from "@lingui/macro"; + +import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; +import { CSSResult, TemplateResult, html } from "lit"; + +import AKGlobal from "../../../../authentik.css"; +import PFButton from "@patternfly/patternfly/components/Button/button.css"; +import PFForm from "@patternfly/patternfly/components/Form/form.css"; +import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; + +import { TypeCreate } from "@goauthentik/api"; + +import "../../../../elements/forms/HorizontalFormElement"; +import { WizardPage } from "../../../../elements/wizard/WizardPage"; + +@customElement("ak-application-wizard-type-oauth") +export class TypeOAuthApplicationWizardPage extends WizardPage { + applicationTypes: TypeCreate[] = [ + { + component: "ak-application-wizard-type-oauth-code", + name: t`Web application`, + description: t`Applications which handle the authentication server-side (for example, Python, Go, Rust, Java, PHP)`, + modelName: "", + }, + { + component: "ak-application-wizard-type-oauth-implicit", + name: t`Single-page applications`, + description: t`Single-page applications which handle authentication in the browser (for example, Javascript, Angular, React, Vue)`, + modelName: "", + }, + { + component: "ak-application-wizard-type-oauth-implicit", + name: t`Native application`, + description: t`Applications which redirect users to a non-web callback (for example, Android, iOS)`, + modelName: "", + }, + { + component: "ak-application-wizard-type-oauth-api", + name: t`API`, + description: t`Authentication without user interaction, or machine-to-machine authentication.`, + modelName: "", + }, + ]; + + static get styles(): CSSResult[] { + return [PFBase, PFButton, PFForm, PFRadio, AKGlobal]; + } + + sidebarLabel = () => t`Application type`; + + render(): TemplateResult { + return html`
+ ${this.applicationTypes.map((type) => { + return html`
+ { + this.host.steps = [ + "ak-application-wizard-initial", + "ak-application-wizard-type", + "ak-application-wizard-type-oauth", + type.component, + ]; + this.host.state["oauth-type"] = type.component; + this.host.isValid = true; + }} + /> + + ${type.description} +
`; + })} +
`; + } +} diff --git a/web/src/pages/applications/wizard/oauth/TypeOAuthCodeApplicationWizardPage.ts b/web/src/pages/applications/wizard/oauth/TypeOAuthCodeApplicationWizardPage.ts new file mode 100644 index 000000000..0655295bb --- /dev/null +++ b/web/src/pages/applications/wizard/oauth/TypeOAuthCodeApplicationWizardPage.ts @@ -0,0 +1,72 @@ +import { t } from "@lingui/macro"; + +import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; +import { TemplateResult, html } from "lit"; +import { ifDefined } from "lit/directives/if-defined.js"; +import { until } from "lit/directives/until.js"; + +import { + ClientTypeEnum, + FlowsApi, + FlowsInstancesListDesignationEnum, + OAuth2ProviderRequest, + ProvidersApi, +} from "@goauthentik/api"; + +import { DEFAULT_CONFIG } from "../../../../api/Config"; +import { KeyUnknown } from "../../../../elements/forms/Form"; +import "../../../../elements/forms/HorizontalFormElement"; +import { WizardFormPage } from "../../../../elements/wizard/WizardFormPage"; +import "../../../../elements/wizard/WizardFormPage"; + +@customElement("ak-application-wizard-type-oauth-code") +export class TypeOAuthCodeApplicationWizardPage extends WizardFormPage { + sidebarLabel = () => t`Method details`; + + nextDataCallback = async (data: KeyUnknown): Promise => { + this.host.addActionBefore(t`Create provider`, async (): Promise => { + const req: OAuth2ProviderRequest = { + name: this.host.state["name"] as string, + clientType: ClientTypeEnum.Confidential, + authorizationFlow: data.authorizationFlow as string, + }; + const provider = await new ProvidersApi(DEFAULT_CONFIG).providersOauth2Create({ + oAuth2ProviderRequest: req, + }); + this.host.state["provider"] = provider; + return true; + }); + return true; + }; + + renderForm(): TemplateResult { + return html`
+ + +

+ ${t`Flow used when users access this application.`} +

+
+
`; + } +} diff --git a/web/src/pages/applications/wizard/oauth/TypeOAuthImplicitApplicationWizardPage.ts b/web/src/pages/applications/wizard/oauth/TypeOAuthImplicitApplicationWizardPage.ts new file mode 100644 index 000000000..e8bca45ee --- /dev/null +++ b/web/src/pages/applications/wizard/oauth/TypeOAuthImplicitApplicationWizardPage.ts @@ -0,0 +1,16 @@ +import { t } from "@lingui/macro"; + +import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; +import { TemplateResult, html } from "lit"; + +import "../../../../elements/forms/HorizontalFormElement"; +import { WizardFormPage } from "../../../../elements/wizard/WizardFormPage"; + +@customElement("ak-application-wizard-type-oauth-implicit") +export class TypeOAuthImplicitApplicationWizardPage extends WizardFormPage { + sidebarLabel = () => t`Method details`; + + render(): TemplateResult { + return html`
some stuff idk
`; + } +} diff --git a/web/src/pages/applications/wizard/proxy/TypeProxyApplicationWizardPage.ts b/web/src/pages/applications/wizard/proxy/TypeProxyApplicationWizardPage.ts new file mode 100644 index 000000000..47a0fc165 --- /dev/null +++ b/web/src/pages/applications/wizard/proxy/TypeProxyApplicationWizardPage.ts @@ -0,0 +1,65 @@ +import { t } from "@lingui/macro"; + +import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; +import { TemplateResult, html } from "lit"; + +import { + FlowDesignationEnum, + FlowsApi, + ProvidersApi, + ProxyProviderRequest, +} from "@goauthentik/api"; + +import { DEFAULT_CONFIG } from "../../../../api/Config"; +import { KeyUnknown } from "../../../../elements/forms/Form"; +import "../../../../elements/forms/HorizontalFormElement"; +import { WizardFormPage } from "../../../../elements/wizard/WizardFormPage"; + +@customElement("ak-application-wizard-type-proxy") +export class TypeProxyApplicationWizardPage extends WizardFormPage { + sidebarLabel = () => t`Proxy details`; + + nextDataCallback = async (data: KeyUnknown): Promise => { + let name = this.host.state["name"] as string; + // Check if a provider with the name already exists + const providers = await new ProvidersApi(DEFAULT_CONFIG).providersAllList({ + search: name, + }); + if (providers.results.filter((provider) => provider.name == name)) { + name += "-1"; + } + this.host.addActionBefore(t`Create provider`, async (): Promise => { + // Get all flows and default to the implicit authorization + const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({ + designation: FlowDesignationEnum.Authorization, + ordering: "slug", + }); + const req: ProxyProviderRequest = { + name: name, + authorizationFlow: flows.results[0].pk, + externalHost: data.externalHost as string, + }; + const provider = await new ProvidersApi(DEFAULT_CONFIG).providersProxyCreate({ + proxyProviderRequest: req, + }); + this.host.state["provider"] = provider; + return true; + }); + return true; + }; + + renderForm(): TemplateResult { + return html`
+ + +

+ ${t`External domain you will be accessing the domain from.`} +

+
+
`; + } +} diff --git a/web/src/pages/applications/wizard/saml/TypeSAMLApplicationWizardPage.ts b/web/src/pages/applications/wizard/saml/TypeSAMLApplicationWizardPage.ts new file mode 100644 index 000000000..b2565ab31 --- /dev/null +++ b/web/src/pages/applications/wizard/saml/TypeSAMLApplicationWizardPage.ts @@ -0,0 +1,66 @@ +import { t } from "@lingui/macro"; + +import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; +import { CSSResult, TemplateResult, html } from "lit"; + +import AKGlobal from "../../../../authentik.css"; +import PFButton from "@patternfly/patternfly/components/Button/button.css"; +import PFForm from "@patternfly/patternfly/components/Form/form.css"; +import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; + +import { TypeCreate } from "@goauthentik/api"; + +import "../../../../elements/forms/HorizontalFormElement"; +import { WizardPage } from "../../../../elements/wizard/WizardPage"; + +@customElement("ak-application-wizard-type-saml") +export class TypeOAuthApplicationWizardPage extends WizardPage { + applicationTypes: TypeCreate[] = [ + { + component: "ak-application-wizard-type-saml-import", + name: t`Import SAML Metadata`, + description: t`Import the metadata document of the applicaation you want to configure.`, + modelName: "", + }, + { + component: "ak-application-wizard-type-saml-config", + name: t`Manual configuration`, + description: t`Manually configure SAML`, + modelName: "", + }, + ]; + + static get styles(): CSSResult[] { + return [PFBase, PFButton, PFForm, PFRadio, AKGlobal]; + } + + sidebarLabel = () => t`Application type`; + + render(): TemplateResult { + return html`
+ ${this.applicationTypes.map((type) => { + return html`
+ { + this.host.steps = [ + "ak-application-wizard-initial", + "ak-application-wizard-type", + "ak-application-wizard-type-saml", + type.component, + ]; + this.host.state["saml-type"] = type.component; + this.host.isValid = true; + }} + /> + + ${type.description} +
`; + })} +
`; + } +} diff --git a/web/src/pages/applications/wizard/saml/TypeSAMLConfigApplicationWizardPage.ts b/web/src/pages/applications/wizard/saml/TypeSAMLConfigApplicationWizardPage.ts new file mode 100644 index 000000000..0d0c03ead --- /dev/null +++ b/web/src/pages/applications/wizard/saml/TypeSAMLConfigApplicationWizardPage.ts @@ -0,0 +1,56 @@ +import { t } from "@lingui/macro"; + +import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; +import { TemplateResult, html } from "lit"; + +import { FlowDesignationEnum, FlowsApi, ProvidersApi, SAMLProviderRequest } from "@goauthentik/api"; + +import { DEFAULT_CONFIG } from "../../../../api/Config"; +import { KeyUnknown } from "../../../../elements/forms/Form"; +import "../../../../elements/forms/HorizontalFormElement"; +import { WizardFormPage } from "../../../../elements/wizard/WizardFormPage"; + +@customElement("ak-application-wizard-type-saml-config") +export class TypeSAMLApplicationWizardPage extends WizardFormPage { + sidebarLabel = () => t`SAML details`; + + nextDataCallback = async (data: KeyUnknown): Promise => { + let name = this.host.state["name"] as string; + // Check if a provider with the name already exists + const providers = await new ProvidersApi(DEFAULT_CONFIG).providersAllList({ + search: name, + }); + if (providers.results.filter((provider) => provider.name == name)) { + name += "-1"; + } + this.host.addActionBefore(t`Create provider`, async (): Promise => { + // Get all flows and default to the implicit authorization + const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({ + designation: FlowDesignationEnum.Authorization, + ordering: "slug", + }); + const req: SAMLProviderRequest = { + name: name, + authorizationFlow: flows.results[0].pk, + acsUrl: data.acsUrl as string, + }; + const provider = await new ProvidersApi(DEFAULT_CONFIG).providersSamlCreate({ + sAMLProviderRequest: req, + }); + this.host.state["provider"] = provider; + return true; + }); + return true; + }; + + renderForm(): TemplateResult { + return html`
+ + +

+ ${t`URL that authentik will redirect back to after successful authentication.`} +

+
+
`; + } +} diff --git a/web/src/pages/applications/wizard/saml/TypeSAMLImportApplicationWizardPage.ts b/web/src/pages/applications/wizard/saml/TypeSAMLImportApplicationWizardPage.ts new file mode 100644 index 000000000..634234174 --- /dev/null +++ b/web/src/pages/applications/wizard/saml/TypeSAMLImportApplicationWizardPage.ts @@ -0,0 +1,58 @@ +import { t } from "@lingui/macro"; + +import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; +import { TemplateResult, html } from "lit"; + +import { + FlowDesignationEnum, + FlowsApi, + ProvidersApi, + ProvidersSamlImportMetadataCreateRequest, +} from "@goauthentik/api"; + +import { DEFAULT_CONFIG } from "../../../../api/Config"; +import { KeyUnknown } from "../../../../elements/forms/Form"; +import "../../../../elements/forms/HorizontalFormElement"; +import { WizardFormPage } from "../../../../elements/wizard/WizardFormPage"; + +@customElement("ak-application-wizard-type-saml-import") +export class TypeSAMLImportApplicationWizardPage extends WizardFormPage { + sidebarLabel = () => t`Import SAML metadata`; + + nextDataCallback = async (data: KeyUnknown): Promise => { + let name = this.host.state["name"] as string; + // Check if a provider with the name already exists + const providers = await new ProvidersApi(DEFAULT_CONFIG).providersAllList({ + search: name, + }); + if (providers.results.filter((provider) => provider.name == name)) { + name += "-1"; + } + this.host.addActionBefore(t`Create provider`, async (): Promise => { + // Get all flows and default to the implicit authorization + const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({ + designation: FlowDesignationEnum.Authorization, + ordering: "slug", + }); + const req: ProvidersSamlImportMetadataCreateRequest = { + name: name, + authorizationFlow: flows.results[0].slug, + file: data["metadata"] as Blob, + }; + const provider = await new ProvidersApi( + DEFAULT_CONFIG, + ).providersSamlImportMetadataCreate(req); + this.host.state["provider"] = provider; + return true; + }); + return true; + }; + + renderForm(): TemplateResult { + return html`
+ + + +
`; + } +} diff --git a/web/src/pages/flows/FlowForm.ts b/web/src/pages/flows/FlowForm.ts index 4c439ceca..ea0c18e35 100644 --- a/web/src/pages/flows/FlowForm.ts +++ b/web/src/pages/flows/FlowForm.ts @@ -54,7 +54,7 @@ export class FlowForm extends ModelForm { } const c = await config(); if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) { - const icon = this.getFormFile(); + const icon = this.getFormFiles()["background"]; if (icon || this.clearBackground) { await new FlowsApi(DEFAULT_CONFIG).flowsInstancesSetBackgroundCreate({ slug: flow.slug, diff --git a/web/src/pages/flows/FlowImportForm.ts b/web/src/pages/flows/FlowImportForm.ts index ecc8950f3..d4fc551b5 100644 --- a/web/src/pages/flows/FlowImportForm.ts +++ b/web/src/pages/flows/FlowImportForm.ts @@ -18,7 +18,7 @@ export class FlowImportForm extends Form { // eslint-disable-next-line send = (data: Flow): Promise => { - const file = this.getFormFile(); + const file = this.getFormFiles()["flow"]; if (!file) { throw new SentryIgnoredError("No form data"); } diff --git a/web/src/pages/outposts/ServiceConnectionWizard.ts b/web/src/pages/outposts/ServiceConnectionWizard.ts index 0e9c7e767..94805719f 100644 --- a/web/src/pages/outposts/ServiceConnectionWizard.ts +++ b/web/src/pages/outposts/ServiceConnectionWizard.ts @@ -28,6 +28,7 @@ export class InitialServiceConnectionWizardPage extends WizardPage { static get styles(): CSSResult[] { return [PFBase, PFForm, PFButton, AKGlobal, PFRadio]; } + sidebarLabel = () => t`Select type`; render(): TemplateResult { return html`
@@ -39,10 +40,10 @@ export class InitialServiceConnectionWizardPage extends WizardPage { name="type" id=${`${type.component}-${type.modelName}`} @change=${() => { - this.host.setSteps( + this.host.steps = [ "initial", `type-${type.component}-${type.modelName}`, - ); + ]; this._isValid = true; }} /> @@ -83,7 +84,6 @@ export class ServiceConnectionWizard extends LitElement { > t`Select type`} .connectionTypes=${this.connectionTypes} > diff --git a/web/src/pages/policies/PolicyWizard.ts b/web/src/pages/policies/PolicyWizard.ts index c099473a4..92803870a 100644 --- a/web/src/pages/policies/PolicyWizard.ts +++ b/web/src/pages/policies/PolicyWizard.ts @@ -33,6 +33,7 @@ export class InitialPolicyWizardPage extends WizardPage { static get styles(): CSSResult[] { return [PFBase, PFForm, PFButton, AKGlobal, PFRadio]; } + sidebarLabel = () => t`Select type`; render(): TemplateResult { return html` @@ -44,10 +45,10 @@ export class InitialPolicyWizardPage extends WizardPage { name="type" id=${`${type.component}-${type.modelName}`} @change=${() => { - this.host.setSteps( + this.host.steps = [ "initial", `type-${type.component}-${type.modelName}`, - ); + ]; this._isValid = true; }} /> @@ -86,11 +87,7 @@ export class PolicyWizard extends LitElement { header=${t`New policy`} description=${t`Create a new policy.`} > - t`Select type`} - .policyTypes=${this.policyTypes} - > + ${this.policyTypes.map((type) => { return html` diff --git a/web/src/pages/property-mappings/PropertyMappingWizard.ts b/web/src/pages/property-mappings/PropertyMappingWizard.ts index bb3f2fe6c..2321cb153 100644 --- a/web/src/pages/property-mappings/PropertyMappingWizard.ts +++ b/web/src/pages/property-mappings/PropertyMappingWizard.ts @@ -31,6 +31,7 @@ export class InitialPropertyMappingWizardPage extends WizardPage { static get styles(): CSSResult[] { return [PFBase, PFForm, PFButton, AKGlobal, PFRadio]; } + sidebarLabel = () => t`Select type`; render(): TemplateResult { return html` @@ -42,10 +43,10 @@ export class InitialPropertyMappingWizardPage extends WizardPage { name="type" id=${`${type.component}-${type.modelName}`} @change=${() => { - this.host.setSteps( + this.host.steps = [ "initial", `type-${type.component}-${type.modelName}`, - ); + ]; this._isValid = true; }} /> @@ -83,7 +84,6 @@ export class PropertyMappingWizard extends LitElement { > t`Select type`} .mappingTypes=${this.mappingTypes} > diff --git a/web/src/pages/providers/ProviderWizard.ts b/web/src/pages/providers/ProviderWizard.ts index 9df702397..787904e04 100644 --- a/web/src/pages/providers/ProviderWizard.ts +++ b/web/src/pages/providers/ProviderWizard.ts @@ -18,6 +18,7 @@ import { property } from "lit/decorators.js"; import AKGlobal from "@goauthentik/web/authentik.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFForm from "@patternfly/patternfly/components/Form/form.css"; +import PFHint from "@patternfly/patternfly/components/Hint/hint.css"; import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; @@ -29,28 +30,45 @@ export class InitialProviderWizardPage extends WizardPage { providerTypes: TypeCreate[] = []; static get styles(): CSSResult[] { - return [PFBase, PFForm, PFButton, AKGlobal, PFRadio]; + return [PFBase, PFForm, PFHint, PFButton, AKGlobal, PFRadio]; } + sidebarLabel = () => t`Select type`; render(): TemplateResult { - return html` - ${this.providerTypes.map((type) => { - return html`
- { - this.host.setSteps("initial", `type-${type.component}`); - this._isValid = true; - }} - /> - - ${type.description} -
`; - })} - `; + return html`
+
${t`Try the new application wizard`}
+
+ ${t`The new application wizard greatly simplifies the steps required to create applications and providers.`} +
+ +
+
+
+ ${this.providerTypes.map((type) => { + return html`
+ { + this.host.steps = ["initial", `type-${type.component}`]; + this._isValid = true; + }} + /> + + ${type.description} +
`; + })} +
`; } } @@ -87,11 +105,7 @@ export class ProviderWizard extends LitElement { return this.finalHandler(); }} > - t`Select type`} - .providerTypes=${this.providerTypes} - > + ${this.providerTypes.map((type) => { return html` diff --git a/web/src/pages/providers/saml/SAMLProviderImportForm.ts b/web/src/pages/providers/saml/SAMLProviderImportForm.ts index 0a76b09f8..ef36ac753 100644 --- a/web/src/pages/providers/saml/SAMLProviderImportForm.ts +++ b/web/src/pages/providers/saml/SAMLProviderImportForm.ts @@ -24,7 +24,7 @@ export class SAMLProviderImportForm extends Form { // eslint-disable-next-line send = (data: SAMLProvider): Promise => { - const file = this.getFormFile(); + const file = this.getFormFiles()["metadata"]; if (!file) { throw new SentryIgnoredError("No form data"); } @@ -67,7 +67,7 @@ export class SAMLProviderImportForm extends Form {

- + `; diff --git a/web/src/pages/sources/SourceWizard.ts b/web/src/pages/sources/SourceWizard.ts index b0ab45b3f..cd9791592 100644 --- a/web/src/pages/sources/SourceWizard.ts +++ b/web/src/pages/sources/SourceWizard.ts @@ -30,6 +30,7 @@ export class InitialSourceWizardPage extends WizardPage { static get styles(): CSSResult[] { return [PFBase, PFForm, PFButton, AKGlobal, PFRadio]; } + sidebarLabel = () => t`Select type`; render(): TemplateResult { return html`
@@ -41,10 +42,10 @@ export class InitialSourceWizardPage extends WizardPage { name="type" id=${`${type.component}-${type.modelName}`} @change=${() => { - this.host.setSteps( + this.host.steps = [ "initial", `type-${type.component}-${type.modelName}`, - ); + ]; this._isValid = true; }} /> @@ -80,11 +81,7 @@ export class SourceWizard extends LitElement { header=${t`New source`} description=${t`Create a new source.`} > - t`Select type`} - .sourceTypes=${this.sourceTypes} - > + ${this.sourceTypes.map((type) => { return html` diff --git a/web/src/pages/stages/StageWizard.ts b/web/src/pages/stages/StageWizard.ts index 5dab99dc5..2478ea588 100644 --- a/web/src/pages/stages/StageWizard.ts +++ b/web/src/pages/stages/StageWizard.ts @@ -42,6 +42,7 @@ import "./user_write/UserWriteStageForm.ts"; export class InitialStageWizardPage extends WizardPage { @property({ attribute: false }) stageTypes: TypeCreate[] = []; + sidebarLabel = () => t`Select type`; static get styles(): CSSResult[] { return [PFBase, PFForm, PFButton, AKGlobal, PFRadio]; @@ -57,10 +58,10 @@ export class InitialStageWizardPage extends WizardPage { name="type" id=${`${type.component}-${type.modelName}`} @change=${() => { - this.host.setSteps( + this.host.steps = [ "initial", `type-${type.component}-${type.modelName}`, - ); + ]; this._isValid = true; }} /> @@ -99,11 +100,7 @@ export class StageWizard extends LitElement { header=${t`New stage`} description=${t`Create a new stage.`} > - t`Select type`} - .stageTypes=${this.stageTypes} - > + ${this.stageTypes.map((type) => { return html` diff --git a/web/src/user/user-settings/details/UserSettingsFlowExecutor.ts b/web/src/user/user-settings/details/UserSettingsFlowExecutor.ts index 2f0c7e621..b58ce4cf2 100644 --- a/web/src/user/user-settings/details/UserSettingsFlowExecutor.ts +++ b/web/src/user/user-settings/details/UserSettingsFlowExecutor.ts @@ -203,7 +203,7 @@ export class UserSettingsFlowExecutor extends LitElement implements StageHost { .challenge=${this.challenge} >`; default: - console.log( + console.debug( `authentik/user/flows: unsupported stage type ${this.challenge.component}`, ); return html`