web/admin: application wizard (part 1) (#2745)
* initial Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * remove log Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * start oauth Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * use form for all type wizard pages Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * more oauth Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * basic wizard actions Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * make resets work Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add hint in provider wizard Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * render correct icon in empty state in table page Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * improve empty state Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * more Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add more pages Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * fix Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add group PK to service account creation response Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * use wizard-level isValid prop Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * re-add old buttons Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
a8c04f96d2
commit
504338ea66
3
Makefile
3
Makefile
|
@ -112,6 +112,9 @@ web-install:
|
||||||
cd web && npm ci
|
cd web && npm ci
|
||||||
|
|
||||||
web-watch:
|
web-watch:
|
||||||
|
rm -rf web/dist/
|
||||||
|
mkdir web/dist/
|
||||||
|
touch web/dist/.gitkeep
|
||||||
cd web && npm run watch
|
cd web && npm run watch
|
||||||
|
|
||||||
web-lint-fix:
|
web-lint-fix:
|
||||||
|
|
|
@ -89,6 +89,14 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||||
"meta_publisher",
|
"meta_publisher",
|
||||||
"group",
|
"group",
|
||||||
]
|
]
|
||||||
|
filterset_fields = [
|
||||||
|
"name",
|
||||||
|
"slug",
|
||||||
|
"meta_launch_url",
|
||||||
|
"meta_description",
|
||||||
|
"meta_publisher",
|
||||||
|
"group",
|
||||||
|
]
|
||||||
lookup_field = "slug"
|
lookup_field = "slug"
|
||||||
filterset_fields = ["name", "slug"]
|
filterset_fields = ["name", "slug"]
|
||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
|
|
|
@ -24,7 +24,13 @@ from drf_spectacular.utils import (
|
||||||
)
|
)
|
||||||
from guardian.shortcuts import get_anonymous_user, get_objects_for_user
|
from guardian.shortcuts import get_anonymous_user, get_objects_for_user
|
||||||
from rest_framework.decorators import action
|
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.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import (
|
from rest_framework.serializers import (
|
||||||
|
@ -315,6 +321,9 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||||
{
|
{
|
||||||
"username": CharField(required=True),
|
"username": CharField(required=True),
|
||||||
"token": 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},
|
attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: False},
|
||||||
path=USER_PATH_SERVICE_ACCOUNT,
|
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"):
|
if create_group and self.request.user.has_perm("authentik_core.add_group"):
|
||||||
group = Group.objects.create(
|
group = Group.objects.create(
|
||||||
name=username,
|
name=username,
|
||||||
)
|
)
|
||||||
group.users.add(user)
|
group.users.add(user)
|
||||||
|
response["group_pk"] = str(group.pk)
|
||||||
token = Token.objects.create(
|
token = Token.objects.create(
|
||||||
identifier=slugify(f"service-account-{username}-password"),
|
identifier=slugify(f"service-account-{username}-password"),
|
||||||
intent=TokenIntents.INTENT_APP_PASSWORD,
|
intent=TokenIntents.INTENT_APP_PASSWORD,
|
||||||
user=user,
|
user=user,
|
||||||
expires=now() + timedelta(days=360),
|
expires=now() + timedelta(days=360),
|
||||||
)
|
)
|
||||||
return Response({"username": user.username, "token": token.key})
|
response["token"] = token.key
|
||||||
|
return Response(response)
|
||||||
except (IntegrityError) as exc:
|
except (IntegrityError) as exc:
|
||||||
return Response(data={"non_field_errors": [str(exc)]}, status=400)
|
return Response(data={"non_field_errors": [str(exc)]}, status=400)
|
||||||
|
|
||||||
|
|
|
@ -31589,8 +31589,16 @@ components:
|
||||||
type: string
|
type: string
|
||||||
token:
|
token:
|
||||||
type: string
|
type: string
|
||||||
|
user_uid:
|
||||||
|
type: string
|
||||||
|
user_pk:
|
||||||
|
type: integer
|
||||||
|
group_pk:
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- token
|
- token
|
||||||
|
- user_pk
|
||||||
|
- user_uid
|
||||||
- username
|
- username
|
||||||
UserSetting:
|
UserSetting:
|
||||||
type: object
|
type: object
|
||||||
|
|
|
@ -32,7 +32,7 @@ export class WebsocketClient {
|
||||||
});
|
});
|
||||||
this.messageSocket.addEventListener("close", (e) => {
|
this.messageSocket.addEventListener("close", (e) => {
|
||||||
console.debug(`authentik/ws: closed ws connection: ${e}`);
|
console.debug(`authentik/ws: closed ws connection: ${e}`);
|
||||||
if (this.retryDelay > 3000) {
|
if (this.retryDelay > 6000) {
|
||||||
showMessage(
|
showMessage(
|
||||||
{
|
{
|
||||||
level: MessageLevel.error,
|
level: MessageLevel.error,
|
||||||
|
|
|
@ -29,6 +29,10 @@ export class APIError extends Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface KeyUnknown {
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
@customElement("ak-form")
|
@customElement("ak-form")
|
||||||
export class Form<T> extends LitElement {
|
export class Form<T> extends LitElement {
|
||||||
viewportCheck = true;
|
viewportCheck = true;
|
||||||
|
@ -101,15 +105,11 @@ export class Form<T> extends LitElement {
|
||||||
ironForm?.reset();
|
ironForm?.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getFormFiles(): { [key: string]: File } {
|
||||||
* 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 {
|
|
||||||
const ironForm = this.shadowRoot?.querySelector("iron-form");
|
const ironForm = this.shadowRoot?.querySelector("iron-form");
|
||||||
|
const files: { [key: string]: File } = {};
|
||||||
if (!ironForm) {
|
if (!ironForm) {
|
||||||
return;
|
return files;
|
||||||
}
|
}
|
||||||
const elements = ironForm._getSubmittableElements();
|
const elements = ironForm._getSubmittableElements();
|
||||||
for (let i = 0; i < elements.length; i++) {
|
for (let i = 0; i < elements.length; i++) {
|
||||||
|
@ -118,13 +118,18 @@ export class Form<T> extends LitElement {
|
||||||
if ((element.files || []).length < 1) {
|
if ((element.files || []).length < 1) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// We already checked the length
|
files[element.name] = (element.files || [])[0];
|
||||||
return (element.files || [])[0];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
serializeForm(form: IronFormElement): T {
|
serializeForm(): T | undefined {
|
||||||
|
const form = this.shadowRoot?.querySelector<IronFormElement>("iron-form");
|
||||||
|
if (!form) {
|
||||||
|
console.warn("authentik/forms: failed to find iron-form");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const elements: HTMLInputElement[] = form._getSubmittableElements();
|
const elements: HTMLInputElement[] = form._getSubmittableElements();
|
||||||
const json: { [key: string]: unknown } = {};
|
const json: { [key: string]: unknown } = {};
|
||||||
elements.forEach((element) => {
|
elements.forEach((element) => {
|
||||||
|
@ -189,12 +194,15 @@ export class Form<T> extends LitElement {
|
||||||
|
|
||||||
submit(ev: Event): Promise<unknown> | undefined {
|
submit(ev: Event): Promise<unknown> | undefined {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
const ironForm = this.shadowRoot?.querySelector("iron-form");
|
const data = this.serializeForm();
|
||||||
if (!ironForm) {
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const form = this.shadowRoot?.querySelector<IronFormElement>("iron-form");
|
||||||
|
if (!form) {
|
||||||
console.warn("authentik/forms: failed to find iron-form");
|
console.warn("authentik/forms: failed to find iron-form");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = this.serializeForm(ironForm);
|
|
||||||
return this.send(data)
|
return this.send(data)
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
showMessage({
|
showMessage({
|
||||||
|
@ -221,7 +229,7 @@ export class Form<T> extends LitElement {
|
||||||
throw errorMessage;
|
throw errorMessage;
|
||||||
}
|
}
|
||||||
// assign all input-related errors to their elements
|
// assign all input-related errors to their elements
|
||||||
const elements: HorizontalFormElement[] = ironForm._getSubmittableElements();
|
const elements: HorizontalFormElement[] = form._getSubmittableElements();
|
||||||
elements.forEach((element) => {
|
elements.forEach((element) => {
|
||||||
const elementName = element.name;
|
const elementName = element.name;
|
||||||
if (!elementName) return;
|
if (!elementName) return;
|
||||||
|
|
|
@ -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<void> => {
|
||||||
|
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<void> {
|
||||||
|
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`<div class="pf-l-bullseye">
|
||||||
|
<div class="pf-c-empty-state pf-m-lg">
|
||||||
|
<div class="pf-c-empty-state__content">
|
||||||
|
<i class="fas fa- fa-cogs pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||||
|
<h1 class="pf-c-title pf-m-lg">${this.currentStep?.action.displayName}</h1>
|
||||||
|
<div class="pf-c-empty-state__body">
|
||||||
|
<ol class="pf-c-progress-stepper pf-m-vertical">
|
||||||
|
${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` <li class="pf-c-progress-stepper__step ${cls}">
|
||||||
|
<div class="pf-c-progress-stepper__step-connector">
|
||||||
|
<span class="pf-c-progress-stepper__step-icon">
|
||||||
|
<i class="fas fa-check-circle" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="pf-c-progress-stepper__step-main">
|
||||||
|
<div class="pf-c-progress-stepper__step-title">
|
||||||
|
${state.action.displayName}
|
||||||
|
</div>
|
||||||
|
${state.action.subText
|
||||||
|
? html`<div
|
||||||
|
class="pf-c-progress-stepper__step-description"
|
||||||
|
>
|
||||||
|
${state.action.subText}
|
||||||
|
</div>`
|
||||||
|
: html``}
|
||||||
|
</div>
|
||||||
|
</li>`;
|
||||||
|
})}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,12 +8,6 @@ import { WizardPage } from "./WizardPage";
|
||||||
|
|
||||||
@customElement("ak-wizard-page-form")
|
@customElement("ak-wizard-page-form")
|
||||||
export class FormWizardPage extends WizardPage {
|
export class FormWizardPage extends WizardPage {
|
||||||
_isValid = true;
|
|
||||||
|
|
||||||
isValid(): boolean {
|
|
||||||
return this._isValid;
|
|
||||||
}
|
|
||||||
|
|
||||||
nextCallback = async () => {
|
nextCallback = async () => {
|
||||||
const form = this.querySelector<Form<unknown>>("*");
|
const form = this.querySelector<Form<unknown>>("*");
|
||||||
if (!form) {
|
if (!form) {
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { ModalButton } from "@goauthentik/web/elements/buttons/ModalButton";
|
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";
|
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 PFWizard from "@patternfly/patternfly/components/Wizard/wizard.css";
|
||||||
|
|
||||||
import { WizardPage } from "./WizardPage";
|
export interface WizardAction {
|
||||||
|
displayName: string;
|
||||||
|
subText?: string;
|
||||||
|
run: () => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ApplyActionsSlot = "apply-actions";
|
||||||
|
|
||||||
@customElement("ak-wizard")
|
@customElement("ak-wizard")
|
||||||
export class Wizard extends ModalButton {
|
export class Wizard extends ModalButton {
|
||||||
|
@property({ type: Boolean })
|
||||||
|
canCancel = true;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
canBack = true;
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
header?: string;
|
header?: string;
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
isValid = false;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return super.styles.concat(PFWizard);
|
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 })
|
@property({ attribute: false })
|
||||||
steps: string[] = [];
|
actions: WizardAction[] = [];
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
_currentStep?: WizardPage;
|
_currentStep?: WizardPage;
|
||||||
|
@ -41,39 +85,63 @@ export class Wizard extends ModalButton {
|
||||||
return this._currentStep;
|
return this._currentStep;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSteps(...steps: string[]): void {
|
|
||||||
this.steps = steps;
|
|
||||||
this.requestUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
finalHandler: () => Promise<void> = () => {
|
finalHandler: () => Promise<void> = () => {
|
||||||
return Promise.resolve();
|
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<boolean>): void {
|
||||||
|
this.actions.unshift({
|
||||||
|
displayName,
|
||||||
|
run,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add action at the end of the list
|
||||||
|
*/
|
||||||
|
addActionAfter(displayName: string, run: () => Promise<boolean>): void {
|
||||||
|
this.actions.push({
|
||||||
|
displayName,
|
||||||
|
run,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
renderModalInner(): TemplateResult {
|
renderModalInner(): TemplateResult {
|
||||||
const firstPage = this.querySelector<WizardPage>(`[slot=${this.steps[0]}]`);
|
const firstPage = this.querySelector<WizardPage>(`[slot=${this.steps[0]}]`);
|
||||||
if (!this.currentStep && firstPage) {
|
if (!this.currentStep && firstPage) {
|
||||||
this.currentStep = firstPage;
|
this.currentStep = firstPage;
|
||||||
}
|
}
|
||||||
this.currentStep?.requestUpdate();
|
|
||||||
const currentIndex = this.currentStep ? this.steps.indexOf(this.currentStep.slot) : 0;
|
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`<div class="pf-c-wizard">
|
return html`<div class="pf-c-wizard">
|
||||||
<div class="pf-c-wizard__header">
|
<div class="pf-c-wizard__header">
|
||||||
<button
|
${this.canCancel
|
||||||
|
? html`<button
|
||||||
class="pf-c-button pf-m-plain pf-c-wizard__close"
|
class="pf-c-button pf-m-plain pf-c-wizard__close"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="${t`Close`}"
|
aria-label="${t`Close`}"
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
this.open = false;
|
this.reset();
|
||||||
const firstPage = this.querySelector<WizardPage>(`[slot=${this.steps[0]}]`);
|
|
||||||
if (firstPage) {
|
|
||||||
this.currentStep = firstPage;
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<i class="fas fa-times" aria-hidden="true"></i>
|
<i class="fas fa-times" aria-hidden="true"></i>
|
||||||
</button>
|
</button>`
|
||||||
|
: html``}
|
||||||
<h1 class="pf-c-title pf-m-3xl pf-c-wizard__title">${this.header}</h1>
|
<h1 class="pf-c-title pf-m-3xl pf-c-wizard__title">${this.header}</h1>
|
||||||
<p class="pf-c-wizard__description">${this.description}</p>
|
<p class="pf-c-wizard__description">${this.description}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -120,15 +188,15 @@ export class Wizard extends ModalButton {
|
||||||
<button
|
<button
|
||||||
class="pf-c-button pf-m-primary"
|
class="pf-c-button pf-m-primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
?disabled=${!this._currentStep?.isValid()}
|
?disabled=${!this.isValid}
|
||||||
@click=${async () => {
|
@click=${async () => {
|
||||||
const cb = await this.currentStep?.nextCallback();
|
const cb = await this.currentStep?.nextCallback();
|
||||||
if (!cb) {
|
if (!cb) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (currentIndex === this.steps.length - 1) {
|
if (lastPage) {
|
||||||
await this.finalHandler();
|
await this.finalHandler();
|
||||||
this.open = false;
|
this.reset();
|
||||||
} else {
|
} else {
|
||||||
const nextPage = this.querySelector<WizardPage>(
|
const nextPage = this.querySelector<WizardPage>(
|
||||||
`[slot=${this.steps[currentIndex + 1]}]`,
|
`[slot=${this.steps[currentIndex + 1]}]`,
|
||||||
|
@ -139,9 +207,10 @@ export class Wizard extends ModalButton {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
${currentIndex === this.steps.length - 1 ? t`Finish` : t`Next`}
|
${lastPage ? t`Finish` : t`Next`}
|
||||||
</button>
|
</button>
|
||||||
${(this.currentStep ? this.steps.indexOf(this.currentStep.slot) : 0) > 0
|
${(this.currentStep ? this.steps.indexOf(this.currentStep.slot) : 0) > 0 &&
|
||||||
|
this.canBack
|
||||||
? html`
|
? html`
|
||||||
<button
|
<button
|
||||||
class="pf-c-button pf-m-secondary"
|
class="pf-c-button pf-m-secondary"
|
||||||
|
@ -159,25 +228,34 @@ export class Wizard extends ModalButton {
|
||||||
</button>
|
</button>
|
||||||
`
|
`
|
||||||
: html``}
|
: html``}
|
||||||
<div class="pf-c-wizard__footer-cancel">
|
${this.canCancel
|
||||||
|
? html`<div class="pf-c-wizard__footer-cancel">
|
||||||
<button
|
<button
|
||||||
class="pf-c-button pf-m-link"
|
class="pf-c-button pf-m-link"
|
||||||
type="button"
|
type="button"
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
this.open = false;
|
this.reset();
|
||||||
const firstPage = this.querySelector<WizardPage>(
|
|
||||||
`[slot=${this.steps[0]}]`,
|
|
||||||
);
|
|
||||||
if (firstPage) {
|
|
||||||
this.currentStep = firstPage;
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
${t`Cancel`}
|
${t`Cancel`}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>`
|
||||||
|
: html``}
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<KeyUnknown> {
|
||||||
|
viewportCheck = false;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
nextDataCallback!: (data: KeyUnknown) => Promise<boolean>;
|
||||||
|
|
||||||
|
submit(): Promise<boolean> | 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<HTMLFormElement>("form");
|
||||||
|
if (!form) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const state = form.checkValidity();
|
||||||
|
this.host.isValid = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextCallback = async (): Promise<boolean> => {
|
||||||
|
const form = this.shadowRoot?.querySelector<WizardForm>("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<boolean> = async (data): Promise<boolean> => {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
renderForm(): TemplateResult {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
firstUpdated(): void {
|
||||||
|
this.inputCallback();
|
||||||
|
this.host.isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<ak-wizard-form
|
||||||
|
.nextDataCallback=${this.nextDataCallback}
|
||||||
|
@input=${() => this.inputCallback()}
|
||||||
|
>
|
||||||
|
${this.renderForm()}
|
||||||
|
</ak-wizard-form>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 { customElement, property } from "lit/decorators.js";
|
||||||
|
|
||||||
|
import AKGlobal from "../../authentik.css";
|
||||||
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
|
||||||
import { Wizard } from "./Wizard";
|
import { Wizard } from "./Wizard";
|
||||||
|
|
||||||
@customElement("ak-wizard-page")
|
@customElement("ak-wizard-page")
|
||||||
export class WizardPage extends LitElement {
|
export class WizardPage extends LitElement {
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [PFBase, AKGlobal];
|
||||||
|
}
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
sidebarLabel: () => string = () => {
|
sidebarLabel: () => string = () => {
|
||||||
return "UNNAMED";
|
return "UNNAMED";
|
||||||
};
|
};
|
||||||
|
|
||||||
isValid(): boolean {
|
|
||||||
return this._isValid;
|
|
||||||
}
|
|
||||||
|
|
||||||
get host(): Wizard {
|
get host(): Wizard {
|
||||||
return this.parentElement as Wizard;
|
return this.parentElement as Wizard;
|
||||||
}
|
}
|
||||||
|
|
||||||
_isValid = false;
|
|
||||||
|
|
||||||
activeCallback: () => Promise<void> = () => {
|
activeCallback: () => Promise<void> = () => {
|
||||||
|
this.host.isValid = false;
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
nextCallback: () => Promise<boolean> = async () => {
|
nextCallback: () => Promise<boolean> = async () => {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
|
@ -59,7 +59,7 @@ export class ApplicationForm extends ModelForm<Application, string> {
|
||||||
}
|
}
|
||||||
const c = await config();
|
const c = await config();
|
||||||
if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
|
if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
|
||||||
const icon = this.getFormFile();
|
const icon = this.getFormFiles()["metaIcon"];
|
||||||
if (icon || this.clearIcon) {
|
if (icon || this.clearIcon) {
|
||||||
await new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconCreate({
|
await new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconCreate({
|
||||||
slug: app.slug,
|
slug: app.slug,
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { getURLParam } from "@goauthentik/web/elements/router/RouteMatch";
|
||||||
import { TableColumn } from "@goauthentik/web/elements/table/Table";
|
import { TableColumn } from "@goauthentik/web/elements/table/Table";
|
||||||
import { TablePage } from "@goauthentik/web/elements/table/TablePage";
|
import { TablePage } from "@goauthentik/web/elements/table/TablePage";
|
||||||
import "@goauthentik/web/pages/applications/ApplicationForm";
|
import "@goauthentik/web/pages/applications/ApplicationForm";
|
||||||
|
import "@goauthentik/web/pages/applications/wizard/ApplicationWizard";
|
||||||
|
|
||||||
import { t } from "@lingui/macro";
|
import { t } from "@lingui/macro";
|
||||||
|
|
||||||
|
@ -81,7 +82,13 @@ export class ApplicationListPage extends TablePage<Application> {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSidebarAfter(): TemplateResult {
|
renderSidebarAfter(): TemplateResult {
|
||||||
return html`<div class="pf-c-sidebar__panel pf-m-width-25">
|
// 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`<ak-application-wizard
|
||||||
|
.open=${getURLParam("createWizard", false)}
|
||||||
|
.showButton=${false}
|
||||||
|
></ak-application-wizard>
|
||||||
|
<div class="pf-c-sidebar__panel pf-m-width-25">
|
||||||
<div class="pf-c-card">
|
<div class="pf-c-card">
|
||||||
<div class="pf-c-card__title">${t`About applications`}</div>
|
<div class="pf-c-card__title">${t`About applications`}</div>
|
||||||
<div class="pf-c-card__body">
|
<div class="pf-c-card__body">
|
||||||
|
@ -160,13 +167,11 @@ export class ApplicationListPage extends TablePage<Application> {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderObjectCreate(): TemplateResult {
|
renderObjectCreate(): TemplateResult {
|
||||||
return html`
|
return html`<ak-forms-modal .open=${getURLParam("createForm", false)}>
|
||||||
<ak-forms-modal .open=${getURLParam("createForm", false)}>
|
|
||||||
<span slot="submit"> ${t`Create`} </span>
|
<span slot="submit"> ${t`Create`} </span>
|
||||||
<span slot="header"> ${t`Create Application`} </span>
|
<span slot="header"> ${t`Create Application`} </span>
|
||||||
<ak-application-form slot="form"> </ak-application-form>
|
<ak-application-form slot="form"> </ak-application-form>
|
||||||
<button slot="trigger" class="pf-c-button pf-m-primary">${t`Create`}</button>
|
<button slot="trigger" class="pf-c-button pf-m-primary">${t`Create`}</button>
|
||||||
</ak-forms-modal>
|
</ak-forms-modal>`;
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<void> = () => {
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<ak-wizard
|
||||||
|
.open=${this.open}
|
||||||
|
.steps=${["ak-application-wizard-initial", "ak-application-wizard-type"]}
|
||||||
|
header=${t`New application`}
|
||||||
|
description=${t`Create a new application.`}
|
||||||
|
.finalHandler=${() => {
|
||||||
|
return this.finalHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
${this.showButton
|
||||||
|
? html`<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||||
|
${this.createText}
|
||||||
|
</button>`
|
||||||
|
: html``}
|
||||||
|
</ak-wizard>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<boolean> => {
|
||||||
|
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<boolean> => {
|
||||||
|
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`
|
||||||
|
<form class="pf-c-form pf-m-horizontal">
|
||||||
|
<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name">
|
||||||
|
<input type="text" value="" class="pf-c-form-control" required />
|
||||||
|
<p class="pf-c-form__helper-text">${t`Application's display Name.`}</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-group ?expanded=${true}>
|
||||||
|
<span slot="header"> ${t`Additional UI settings`} </span>
|
||||||
|
<div slot="body" class="pf-c-form">
|
||||||
|
<ak-form-element-horizontal label=${t`Description`} name="metaDescription">
|
||||||
|
<textarea class="pf-c-form-control"></textarea>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal label=${t`Publisher`} name="metaPublisher">
|
||||||
|
<input type="text" value="" class="pf-c-form-control" />
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
</div>
|
||||||
|
</ak-form-group>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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`<form class="pf-c-form pf-m-horizontal">
|
||||||
|
${this.applicationTypes.map((type) => {
|
||||||
|
return html`<div class="pf-c-radio">
|
||||||
|
<input
|
||||||
|
class="pf-c-radio__input"
|
||||||
|
type="radio"
|
||||||
|
name="type"
|
||||||
|
id=${type.component}
|
||||||
|
@change=${() => {
|
||||||
|
this.host.steps = [
|
||||||
|
"ak-application-wizard-initial",
|
||||||
|
"ak-application-wizard-type",
|
||||||
|
type.component,
|
||||||
|
];
|
||||||
|
this.host.isValid = true;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<label class="pf-c-radio__label" for=${type.component}>${type.name}</label>
|
||||||
|
<span class="pf-c-radio__description">${type.description}</span>
|
||||||
|
</div>`;
|
||||||
|
})}
|
||||||
|
</form>`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<boolean> => {
|
||||||
|
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<boolean> => {
|
||||||
|
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<boolean> => {
|
||||||
|
// 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`<form class="pf-c-form pf-m-horizontal">
|
||||||
|
<ak-form-element-horizontal label=${t`Base DN`} name="baseDN" ?required=${true}>
|
||||||
|
<input type="text" value="${defaultBaseDN}" class="pf-c-form-control" required />
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
</form> `;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<boolean> => {
|
||||||
|
this.host.state["link"] = data.link;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
renderForm(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<form class="pf-c-form pf-m-horizontal">
|
||||||
|
<ak-form-element-horizontal label=${t`Link`} ?required=${true} name="link">
|
||||||
|
<input type="text" value="" class="pf-c-form-control" required />
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${t`URL which will be opened when a user clicks on the application.`}
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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`<form class="pf-c-form pf-m-horizontal">
|
||||||
|
<p>
|
||||||
|
${t`This configuration can be used to authenticate to authentik with other APIs other otherwise programmatically.`}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
${t`By default, all service accounts can authenticate as this application, as long as they have a valid token of the type app-password.`}
|
||||||
|
</p>
|
||||||
|
</form> `;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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`<form class="pf-c-form pf-m-horizontal">
|
||||||
|
${this.applicationTypes.map((type) => {
|
||||||
|
return html`<div class="pf-c-radio">
|
||||||
|
<input
|
||||||
|
class="pf-c-radio__input"
|
||||||
|
type="radio"
|
||||||
|
name="type"
|
||||||
|
id=${type.component}
|
||||||
|
@change=${() => {
|
||||||
|
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;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<label class="pf-c-radio__label" for=${type.component}>${type.name}</label>
|
||||||
|
<span class="pf-c-radio__description">${type.description}</span>
|
||||||
|
</div>`;
|
||||||
|
})}
|
||||||
|
</form> `;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<boolean> => {
|
||||||
|
this.host.addActionBefore(t`Create provider`, async (): Promise<boolean> => {
|
||||||
|
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`<form class="pf-c-form pf-m-horizontal">
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${t`Authorization flow`}
|
||||||
|
?required=${true}
|
||||||
|
name="authorizationFlow"
|
||||||
|
>
|
||||||
|
<select class="pf-c-form-control">
|
||||||
|
${until(
|
||||||
|
new FlowsApi(DEFAULT_CONFIG)
|
||||||
|
.flowsInstancesList({
|
||||||
|
ordering: "slug",
|
||||||
|
designation: FlowsInstancesListDesignationEnum.Authorization,
|
||||||
|
})
|
||||||
|
.then((flows) => {
|
||||||
|
return flows.results.map((flow) => {
|
||||||
|
return html`<option value=${ifDefined(flow.pk)}>
|
||||||
|
${flow.name} (${flow.slug})
|
||||||
|
</option>`;
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
html`<option>${t`Loading...`}</option>`,
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${t`Flow used when users access this application.`}
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
</form>`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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`<form class="pf-c-form pf-m-horizontal">some stuff idk</form> `;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<boolean> => {
|
||||||
|
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<boolean> => {
|
||||||
|
// 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`<form class="pf-c-form pf-m-horizontal">
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${t`External domain`}
|
||||||
|
name="externalHost"
|
||||||
|
?required=${true}
|
||||||
|
>
|
||||||
|
<input type="text" value="" class="pf-c-form-control" required />
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${t`External domain you will be accessing the domain from.`}
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
</form> `;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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`<form class="pf-c-form pf-m-horizontal">
|
||||||
|
${this.applicationTypes.map((type) => {
|
||||||
|
return html`<div class="pf-c-radio">
|
||||||
|
<input
|
||||||
|
class="pf-c-radio__input"
|
||||||
|
type="radio"
|
||||||
|
name="type"
|
||||||
|
id=${type.component}
|
||||||
|
@change=${() => {
|
||||||
|
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;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<label class="pf-c-radio__label" for=${type.component}>${type.name}</label>
|
||||||
|
<span class="pf-c-radio__description">${type.description}</span>
|
||||||
|
</div>`;
|
||||||
|
})}
|
||||||
|
</form> `;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<boolean> => {
|
||||||
|
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<boolean> => {
|
||||||
|
// 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`<form class="pf-c-form pf-m-horizontal">
|
||||||
|
<ak-form-element-horizontal label=${t`ACS URL`} name="acsUrl" ?required=${true}>
|
||||||
|
<input type="text" value="" class="pf-c-form-control" required />
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${t`URL that authentik will redirect back to after successful authentication.`}
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
</form> `;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<boolean> => {
|
||||||
|
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<boolean> => {
|
||||||
|
// 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`<form class="pf-c-form pf-m-horizontal">
|
||||||
|
<ak-form-element-horizontal label=${t`Metadata`} name="metadata">
|
||||||
|
<input type="file" value="" class="pf-c-form-control" />
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
</form> `;
|
||||||
|
}
|
||||||
|
}
|
|
@ -54,7 +54,7 @@ export class FlowForm extends ModelForm<Flow, string> {
|
||||||
}
|
}
|
||||||
const c = await config();
|
const c = await config();
|
||||||
if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
|
if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
|
||||||
const icon = this.getFormFile();
|
const icon = this.getFormFiles()["background"];
|
||||||
if (icon || this.clearBackground) {
|
if (icon || this.clearBackground) {
|
||||||
await new FlowsApi(DEFAULT_CONFIG).flowsInstancesSetBackgroundCreate({
|
await new FlowsApi(DEFAULT_CONFIG).flowsInstancesSetBackgroundCreate({
|
||||||
slug: flow.slug,
|
slug: flow.slug,
|
||||||
|
|
|
@ -18,7 +18,7 @@ export class FlowImportForm extends Form<Flow> {
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
send = (data: Flow): Promise<void> => {
|
send = (data: Flow): Promise<void> => {
|
||||||
const file = this.getFormFile();
|
const file = this.getFormFiles()["flow"];
|
||||||
if (!file) {
|
if (!file) {
|
||||||
throw new SentryIgnoredError("No form data");
|
throw new SentryIgnoredError("No form data");
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ export class InitialServiceConnectionWizardPage extends WizardPage {
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFForm, PFButton, AKGlobal, PFRadio];
|
return [PFBase, PFForm, PFButton, AKGlobal, PFRadio];
|
||||||
}
|
}
|
||||||
|
sidebarLabel = () => t`Select type`;
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html`<form class="pf-c-form pf-m-horizontal">
|
return html`<form class="pf-c-form pf-m-horizontal">
|
||||||
|
@ -39,10 +40,10 @@ export class InitialServiceConnectionWizardPage extends WizardPage {
|
||||||
name="type"
|
name="type"
|
||||||
id=${`${type.component}-${type.modelName}`}
|
id=${`${type.component}-${type.modelName}`}
|
||||||
@change=${() => {
|
@change=${() => {
|
||||||
this.host.setSteps(
|
this.host.steps = [
|
||||||
"initial",
|
"initial",
|
||||||
`type-${type.component}-${type.modelName}`,
|
`type-${type.component}-${type.modelName}`,
|
||||||
);
|
];
|
||||||
this._isValid = true;
|
this._isValid = true;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -83,7 +84,6 @@ export class ServiceConnectionWizard extends LitElement {
|
||||||
>
|
>
|
||||||
<ak-service-connection-wizard-initial
|
<ak-service-connection-wizard-initial
|
||||||
slot="initial"
|
slot="initial"
|
||||||
.sidebarLabel=${() => t`Select type`}
|
|
||||||
.connectionTypes=${this.connectionTypes}
|
.connectionTypes=${this.connectionTypes}
|
||||||
>
|
>
|
||||||
</ak-service-connection-wizard-initial>
|
</ak-service-connection-wizard-initial>
|
||||||
|
|
|
@ -33,6 +33,7 @@ export class InitialPolicyWizardPage extends WizardPage {
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFForm, PFButton, AKGlobal, PFRadio];
|
return [PFBase, PFForm, PFButton, AKGlobal, PFRadio];
|
||||||
}
|
}
|
||||||
|
sidebarLabel = () => t`Select type`;
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html`<form class="pf-c-form pf-m-horizontal">
|
return html`<form class="pf-c-form pf-m-horizontal">
|
||||||
|
@ -44,10 +45,10 @@ export class InitialPolicyWizardPage extends WizardPage {
|
||||||
name="type"
|
name="type"
|
||||||
id=${`${type.component}-${type.modelName}`}
|
id=${`${type.component}-${type.modelName}`}
|
||||||
@change=${() => {
|
@change=${() => {
|
||||||
this.host.setSteps(
|
this.host.steps = [
|
||||||
"initial",
|
"initial",
|
||||||
`type-${type.component}-${type.modelName}`,
|
`type-${type.component}-${type.modelName}`,
|
||||||
);
|
];
|
||||||
this._isValid = true;
|
this._isValid = true;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -86,11 +87,7 @@ export class PolicyWizard extends LitElement {
|
||||||
header=${t`New policy`}
|
header=${t`New policy`}
|
||||||
description=${t`Create a new policy.`}
|
description=${t`Create a new policy.`}
|
||||||
>
|
>
|
||||||
<ak-policy-wizard-initial
|
<ak-policy-wizard-initial slot="initial" .policyTypes=${this.policyTypes}>
|
||||||
slot="initial"
|
|
||||||
.sidebarLabel=${() => t`Select type`}
|
|
||||||
.policyTypes=${this.policyTypes}
|
|
||||||
>
|
|
||||||
</ak-policy-wizard-initial>
|
</ak-policy-wizard-initial>
|
||||||
${this.policyTypes.map((type) => {
|
${this.policyTypes.map((type) => {
|
||||||
return html`
|
return html`
|
||||||
|
|
|
@ -31,6 +31,7 @@ export class InitialPropertyMappingWizardPage extends WizardPage {
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFForm, PFButton, AKGlobal, PFRadio];
|
return [PFBase, PFForm, PFButton, AKGlobal, PFRadio];
|
||||||
}
|
}
|
||||||
|
sidebarLabel = () => t`Select type`;
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html`<form class="pf-c-form pf-m-horizontal">
|
return html`<form class="pf-c-form pf-m-horizontal">
|
||||||
|
@ -42,10 +43,10 @@ export class InitialPropertyMappingWizardPage extends WizardPage {
|
||||||
name="type"
|
name="type"
|
||||||
id=${`${type.component}-${type.modelName}`}
|
id=${`${type.component}-${type.modelName}`}
|
||||||
@change=${() => {
|
@change=${() => {
|
||||||
this.host.setSteps(
|
this.host.steps = [
|
||||||
"initial",
|
"initial",
|
||||||
`type-${type.component}-${type.modelName}`,
|
`type-${type.component}-${type.modelName}`,
|
||||||
);
|
];
|
||||||
this._isValid = true;
|
this._isValid = true;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -83,7 +84,6 @@ export class PropertyMappingWizard extends LitElement {
|
||||||
>
|
>
|
||||||
<ak-property-mapping-wizard-initial
|
<ak-property-mapping-wizard-initial
|
||||||
slot="initial"
|
slot="initial"
|
||||||
.sidebarLabel=${() => t`Select type`}
|
|
||||||
.mappingTypes=${this.mappingTypes}
|
.mappingTypes=${this.mappingTypes}
|
||||||
>
|
>
|
||||||
</ak-property-mapping-wizard-initial>
|
</ak-property-mapping-wizard-initial>
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { property } from "lit/decorators.js";
|
||||||
import AKGlobal from "@goauthentik/web/authentik.css";
|
import AKGlobal from "@goauthentik/web/authentik.css";
|
||||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||||
import PFForm from "@patternfly/patternfly/components/Form/form.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 PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
|
||||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
|
||||||
|
@ -29,11 +30,28 @@ export class InitialProviderWizardPage extends WizardPage {
|
||||||
providerTypes: TypeCreate[] = [];
|
providerTypes: TypeCreate[] = [];
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFForm, PFButton, AKGlobal, PFRadio];
|
return [PFBase, PFForm, PFHint, PFButton, AKGlobal, PFRadio];
|
||||||
}
|
}
|
||||||
|
sidebarLabel = () => t`Select type`;
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html`<form class="pf-c-form pf-m-horizontal">
|
return html` <div class="pf-c-hint">
|
||||||
|
<div class="pf-c-hint__title">${t`Try the new application wizard`}</div>
|
||||||
|
<div class="pf-c-hint__body">
|
||||||
|
${t`The new application wizard greatly simplifies the steps required to create applications and providers.`}
|
||||||
|
</div>
|
||||||
|
<div class="pf-c-hint__footer">
|
||||||
|
<a
|
||||||
|
class="pf-c-button pf-m-link pf-m-inline"
|
||||||
|
href=${paramURL("/core/applications", {
|
||||||
|
createForm: true,
|
||||||
|
})}
|
||||||
|
>${t`Try it now`}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<form class="pf-c-form pf-m-horizontal">
|
||||||
${this.providerTypes.map((type) => {
|
${this.providerTypes.map((type) => {
|
||||||
return html`<div class="pf-c-radio">
|
return html`<div class="pf-c-radio">
|
||||||
<input
|
<input
|
||||||
|
@ -42,7 +60,7 @@ export class InitialProviderWizardPage extends WizardPage {
|
||||||
name="type"
|
name="type"
|
||||||
id=${type.component}
|
id=${type.component}
|
||||||
@change=${() => {
|
@change=${() => {
|
||||||
this.host.setSteps("initial", `type-${type.component}`);
|
this.host.steps = ["initial", `type-${type.component}`];
|
||||||
this._isValid = true;
|
this._isValid = true;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -87,11 +105,7 @@ export class ProviderWizard extends LitElement {
|
||||||
return this.finalHandler();
|
return this.finalHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ak-provider-wizard-initial
|
<ak-provider-wizard-initial slot="initial" .providerTypes=${this.providerTypes}>
|
||||||
slot="initial"
|
|
||||||
.sidebarLabel=${() => t`Select type`}
|
|
||||||
.providerTypes=${this.providerTypes}
|
|
||||||
>
|
|
||||||
</ak-provider-wizard-initial>
|
</ak-provider-wizard-initial>
|
||||||
${this.providerTypes.map((type) => {
|
${this.providerTypes.map((type) => {
|
||||||
return html`
|
return html`
|
||||||
|
|
|
@ -24,7 +24,7 @@ export class SAMLProviderImportForm extends Form<SAMLProvider> {
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
send = (data: SAMLProvider): Promise<void> => {
|
send = (data: SAMLProvider): Promise<void> => {
|
||||||
const file = this.getFormFile();
|
const file = this.getFormFiles()["metadata"];
|
||||||
if (!file) {
|
if (!file) {
|
||||||
throw new SentryIgnoredError("No form data");
|
throw new SentryIgnoredError("No form data");
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,7 @@ export class SAMLProviderImportForm extends Form<SAMLProvider> {
|
||||||
</p>
|
</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
|
|
||||||
<ak-form-element-horizontal label=${t`Metadata`} name="flow">
|
<ak-form-element-horizontal label=${t`Metadata`} name="metadata">
|
||||||
<input type="file" value="" class="pf-c-form-control" />
|
<input type="file" value="" class="pf-c-form-control" />
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
</form>`;
|
</form>`;
|
||||||
|
|
|
@ -30,6 +30,7 @@ export class InitialSourceWizardPage extends WizardPage {
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFForm, PFButton, AKGlobal, PFRadio];
|
return [PFBase, PFForm, PFButton, AKGlobal, PFRadio];
|
||||||
}
|
}
|
||||||
|
sidebarLabel = () => t`Select type`;
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html`<form class="pf-c-form pf-m-horizontal">
|
return html`<form class="pf-c-form pf-m-horizontal">
|
||||||
|
@ -41,10 +42,10 @@ export class InitialSourceWizardPage extends WizardPage {
|
||||||
name="type"
|
name="type"
|
||||||
id=${`${type.component}-${type.modelName}`}
|
id=${`${type.component}-${type.modelName}`}
|
||||||
@change=${() => {
|
@change=${() => {
|
||||||
this.host.setSteps(
|
this.host.steps = [
|
||||||
"initial",
|
"initial",
|
||||||
`type-${type.component}-${type.modelName}`,
|
`type-${type.component}-${type.modelName}`,
|
||||||
);
|
];
|
||||||
this._isValid = true;
|
this._isValid = true;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -80,11 +81,7 @@ export class SourceWizard extends LitElement {
|
||||||
header=${t`New source`}
|
header=${t`New source`}
|
||||||
description=${t`Create a new source.`}
|
description=${t`Create a new source.`}
|
||||||
>
|
>
|
||||||
<ak-source-wizard-initial
|
<ak-source-wizard-initial slot="initial" .sourceTypes=${this.sourceTypes}>
|
||||||
slot="initial"
|
|
||||||
.sidebarLabel=${() => t`Select type`}
|
|
||||||
.sourceTypes=${this.sourceTypes}
|
|
||||||
>
|
|
||||||
</ak-source-wizard-initial>
|
</ak-source-wizard-initial>
|
||||||
${this.sourceTypes.map((type) => {
|
${this.sourceTypes.map((type) => {
|
||||||
return html`
|
return html`
|
||||||
|
|
|
@ -42,6 +42,7 @@ import "./user_write/UserWriteStageForm.ts";
|
||||||
export class InitialStageWizardPage extends WizardPage {
|
export class InitialStageWizardPage extends WizardPage {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
stageTypes: TypeCreate[] = [];
|
stageTypes: TypeCreate[] = [];
|
||||||
|
sidebarLabel = () => t`Select type`;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFForm, PFButton, AKGlobal, PFRadio];
|
return [PFBase, PFForm, PFButton, AKGlobal, PFRadio];
|
||||||
|
@ -57,10 +58,10 @@ export class InitialStageWizardPage extends WizardPage {
|
||||||
name="type"
|
name="type"
|
||||||
id=${`${type.component}-${type.modelName}`}
|
id=${`${type.component}-${type.modelName}`}
|
||||||
@change=${() => {
|
@change=${() => {
|
||||||
this.host.setSteps(
|
this.host.steps = [
|
||||||
"initial",
|
"initial",
|
||||||
`type-${type.component}-${type.modelName}`,
|
`type-${type.component}-${type.modelName}`,
|
||||||
);
|
];
|
||||||
this._isValid = true;
|
this._isValid = true;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -99,11 +100,7 @@ export class StageWizard extends LitElement {
|
||||||
header=${t`New stage`}
|
header=${t`New stage`}
|
||||||
description=${t`Create a new stage.`}
|
description=${t`Create a new stage.`}
|
||||||
>
|
>
|
||||||
<ak-stage-wizard-initial
|
<ak-stage-wizard-initial slot="initial" .stageTypes=${this.stageTypes}>
|
||||||
slot="initial"
|
|
||||||
.sidebarLabel=${() => t`Select type`}
|
|
||||||
.stageTypes=${this.stageTypes}
|
|
||||||
>
|
|
||||||
</ak-stage-wizard-initial>
|
</ak-stage-wizard-initial>
|
||||||
${this.stageTypes.map((type) => {
|
${this.stageTypes.map((type) => {
|
||||||
return html`
|
return html`
|
||||||
|
|
|
@ -203,7 +203,7 @@ export class UserSettingsFlowExecutor extends LitElement implements StageHost {
|
||||||
.challenge=${this.challenge}
|
.challenge=${this.challenge}
|
||||||
></ak-user-stage-prompt>`;
|
></ak-user-stage-prompt>`;
|
||||||
default:
|
default:
|
||||||
console.log(
|
console.debug(
|
||||||
`authentik/user/flows: unsupported stage type ${this.challenge.component}`,
|
`authentik/user/flows: unsupported stage type ${this.challenge.component}`,
|
||||||
);
|
);
|
||||||
return html`
|
return html`
|
||||||
|
|
Reference in New Issue