web: ak-application-wizard-authentication-by-oauth, and many fixes!
1. Fixed `eventEmitter` so that if the detail object is a scalar, it will not attempt to "objectify" it. This was causing a bug where retrofitting the eventEmitter to some older components resulted in a detail of "some" being translated into ['s', 'o', 'm', 'e']. Not what is wanted. 2. Removed the "transitional form" from the existing components; they had a two-step where the web component class was just a wrapper around an independent rendering function. While this worked, it was only to make the case that they *were* independent rendering objects and could be supported with the right web component framework. We're halfway there now; the last step will be to transform the horizontal-element and various input CSS into componentized CSS, the way Patternfly-Elements is currently doing. 3. Fixed the `help` field so that it could take a string or a TemplateResult, and if the latter, don't bother wrapping it in the helper text functionality; just let it be its own thing. This supports the multi-line help of redirectURI as well as the `ak-utils-time-delta` capability. 4. Transform Oauth2ProviderForm to use the new components, to the best of our ability. Also used the `provider = this.wizard.provider` and `provider = this.instance` syntax to make the render function *completely portable*; it's the exact same text that is dropped into... 5. The complete `ak-application-wizard-authentication-by-oauth` component. They're so similar part of me wonders if I could push them both out to a common reference, or a collection of common references. Both components use the PropertyMapping and Sources, and both use the same collection of searches (Crypto, Flow). 6. A Storybook for `ak-application-wizard-authentication-by-oauth`, showing the works working. 7. New mocks for `authorizationFlow`, `propertyMappings`, and `hasJWKs`. This sequence has revealed a bug in the radio control. (It's always the radio control.) If the default doesn't match the current setting, the radio control doesn't behave as expected; it won't change when you fully expect that it should. I'll investigate how to harmonize those tomorrow.
This commit is contained in:
parent
f4807a3081
commit
0bba3ae97f
|
@ -1,5 +1,4 @@
|
||||||
import "@goauthentik/admin/applications/ApplicationForm";
|
import "@goauthentik/admin/applications/ApplicationForm";
|
||||||
import "@goauthentik/admin/applications/wizard/ApplicationWizard";
|
|
||||||
import { PFSize } from "@goauthentik/app/elements/Spinner";
|
import { PFSize } from "@goauthentik/app/elements/Spinner";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||||
|
@ -89,11 +88,14 @@ export class ApplicationListPage extends TablePage<Application> {
|
||||||
renderSidebarAfter(): TemplateResult {
|
renderSidebarAfter(): TemplateResult {
|
||||||
// Rendering the wizard with .open here, as if we set the attribute in
|
// 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
|
// renderObjectCreate() it'll open two wizards, since that function gets called twice
|
||||||
return html`<ak-application-wizard
|
|
||||||
|
/* Re-enable the wizard later:
|
||||||
|
<ak-application-wizard
|
||||||
.open=${getURLParam("createWizard", false)}
|
.open=${getURLParam("createWizard", false)}
|
||||||
.showButton=${false}
|
.showButton=${false}
|
||||||
></ak-application-wizard>
|
></ak-application-wizard>*/
|
||||||
<div class="pf-c-sidebar__panel pf-m-width-25">
|
|
||||||
|
return html` <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__body">
|
<div class="pf-c-card__body">
|
||||||
<ak-markdown .md=${MDApplication}></ak-markdown>
|
<ak-markdown .md=${MDApplication}></ak-markdown>
|
||||||
|
|
|
@ -0,0 +1,312 @@
|
||||||
|
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||||
|
import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search";
|
||||||
|
import {
|
||||||
|
clientTypeOptions,
|
||||||
|
issuerModeOptions,
|
||||||
|
redirectUriHelp,
|
||||||
|
subjectModeOptions,
|
||||||
|
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm";
|
||||||
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
|
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
|
||||||
|
import "@goauthentik/components/ak-number-input";
|
||||||
|
import "@goauthentik/components/ak-radio-input";
|
||||||
|
import "@goauthentik/components/ak-switch-input";
|
||||||
|
import "@goauthentik/components/ak-text-input";
|
||||||
|
import "@goauthentik/components/ak-textarea-input";
|
||||||
|
import "@goauthentik/elements/forms/FormGroup";
|
||||||
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
|
|
||||||
|
import { msg } from "@lit/localize";
|
||||||
|
import { customElement, state } from "@lit/reactive-element/decorators.js";
|
||||||
|
import { html, nothing } from "lit";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ClientTypeEnum,
|
||||||
|
FlowsInstancesListDesignationEnum,
|
||||||
|
PropertymappingsApi,
|
||||||
|
SourcesApi,
|
||||||
|
} from "@goauthentik/api";
|
||||||
|
import type {
|
||||||
|
OAuth2Provider,
|
||||||
|
PaginatedOAuthSourceList,
|
||||||
|
PaginatedScopeMappingList,
|
||||||
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
|
import ApplicationWizardPageBase from "../ApplicationWizardPageBase";
|
||||||
|
|
||||||
|
@customElement("ak-application-wizard-authentication-by-oauth")
|
||||||
|
export class ApplicationWizardAuthenticationByOauth extends ApplicationWizardPageBase {
|
||||||
|
@state()
|
||||||
|
showClientSecret = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
propertyMappings?: PaginatedScopeMappingList;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
oauthSources?: PaginatedOAuthSourceList;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
new PropertymappingsApi(DEFAULT_CONFIG)
|
||||||
|
.propertymappingsScopeList({
|
||||||
|
ordering: "scope_name",
|
||||||
|
})
|
||||||
|
.then((propertyMappings: PaginatedScopeMappingList) => {
|
||||||
|
this.propertyMappings = propertyMappings;
|
||||||
|
});
|
||||||
|
|
||||||
|
new SourcesApi(DEFAULT_CONFIG)
|
||||||
|
.sourcesOauthList({
|
||||||
|
ordering: "name",
|
||||||
|
hasJwks: true,
|
||||||
|
})
|
||||||
|
.then((oauthSources: PaginatedOAuthSourceList) => {
|
||||||
|
this.oauthSources = oauthSources;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange(ev: InputEvent) {
|
||||||
|
if (!ev.target) {
|
||||||
|
console.warn(`Received event with no target: ${ev}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const target = ev.target as HTMLInputElement;
|
||||||
|
const value = target.type === "checkbox" ? target.checked : target.value;
|
||||||
|
this.dispatchWizardUpdate({
|
||||||
|
provider: {
|
||||||
|
...this.wizard.provider,
|
||||||
|
[target.name]: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const provider = this.wizard.provider as OAuth2Provider | undefined;
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
return html`<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
||||||
|
<ak-text-input
|
||||||
|
name="name"
|
||||||
|
label=${msg("Name")}
|
||||||
|
value=${ifDefined(provider?.name)}
|
||||||
|
required
|
||||||
|
></ak-text-input>
|
||||||
|
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
name="authenticationFlow"
|
||||||
|
label=${msg("Authentication flow")}
|
||||||
|
>
|
||||||
|
<ak-flow-search
|
||||||
|
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||||
|
.currentFlow=${provider?.authenticationFlow}
|
||||||
|
required
|
||||||
|
></ak-flow-search>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${msg("Flow used when a user access this provider and is not authenticated.")}
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
name="authorizationFlow"
|
||||||
|
label=${msg("Authorization flow")}
|
||||||
|
?required=${true}
|
||||||
|
>
|
||||||
|
<ak-flow-search
|
||||||
|
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||||
|
.currentFlow=${provider?.authorizationFlow}
|
||||||
|
required
|
||||||
|
></ak-flow-search>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${msg("Flow used when authorizing this provider.")}
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
|
||||||
|
<ak-form-group .expanded=${true}>
|
||||||
|
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||||
|
<div slot="body" class="pf-c-form">
|
||||||
|
<ak-radio-input
|
||||||
|
name="clientType"
|
||||||
|
label=${msg("Client type")}
|
||||||
|
.value=${provider?.clientType}
|
||||||
|
required
|
||||||
|
@change=${(ev: CustomEvent<ClientTypeEnum>) => {
|
||||||
|
this.showClientSecret = ev.detail !== ClientTypeEnum.Public;
|
||||||
|
}}
|
||||||
|
.options=${clientTypeOptions}
|
||||||
|
>
|
||||||
|
</ak-radio-input>
|
||||||
|
<ak-text-input
|
||||||
|
name="clientId"
|
||||||
|
label=${msg("Client ID")}
|
||||||
|
value="${first(
|
||||||
|
provider?.clientId,
|
||||||
|
randomString(40, ascii_letters + digits)
|
||||||
|
)}"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</ak-text-input>
|
||||||
|
<ak-text-input
|
||||||
|
name="clientSecret"
|
||||||
|
label=${msg("Client Secret")}
|
||||||
|
value="${first(
|
||||||
|
provider?.clientSecret,
|
||||||
|
randomString(128, ascii_letters + digits)
|
||||||
|
)}"
|
||||||
|
?hidden=${!this.showClientSecret}
|
||||||
|
>
|
||||||
|
</ak-text-input>
|
||||||
|
<ak-textarea-input
|
||||||
|
name="redirectUris"
|
||||||
|
label=${msg("Redirect URIs/Origins (RegEx)")}
|
||||||
|
.value=${provider?.redirectUris}
|
||||||
|
.bighelp=${redirectUriHelp}
|
||||||
|
>
|
||||||
|
</ak-textarea-input>
|
||||||
|
|
||||||
|
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
|
||||||
|
<ak-crypto-certificate-search
|
||||||
|
certificate=${ifDefined(provider?.signingKey ?? nothing)}
|
||||||
|
name="certificate"
|
||||||
|
singleton
|
||||||
|
>
|
||||||
|
</ak-crypto-certificate-search>
|
||||||
|
<p class="pf-c-form__helper-text">${msg("Key used to sign the tokens.")}</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
</div>
|
||||||
|
</ak-form-group>
|
||||||
|
|
||||||
|
<ak-form-group>
|
||||||
|
<span slot="header"> ${msg("Advanced protocol settings")} </span>
|
||||||
|
<div slot="body" class="pf-c-form">
|
||||||
|
<ak-text-input
|
||||||
|
name="accessCodeValidity"
|
||||||
|
label=${msg("Access code validity")}
|
||||||
|
required
|
||||||
|
value="${first(provider?.accessCodeValidity, "minutes=1")}"
|
||||||
|
.bighelp=${html`<p class="pf-c-form__helper-text">
|
||||||
|
${msg("Configure how long access codes are valid for.")}
|
||||||
|
</p>
|
||||||
|
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||||
|
>
|
||||||
|
</ak-text-input>
|
||||||
|
<ak-text-input
|
||||||
|
name="accessTokenValidity"
|
||||||
|
label=${msg("Access Token validity")}
|
||||||
|
value="${first(provider?.accessTokenValidity, "minutes=5")}"
|
||||||
|
required
|
||||||
|
.bighelp=${html` <p class="pf-c-form__helper-text">
|
||||||
|
${msg("Configure how long access tokens are valid for.")}
|
||||||
|
</p>
|
||||||
|
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||||
|
>
|
||||||
|
</ak-text-input>
|
||||||
|
|
||||||
|
<ak-text-input
|
||||||
|
name="refreshTokenValidity"
|
||||||
|
label=${msg("Refresh Token validity")}
|
||||||
|
value="${first(provider?.refreshTokenValidity, "days=30")}"
|
||||||
|
?required=${true}
|
||||||
|
.bighelp=${html` <p class="pf-c-form__helper-text">
|
||||||
|
${msg("Configure how long refresh tokens are valid for.")}
|
||||||
|
</p>
|
||||||
|
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||||
|
>
|
||||||
|
</ak-text-input>
|
||||||
|
<ak-form-element-horizontal label=${msg("Scopes")} name="propertyMappings">
|
||||||
|
<select class="pf-c-form-control" multiple>
|
||||||
|
${this.propertyMappings?.results.map((scope) => {
|
||||||
|
let selected = false;
|
||||||
|
if (!provider?.propertyMappings) {
|
||||||
|
selected =
|
||||||
|
scope.managed?.startsWith(
|
||||||
|
"goauthentik.io/providers/oauth2/scope-"
|
||||||
|
) || false;
|
||||||
|
} else {
|
||||||
|
selected = Array.from(provider?.propertyMappings).some(
|
||||||
|
(su) => {
|
||||||
|
return su == scope.pk;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return html`<option
|
||||||
|
value=${ifDefined(scope.pk)}
|
||||||
|
?selected=${selected}
|
||||||
|
>
|
||||||
|
${scope.name}
|
||||||
|
</option>`;
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${msg(
|
||||||
|
"Select which scopes can be used by the client. The client still has to specify the scope to access the data."
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${msg("Hold control/command to select multiple items.")}
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-radio-input
|
||||||
|
name="subMode"
|
||||||
|
label=${msg("Subject mode")}
|
||||||
|
required
|
||||||
|
.options=${subjectModeOptions}
|
||||||
|
.value=${provider?.subMode}
|
||||||
|
help=${msg(
|
||||||
|
"Configure what data should be used as unique User Identifier. For most cases, the default should be fine."
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
</ak-radio-input>
|
||||||
|
<ak-switch-input name="includeClaimsInIdToken">
|
||||||
|
label=${msg("Include claims in id_token")}
|
||||||
|
?checked=${first(provider?.includeClaimsInIdToken, true)}
|
||||||
|
help=${msg(
|
||||||
|
"Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint."
|
||||||
|
)}></ak-switch-input
|
||||||
|
>
|
||||||
|
<ak-radio-input
|
||||||
|
name="issuerMode"
|
||||||
|
label=${msg("Issuer mode")}
|
||||||
|
required
|
||||||
|
.options=${issuerModeOptions}
|
||||||
|
.value=${provider?.issuerMode}
|
||||||
|
help=${msg(
|
||||||
|
"Configure how the issuer field of the ID Token should be filled."
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
</ak-radio-input>
|
||||||
|
</div>
|
||||||
|
</ak-form-group>
|
||||||
|
|
||||||
|
<ak-form-group>
|
||||||
|
<span slot="header">${msg("Machine-to-Machine authentication settings")}</span>
|
||||||
|
<div slot="body" class="pf-c-form">
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${msg("Trusted OIDC Sources")}
|
||||||
|
name="jwksSources"
|
||||||
|
>
|
||||||
|
<select class="pf-c-form-control" multiple>
|
||||||
|
${this.oauthSources?.results.map((source) => {
|
||||||
|
const selected = (provider?.jwksSources || []).some((su) => {
|
||||||
|
return su == source.pk;
|
||||||
|
});
|
||||||
|
return html`<option value=${source.pk} ?selected=${selected}>
|
||||||
|
${source.name} (${source.slug})
|
||||||
|
</option>`;
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${msg(
|
||||||
|
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider."
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${msg("Hold control/command to select multiple items.")}
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
</div>
|
||||||
|
</ak-form-group>
|
||||||
|
</form>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApplicationWizardAuthenticationByOauth;
|
|
@ -7,11 +7,15 @@ import AkApplicationWizardApplicationDetails from "../ak-application-wizard-appl
|
||||||
import "../ak-application-wizard-authentication-method-choice";
|
import "../ak-application-wizard-authentication-method-choice";
|
||||||
import "../ak-application-wizard-context";
|
import "../ak-application-wizard-context";
|
||||||
import "../ldap/ak-application-wizard-authentication-by-ldap";
|
import "../ldap/ak-application-wizard-authentication-by-ldap";
|
||||||
|
import "../oauth/ak-application-wizard-authentication-by-oauth";
|
||||||
import "./ak-application-context-display-for-test";
|
import "./ak-application-context-display-for-test";
|
||||||
import {
|
import {
|
||||||
dummyAuthenticationFlowsSearch,
|
dummyAuthenticationFlowsSearch,
|
||||||
|
dummyAuthorizationFlowsSearch,
|
||||||
dummyCoreGroupsSearch,
|
dummyCoreGroupsSearch,
|
||||||
dummyCryptoCertsSearch,
|
dummyCryptoCertsSearch,
|
||||||
|
dummyHasJwks,
|
||||||
|
dummyPropertyMappings,
|
||||||
dummyProviderTypesList,
|
dummyProviderTypesList,
|
||||||
} from "./samples";
|
} from "./samples";
|
||||||
|
|
||||||
|
@ -50,6 +54,26 @@ const metadata: Meta<AkApplicationWizardApplicationDetails> = {
|
||||||
status: 200,
|
status: 200,
|
||||||
response: dummyAuthenticationFlowsSearch,
|
response: dummyAuthenticationFlowsSearch,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
url: "/api/v3/flows/instances/?designation=authorization&ordering=slug",
|
||||||
|
method: "GET",
|
||||||
|
status: 200,
|
||||||
|
response: dummyAuthorizationFlowsSearch,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "/api/v3/propertymappings/scope/?ordering=scope_name",
|
||||||
|
method: "GET",
|
||||||
|
status: 200,
|
||||||
|
response: dummyPropertyMappings,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "/api/v3/sources/oauth/?has_jwks=true&ordering=name",
|
||||||
|
method: "GET",
|
||||||
|
status: 200,
|
||||||
|
response: dummyHasJwks,
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -60,10 +84,9 @@ function injectTheme() {
|
||||||
if (!document.body.classList.contains(LIGHT)) {
|
if (!document.body.classList.contains(LIGHT)) {
|
||||||
document.body.classList.add(LIGHT);
|
document.body.classList.add(LIGHT);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default metadata;
|
export default metadata;
|
||||||
|
|
||||||
const container = (testItem: TemplateResult) => {
|
const container = (testItem: TemplateResult) => {
|
||||||
|
@ -79,7 +102,7 @@ const container = (testItem: TemplateResult) => {
|
||||||
</style>
|
</style>
|
||||||
${testItem}
|
${testItem}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const PageOne = () => {
|
export const PageOne = () => {
|
||||||
return container(
|
return container(
|
||||||
|
@ -110,3 +133,14 @@ export const PageThreeLdap = () => {
|
||||||
</ak-application-wizard-context>`,
|
</ak-application-wizard-context>`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const PageThreeOauth2 = () => {
|
||||||
|
return container(
|
||||||
|
html`<ak-application-wizard-context>
|
||||||
|
<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>
|
||||||
|
<hr />
|
||||||
|
<ak-application-context-display-for-test></ak-application-context-display-for-test>
|
||||||
|
</ak-application-wizard-context>`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -82,6 +82,58 @@ export const dummyAuthenticationFlowsSearch = {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const dummyAuthorizationFlowsSearch = {
|
||||||
|
pagination: {
|
||||||
|
next: 0,
|
||||||
|
previous: 0,
|
||||||
|
count: 2,
|
||||||
|
current: 1,
|
||||||
|
total_pages: 1,
|
||||||
|
start_index: 1,
|
||||||
|
end_index: 2,
|
||||||
|
},
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
pk: "9e01f011-8b3f-43d6-bedf-c29be5f3a428",
|
||||||
|
policybindingmodel_ptr_id: "14179ef8-2726-4027-9e2f-dc99185199bf",
|
||||||
|
name: "Authorize Application",
|
||||||
|
slug: "default-provider-authorization-explicit-consent",
|
||||||
|
title: "Redirecting to %(app)s",
|
||||||
|
designation: "authorization",
|
||||||
|
background: "/static/dist/assets/images/flow_background.jpg",
|
||||||
|
stages: ["ed5f015f-82b9-450f-addf-1e9d21d8dda3"],
|
||||||
|
policies: [],
|
||||||
|
cache_count: 0,
|
||||||
|
policy_engine_mode: "any",
|
||||||
|
compatibility_mode: false,
|
||||||
|
export_url:
|
||||||
|
"/api/v3/flows/instances/default-provider-authorization-explicit-consent/export/",
|
||||||
|
layout: "stacked",
|
||||||
|
denied_action: "message_continue",
|
||||||
|
authentication: "require_authenticated",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pk: "06f11ee3-cbe3-456d-81df-fae4c0a62951",
|
||||||
|
policybindingmodel_ptr_id: "686e6539-8b9f-473e-9f54-e05cc207dd2a",
|
||||||
|
name: "Authorize Application",
|
||||||
|
slug: "default-provider-authorization-implicit-consent",
|
||||||
|
title: "Redirecting to %(app)s",
|
||||||
|
designation: "authorization",
|
||||||
|
background: "/static/dist/assets/images/flow_background.jpg",
|
||||||
|
stages: [],
|
||||||
|
policies: [],
|
||||||
|
cache_count: 0,
|
||||||
|
policy_engine_mode: "any",
|
||||||
|
compatibility_mode: false,
|
||||||
|
export_url:
|
||||||
|
"/api/v3/flows/instances/default-provider-authorization-implicit-consent/export/",
|
||||||
|
layout: "stacked",
|
||||||
|
denied_action: "message_continue",
|
||||||
|
authentication: "require_authenticated",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
export const dummyCoreGroupsSearch = {
|
export const dummyCoreGroupsSearch = {
|
||||||
pagination: {
|
pagination: {
|
||||||
next: 0,
|
next: 0,
|
||||||
|
@ -121,6 +173,84 @@ export const dummyCoreGroupsSearch = {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const dummyPropertyMappings = {
|
||||||
|
pagination: {
|
||||||
|
next: 0,
|
||||||
|
previous: 0,
|
||||||
|
count: 4,
|
||||||
|
current: 1,
|
||||||
|
total_pages: 1,
|
||||||
|
start_index: 1,
|
||||||
|
end_index: 4,
|
||||||
|
},
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
pk: "30d87af7-9d9d-4292-873e-a52145ba4bcb",
|
||||||
|
managed: "goauthentik.io/providers/proxy/scope-proxy",
|
||||||
|
name: "authentik default OAuth Mapping: Proxy outpost",
|
||||||
|
expression:
|
||||||
|
'# This mapping is used by the authentik proxy. It passes extra user attributes,\n# which are used for example for the HTTP-Basic Authentication mapping.\nreturn {\n "ak_proxy": {\n "user_attributes": request.user.group_attributes(request),\n "is_superuser": request.user.is_superuser,\n }\n}',
|
||||||
|
component: "ak-property-mapping-scope-form",
|
||||||
|
verbose_name: "Scope Mapping",
|
||||||
|
verbose_name_plural: "Scope Mappings",
|
||||||
|
meta_model_name: "authentik_providers_oauth2.scopemapping",
|
||||||
|
scope_name: "ak_proxy",
|
||||||
|
description: "authentik Proxy - User information",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pk: "3e3751ed-a24c-4f47-a051-e2e05b5cd306",
|
||||||
|
managed: "goauthentik.io/providers/oauth2/scope-email",
|
||||||
|
name: "authentik default OAuth Mapping: OpenID 'email'",
|
||||||
|
expression: 'return {\n "email": request.user.email,\n "email_verified": True\n}',
|
||||||
|
component: "ak-property-mapping-scope-form",
|
||||||
|
verbose_name: "Scope Mapping",
|
||||||
|
verbose_name_plural: "Scope Mappings",
|
||||||
|
meta_model_name: "authentik_providers_oauth2.scopemapping",
|
||||||
|
scope_name: "email",
|
||||||
|
description: "Email address",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pk: "81c5e330-d8a0-45cd-9cad-e6a49a9c428f",
|
||||||
|
managed: "goauthentik.io/providers/oauth2/scope-openid",
|
||||||
|
name: "authentik default OAuth Mapping: OpenID 'openid'",
|
||||||
|
expression:
|
||||||
|
"# This scope is required by the OpenID-spec, and must as such exist in authentik.\n# The scope by itself does not grant any information\nreturn {}",
|
||||||
|
component: "ak-property-mapping-scope-form",
|
||||||
|
verbose_name: "Scope Mapping",
|
||||||
|
verbose_name_plural: "Scope Mappings",
|
||||||
|
meta_model_name: "authentik_providers_oauth2.scopemapping",
|
||||||
|
scope_name: "openid",
|
||||||
|
description: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pk: "7ad9cd6f-bcc8-425d-b7c2-c7c4592a1b36",
|
||||||
|
managed: "goauthentik.io/providers/oauth2/scope-profile",
|
||||||
|
name: "authentik default OAuth Mapping: OpenID 'profile'",
|
||||||
|
expression:
|
||||||
|
'return {\n # Because authentik only saves the user\'s full name, and has no concept of first and last names,\n # the full name is used as given name.\n # You can override this behaviour in custom mappings, i.e. `request.user.name.split(" ")`\n "name": request.user.name,\n "given_name": request.user.name,\n "preferred_username": request.user.username,\n "nickname": request.user.username,\n # groups is not part of the official userinfo schema, but is a quasi-standard\n "groups": [group.name for group in request.user.ak_groups.all()],\n}',
|
||||||
|
component: "ak-property-mapping-scope-form",
|
||||||
|
verbose_name: "Scope Mapping",
|
||||||
|
verbose_name_plural: "Scope Mappings",
|
||||||
|
meta_model_name: "authentik_providers_oauth2.scopemapping",
|
||||||
|
scope_name: "profile",
|
||||||
|
description: "General Profile Information",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dummyHasJwks = {
|
||||||
|
pagination: {
|
||||||
|
next: 0,
|
||||||
|
previous: 0,
|
||||||
|
count: 0,
|
||||||
|
current: 1,
|
||||||
|
total_pages: 1,
|
||||||
|
start_index: 0,
|
||||||
|
end_index: 0,
|
||||||
|
},
|
||||||
|
results: [],
|
||||||
|
};
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
export const dummyProviderTypesList = [
|
export const dummyProviderTypesList = [
|
||||||
["LDAP Provider", "ldapprovider",
|
["LDAP Provider", "ldapprovider",
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
|
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
|
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
|
||||||
|
import "@goauthentik/components/ak-radio-input";
|
||||||
|
import "@goauthentik/components/ak-text-input";
|
||||||
|
import "@goauthentik/components/ak-textarea-input";
|
||||||
import "@goauthentik/elements/forms/FormGroup";
|
import "@goauthentik/elements/forms/FormGroup";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||||
|
@ -9,15 +13,12 @@ import "@goauthentik/elements/forms/SearchSelect";
|
||||||
import "@goauthentik/elements/utils/TimeDeltaHelp";
|
import "@goauthentik/elements/utils/TimeDeltaHelp";
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { TemplateResult, html } from "lit";
|
import { TemplateResult, html, nothing } from "lit";
|
||||||
import { customElement, state } from "lit/decorators.js";
|
import { customElement, state } from "lit/decorators.js";
|
||||||
import { ifDefined } from "lit/directives/if-defined.js";
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CertificateKeyPair,
|
|
||||||
ClientTypeEnum,
|
ClientTypeEnum,
|
||||||
CryptoApi,
|
|
||||||
CryptoCertificatekeypairsListRequest,
|
|
||||||
FlowsInstancesListDesignationEnum,
|
FlowsInstancesListDesignationEnum,
|
||||||
IssuerModeEnum,
|
IssuerModeEnum,
|
||||||
OAuth2Provider,
|
OAuth2Provider,
|
||||||
|
@ -29,6 +30,91 @@ import {
|
||||||
SubModeEnum,
|
SubModeEnum,
|
||||||
} from "@goauthentik/api";
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
|
export const clientTypeOptions = [
|
||||||
|
{
|
||||||
|
label: msg("Confidential"),
|
||||||
|
value: ClientTypeEnum.Confidential,
|
||||||
|
default: true,
|
||||||
|
description: html`${msg(
|
||||||
|
"Confidential clients are capable of maintaining the confidentiality of their credentials such as client secrets",
|
||||||
|
)}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: msg("Public"),
|
||||||
|
value: ClientTypeEnum.Public,
|
||||||
|
description: html`${msg(
|
||||||
|
"Public clients are incapable of maintaining the confidentiality and should use methods like PKCE. ",
|
||||||
|
)}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const subjectModeOptions = [
|
||||||
|
{
|
||||||
|
label: msg("Based on the User's hashed ID"),
|
||||||
|
value: SubModeEnum.HashedUserId,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: msg("Based on the User's ID"),
|
||||||
|
value: SubModeEnum.UserId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: msg("Based on the User's UUID"),
|
||||||
|
value: SubModeEnum.UserUuid,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: msg("Based on the User's username"),
|
||||||
|
value: SubModeEnum.UserUsername,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: msg("Based on the User's Email"),
|
||||||
|
value: SubModeEnum.UserEmail,
|
||||||
|
description: html`${msg("This is recommended over the UPN mode.")}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: msg("Based on the User's UPN"),
|
||||||
|
value: SubModeEnum.UserUpn,
|
||||||
|
description: html`${msg(
|
||||||
|
"Requires the user to have a 'upn' attribute set, and falls back to hashed user ID. Use this mode only if you have different UPN and Mail domains.",
|
||||||
|
)}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const issuerModeOptions = [
|
||||||
|
{
|
||||||
|
label: msg("Each provider has a different issuer, based on the application slug"),
|
||||||
|
value: IssuerModeEnum.PerProvider,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: msg("Same identifier is used for all providers"),
|
||||||
|
value: IssuerModeEnum.Global,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const redirectUriHelpMessages = [
|
||||||
|
msg(
|
||||||
|
"Valid redirect URLs after a successful authorization flow. Also specify any origins here for Implicit flows.",
|
||||||
|
),
|
||||||
|
msg(
|
||||||
|
"If no explicit redirect URIs are specified, the first successfully used redirect URI will be saved.",
|
||||||
|
),
|
||||||
|
msg(
|
||||||
|
'To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const redirectUriHelp = html`${redirectUriHelpMessages.map(
|
||||||
|
(m) => html`<p class="pf-c-form__helper-text">${m}</p>`,
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form page for OAuth2 Authentication Method
|
||||||
|
*
|
||||||
|
* @element ak-provider-oauth2-form
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
@customElement("ak-provider-oauth2-form")
|
@customElement("ak-provider-oauth2-form")
|
||||||
export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
|
export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
|
||||||
propertyMappings?: PaginatedScopeMappingList;
|
propertyMappings?: PaginatedScopeMappingList;
|
||||||
|
@ -79,22 +165,23 @@ export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderForm(): TemplateResult {
|
renderForm(): TemplateResult {
|
||||||
|
const provider = this.instance;
|
||||||
|
|
||||||
return html`<form class="pf-c-form pf-m-horizontal">
|
return html`<form class="pf-c-form pf-m-horizontal">
|
||||||
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
<ak-text-input
|
||||||
<input
|
name="name"
|
||||||
type="text"
|
label=${msg("Name")}
|
||||||
value="${ifDefined(this.instance?.name)}"
|
value=${ifDefined(provider?.name)}
|
||||||
class="pf-c-form-control"
|
|
||||||
required
|
required
|
||||||
/>
|
></ak-text-input>
|
||||||
</ak-form-element-horizontal>
|
|
||||||
<ak-form-element-horizontal
|
<ak-form-element-horizontal
|
||||||
label=${msg("Authentication flow")}
|
|
||||||
name="authenticationFlow"
|
name="authenticationFlow"
|
||||||
|
label=${msg("Authentication flow")}
|
||||||
>
|
>
|
||||||
<ak-flow-search
|
<ak-flow-search
|
||||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||||
.currentFlow=${this.instance?.authenticationFlow}
|
.currentFlow=${provider?.authenticationFlow}
|
||||||
required
|
required
|
||||||
></ak-flow-search>
|
></ak-flow-search>
|
||||||
<p class="pf-c-form__helper-text">
|
<p class="pf-c-form__helper-text">
|
||||||
|
@ -102,13 +189,13 @@ export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
|
||||||
</p>
|
</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
<ak-form-element-horizontal
|
||||||
|
name="authorizationFlow"
|
||||||
label=${msg("Authorization flow")}
|
label=${msg("Authorization flow")}
|
||||||
?required=${true}
|
?required=${true}
|
||||||
name="authorizationFlow"
|
|
||||||
>
|
>
|
||||||
<ak-flow-search
|
<ak-flow-search
|
||||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||||
.currentFlow=${this.instance?.authorizationFlow}
|
.currentFlow=${provider?.authorizationFlow}
|
||||||
required
|
required
|
||||||
></ak-flow-search>
|
></ak-flow-search>
|
||||||
<p class="pf-c-form__helper-text">
|
<p class="pf-c-form__helper-text">
|
||||||
|
@ -119,129 +206,52 @@ export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
|
||||||
<ak-form-group .expanded=${true}>
|
<ak-form-group .expanded=${true}>
|
||||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||||
<div slot="body" class="pf-c-form">
|
<div slot="body" class="pf-c-form">
|
||||||
<ak-form-element-horizontal
|
<ak-radio-input
|
||||||
label=${msg("Client type")}
|
|
||||||
?required=${true}
|
|
||||||
name="clientType"
|
name="clientType"
|
||||||
>
|
label=${msg("Client type")}
|
||||||
<ak-radio
|
.value=${provider?.clientType}
|
||||||
|
required
|
||||||
@change=${(ev: CustomEvent<ClientTypeEnum>) => {
|
@change=${(ev: CustomEvent<ClientTypeEnum>) => {
|
||||||
if (ev.detail === ClientTypeEnum.Public) {
|
this.showClientSecret = ev.detail !== ClientTypeEnum.Public;
|
||||||
this.showClientSecret = false;
|
|
||||||
} else {
|
|
||||||
this.showClientSecret = true;
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
.options=${[
|
.options=${clientTypeOptions}
|
||||||
{
|
|
||||||
label: msg("Confidential"),
|
|
||||||
value: ClientTypeEnum.Confidential,
|
|
||||||
default: true,
|
|
||||||
description: html`${msg(
|
|
||||||
"Confidential clients are capable of maintaining the confidentiality of their credentials such as client secrets",
|
|
||||||
)}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: msg("Public"),
|
|
||||||
value: ClientTypeEnum.Public,
|
|
||||||
description: html`${msg(
|
|
||||||
"Public clients are incapable of maintaining the confidentiality and should use methods like PKCE. ",
|
|
||||||
)}`,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
.value=${this.instance?.clientType}
|
|
||||||
>
|
>
|
||||||
</ak-radio>
|
</ak-radio-input>
|
||||||
</ak-form-element-horizontal>
|
<ak-text-input
|
||||||
<ak-form-element-horizontal
|
|
||||||
label=${msg("Client ID")}
|
|
||||||
?required=${true}
|
|
||||||
name="clientId"
|
name="clientId"
|
||||||
>
|
label=${msg("Client ID")}
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value="${first(
|
value="${first(
|
||||||
this.instance?.clientId,
|
provider?.clientId,
|
||||||
randomString(40, ascii_letters + digits),
|
randomString(40, ascii_letters + digits),
|
||||||
)}"
|
)}"
|
||||||
class="pf-c-form-control"
|
|
||||||
required
|
required
|
||||||
/>
|
|
||||||
</ak-form-element-horizontal>
|
|
||||||
<ak-form-element-horizontal
|
|
||||||
?hidden=${!this.showClientSecret}
|
|
||||||
label=${msg("Client Secret")}
|
|
||||||
name="clientSecret"
|
|
||||||
>
|
>
|
||||||
<input
|
</ak-text-input>
|
||||||
type="text"
|
<ak-text-input
|
||||||
|
name="clientSecret"
|
||||||
|
label=${msg("Client Secret")}
|
||||||
value="${first(
|
value="${first(
|
||||||
this.instance?.clientSecret,
|
provider?.clientSecret,
|
||||||
randomString(128, ascii_letters + digits),
|
randomString(128, ascii_letters + digits),
|
||||||
)}"
|
)}"
|
||||||
class="pf-c-form-control"
|
?hidden=${!this.showClientSecret}
|
||||||
/>
|
>
|
||||||
</ak-form-element-horizontal>
|
</ak-text-input>
|
||||||
<ak-form-element-horizontal
|
<ak-textarea-input
|
||||||
label=${msg("Redirect URIs/Origins (RegEx)")}
|
|
||||||
name="redirectUris"
|
name="redirectUris"
|
||||||
|
label=${msg("Redirect URIs/Origins (RegEx)")}
|
||||||
|
.value=${provider?.redirectUris}
|
||||||
|
.bighelp=${redirectUriHelp}
|
||||||
>
|
>
|
||||||
<textarea class="pf-c-form-control">
|
</ak-textarea-input>
|
||||||
${this.instance?.redirectUris}</textarea
|
|
||||||
>
|
|
||||||
<p class="pf-c-form__helper-text">
|
|
||||||
${msg(
|
|
||||||
"Valid redirect URLs after a successful authorization flow. Also specify any origins here for Implicit flows.",
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<p class="pf-c-form__helper-text">
|
|
||||||
${msg(
|
|
||||||
"If no explicit redirect URIs are specified, the first successfully used redirect URI will be saved.",
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<p class="pf-c-form__helper-text">
|
|
||||||
${msg(
|
|
||||||
'To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.',
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</ak-form-element-horizontal>
|
|
||||||
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
|
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
|
||||||
<ak-search-select
|
<ak-crypto-certificate-search
|
||||||
.fetchObjects=${async (
|
certificate=${ifDefined(provider?.signingKey ?? nothing)}
|
||||||
query?: string,
|
name="certificate"
|
||||||
): Promise<CertificateKeyPair[]> => {
|
singleton
|
||||||
const args: CryptoCertificatekeypairsListRequest = {
|
|
||||||
ordering: "name",
|
|
||||||
hasKey: true,
|
|
||||||
includeDetails: false,
|
|
||||||
};
|
|
||||||
if (query !== undefined) {
|
|
||||||
args.search = query;
|
|
||||||
}
|
|
||||||
const certificates = await new CryptoApi(
|
|
||||||
DEFAULT_CONFIG,
|
|
||||||
).cryptoCertificatekeypairsList(args);
|
|
||||||
return certificates.results;
|
|
||||||
}}
|
|
||||||
.renderElement=${(item: CertificateKeyPair): string => {
|
|
||||||
return item.name;
|
|
||||||
}}
|
|
||||||
.value=${(item: CertificateKeyPair | undefined): string | undefined => {
|
|
||||||
return item?.pk;
|
|
||||||
}}
|
|
||||||
.selected=${(
|
|
||||||
item: CertificateKeyPair,
|
|
||||||
items: CertificateKeyPair[],
|
|
||||||
): boolean => {
|
|
||||||
let selected = this.instance?.signingKey === item.pk;
|
|
||||||
if (!this.instance && items.length === 1) {
|
|
||||||
selected = true;
|
|
||||||
}
|
|
||||||
return selected;
|
|
||||||
}}
|
|
||||||
?blankable=${true}
|
|
||||||
>
|
>
|
||||||
</ak-search-select>
|
</ak-crypto-certificate-search>
|
||||||
<p class="pf-c-form__helper-text">${msg("Key used to sign the tokens.")}</p>
|
<p class="pf-c-form__helper-text">${msg("Key used to sign the tokens.")}</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
</div>
|
</div>
|
||||||
|
@ -250,69 +260,53 @@ ${this.instance?.redirectUris}</textarea
|
||||||
<ak-form-group>
|
<ak-form-group>
|
||||||
<span slot="header"> ${msg("Advanced protocol settings")} </span>
|
<span slot="header"> ${msg("Advanced protocol settings")} </span>
|
||||||
<div slot="body" class="pf-c-form">
|
<div slot="body" class="pf-c-form">
|
||||||
<ak-form-element-horizontal
|
<ak-text-input
|
||||||
label=${msg("Access code validity")}
|
|
||||||
?required=${true}
|
|
||||||
name="accessCodeValidity"
|
name="accessCodeValidity"
|
||||||
>
|
label=${msg("Access code validity")}
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value="${first(this.instance?.accessCodeValidity, "minutes=1")}"
|
|
||||||
class="pf-c-form-control"
|
|
||||||
required
|
required
|
||||||
/>
|
value="${first(provider?.accessCodeValidity, "minutes=1")}"
|
||||||
<p class="pf-c-form__helper-text">
|
.bighelp=${html`<p class="pf-c-form__helper-text">
|
||||||
${msg("Configure how long access codes are valid for.")}
|
${msg("Configure how long access codes are valid for.")}
|
||||||
</p>
|
</p>
|
||||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>
|
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||||
</ak-form-element-horizontal>
|
|
||||||
<ak-form-element-horizontal
|
|
||||||
label=${msg("Access Token validity")}
|
|
||||||
?required=${true}
|
|
||||||
name="accessTokenValidity"
|
|
||||||
>
|
>
|
||||||
<input
|
</ak-text-input>
|
||||||
type="text"
|
<ak-text-input
|
||||||
value="${first(this.instance?.accessTokenValidity, "minutes=5")}"
|
name="accessTokenValidity"
|
||||||
class="pf-c-form-control"
|
label=${msg("Access Token validity")}
|
||||||
|
value="${first(provider?.accessTokenValidity, "minutes=5")}"
|
||||||
required
|
required
|
||||||
/>
|
.bighelp=${html` <p class="pf-c-form__helper-text">
|
||||||
<p class="pf-c-form__helper-text">
|
|
||||||
${msg("Configure how long access tokens are valid for.")}
|
${msg("Configure how long access tokens are valid for.")}
|
||||||
</p>
|
</p>
|
||||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>
|
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||||
</ak-form-element-horizontal>
|
|
||||||
<ak-form-element-horizontal
|
|
||||||
label=${msg("Refresh Token validity")}
|
|
||||||
?required=${true}
|
|
||||||
name="refreshTokenValidity"
|
|
||||||
>
|
>
|
||||||
<input
|
</ak-text-input>
|
||||||
type="text"
|
|
||||||
value="${first(this.instance?.refreshTokenValidity, "days=30")}"
|
<ak-text-input
|
||||||
class="pf-c-form-control"
|
name="refreshTokenValidity"
|
||||||
required
|
label=${msg("Refresh Token validity")}
|
||||||
/>
|
value="${first(provider?.refreshTokenValidity, "days=30")}"
|
||||||
<p class="pf-c-form__helper-text">
|
?required=${true}
|
||||||
|
.bighelp=${html` <p class="pf-c-form__helper-text">
|
||||||
${msg("Configure how long refresh tokens are valid for.")}
|
${msg("Configure how long refresh tokens are valid for.")}
|
||||||
</p>
|
</p>
|
||||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>
|
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||||
</ak-form-element-horizontal>
|
>
|
||||||
|
</ak-text-input>
|
||||||
<ak-form-element-horizontal label=${msg("Scopes")} name="propertyMappings">
|
<ak-form-element-horizontal label=${msg("Scopes")} name="propertyMappings">
|
||||||
<select class="pf-c-form-control" multiple>
|
<select class="pf-c-form-control" multiple>
|
||||||
${this.propertyMappings?.results.map((scope) => {
|
${this.propertyMappings?.results.map((scope) => {
|
||||||
let selected = false;
|
let selected = false;
|
||||||
if (!this.instance?.propertyMappings) {
|
if (!provider?.propertyMappings) {
|
||||||
selected =
|
selected =
|
||||||
scope.managed?.startsWith(
|
scope.managed?.startsWith(
|
||||||
"goauthentik.io/providers/oauth2/scope-",
|
"goauthentik.io/providers/oauth2/scope-",
|
||||||
) || false;
|
) || false;
|
||||||
} else {
|
} else {
|
||||||
selected = Array.from(this.instance?.propertyMappings).some(
|
selected = Array.from(provider?.propertyMappings).some((su) => {
|
||||||
(su) => {
|
|
||||||
return su == scope.pk;
|
return su == scope.pk;
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return html`<option
|
return html`<option
|
||||||
value=${ifDefined(scope.pk)}
|
value=${ifDefined(scope.pk)}
|
||||||
|
@ -331,104 +325,35 @@ ${this.instance?.redirectUris}</textarea
|
||||||
${msg("Hold control/command to select multiple items.")}
|
${msg("Hold control/command to select multiple items.")}
|
||||||
</p>
|
</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
<ak-radio-input
|
||||||
label=${msg("Subject mode")}
|
|
||||||
?required=${true}
|
|
||||||
name="subMode"
|
name="subMode"
|
||||||
>
|
label=${msg("Subject mode")}
|
||||||
<ak-radio
|
required
|
||||||
.options=${[
|
.options=${subjectModeOptions}
|
||||||
{
|
.value=${provider?.subMode}
|
||||||
label: msg("Based on the User's hashed ID"),
|
help=${msg(
|
||||||
value: SubModeEnum.HashedUserId,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: msg("Based on the User's ID"),
|
|
||||||
value: SubModeEnum.UserId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: msg("Based on the User's UUID"),
|
|
||||||
value: SubModeEnum.UserUuid,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: msg("Based on the User's username"),
|
|
||||||
value: SubModeEnum.UserUsername,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: msg("Based on the User's Email"),
|
|
||||||
value: SubModeEnum.UserEmail,
|
|
||||||
description: html`${msg(
|
|
||||||
"This is recommended over the UPN mode.",
|
|
||||||
)}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: msg("Based on the User's UPN"),
|
|
||||||
value: SubModeEnum.UserUpn,
|
|
||||||
description: html`${msg(
|
|
||||||
"Requires the user to have a 'upn' attribute set, and falls back to hashed user ID. Use this mode only if you have different UPN and Mail domains.",
|
|
||||||
)}`,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
.value=${this.instance?.subMode}
|
|
||||||
>
|
|
||||||
</ak-radio>
|
|
||||||
<p class="pf-c-form__helper-text">
|
|
||||||
${msg(
|
|
||||||
"Configure what data should be used as unique User Identifier. For most cases, the default should be fine.",
|
"Configure what data should be used as unique User Identifier. For most cases, the default should be fine.",
|
||||||
)}
|
)}
|
||||||
</p>
|
|
||||||
</ak-form-element-horizontal>
|
|
||||||
<ak-form-element-horizontal name="includeClaimsInIdToken">
|
|
||||||
<label class="pf-c-switch">
|
|
||||||
<input
|
|
||||||
class="pf-c-switch__input"
|
|
||||||
type="checkbox"
|
|
||||||
?checked=${first(this.instance?.includeClaimsInIdToken, true)}
|
|
||||||
/>
|
|
||||||
<span class="pf-c-switch__toggle">
|
|
||||||
<span class="pf-c-switch__toggle-icon">
|
|
||||||
<i class="fas fa-check" aria-hidden="true"></i>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span class="pf-c-switch__label"
|
|
||||||
>${msg("Include claims in id_token")}</span
|
|
||||||
>
|
>
|
||||||
</label>
|
</ak-radio-input>
|
||||||
<p class="pf-c-form__helper-text">
|
<ak-switch-input name="includeClaimsInIdToken">
|
||||||
${msg(
|
label=${msg("Include claims in id_token")}
|
||||||
|
?checked=${first(provider?.includeClaimsInIdToken, true)}
|
||||||
|
help=${msg(
|
||||||
"Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint.",
|
"Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint.",
|
||||||
)}
|
)}></ak-switch-input
|
||||||
</p>
|
>
|
||||||
</ak-form-element-horizontal>
|
<ak-radio-input
|
||||||
<ak-form-element-horizontal
|
|
||||||
label=${msg("Issuer mode")}
|
|
||||||
?required=${true}
|
|
||||||
name="issuerMode"
|
name="issuerMode"
|
||||||
>
|
label=${msg("Issuer mode")}
|
||||||
<ak-radio
|
required
|
||||||
.options=${[
|
.options=${issuerModeOptions}
|
||||||
{
|
.value=${provider?.issuerMode}
|
||||||
label: msg(
|
help=${msg(
|
||||||
"Each provider has a different issuer, based on the application slug",
|
|
||||||
),
|
|
||||||
value: IssuerModeEnum.PerProvider,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: msg("Same identifier is used for all providers"),
|
|
||||||
value: IssuerModeEnum.Global,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
.value=${this.instance?.issuerMode}
|
|
||||||
>
|
|
||||||
</ak-radio>
|
|
||||||
<p class="pf-c-form__helper-text">
|
|
||||||
${msg(
|
|
||||||
"Configure how the issuer field of the ID Token should be filled.",
|
"Configure how the issuer field of the ID Token should be filled.",
|
||||||
)}
|
)}
|
||||||
</p>
|
>
|
||||||
</ak-form-element-horizontal>
|
</ak-radio-input>
|
||||||
</div>
|
</div>
|
||||||
</ak-form-group>
|
</ak-form-group>
|
||||||
|
|
||||||
|
@ -441,7 +366,7 @@ ${this.instance?.redirectUris}</textarea
|
||||||
>
|
>
|
||||||
<select class="pf-c-form-control" multiple>
|
<select class="pf-c-form-control" multiple>
|
||||||
${this.oauthSources?.results.map((source) => {
|
${this.oauthSources?.results.map((source) => {
|
||||||
const selected = (this.instance?.jwksSources || []).some((su) => {
|
const selected = (provider?.jwksSources || []).some((su) => {
|
||||||
return su == source.pk;
|
return su == source.pk;
|
||||||
});
|
});
|
||||||
return html`<option value=${source.pk} ?selected=${selected}>
|
return html`<option value=${source.pk} ?selected=${selected}>
|
||||||
|
|
|
@ -4,43 +4,6 @@ import { msg } from "@lit/localize";
|
||||||
import { html, nothing } from "lit";
|
import { html, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
|
||||||
type AkFileArgs = {
|
|
||||||
// The name of the field, snake-to-camel'd if necessary.
|
|
||||||
name: string;
|
|
||||||
// The label of the field.
|
|
||||||
label: string;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
value?: any;
|
|
||||||
required: boolean;
|
|
||||||
// The message to show next to the "current icon".
|
|
||||||
current: string;
|
|
||||||
// The help message, shown at the bottom.
|
|
||||||
help?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const akFileDefaults = {
|
|
||||||
name: "",
|
|
||||||
required: false,
|
|
||||||
current: msg("Currently set to:"),
|
|
||||||
};
|
|
||||||
|
|
||||||
export function akFile(args: AkFileArgs) {
|
|
||||||
const { name, label, required, value, help, current } = {
|
|
||||||
...akFileDefaults,
|
|
||||||
...args,
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentMsg =
|
|
||||||
value && current
|
|
||||||
? html` <p class="pf-c-form__helper-text">${current} ${value}</p> `
|
|
||||||
: nothing;
|
|
||||||
|
|
||||||
return html`<ak-form-element-horizontal ?required="${required}" label=${label} name=${name}>
|
|
||||||
<input type="file" value="" class="pf-c-form-control" />
|
|
||||||
${currentMsg} ${help ? html`<p class="pf-c-form__helper-text">${help}</p>` : nothing}
|
|
||||||
</ak-form-element-horizontal>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("ak-file-input")
|
@customElement("ak-file-input")
|
||||||
export class AkFileInput extends AKElement {
|
export class AkFileInput extends AKElement {
|
||||||
// Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but
|
// Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but
|
||||||
|
@ -60,6 +23,11 @@ export class AkFileInput extends AKElement {
|
||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
label = "";
|
label = "";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The message to show next to the "current icon".
|
||||||
|
*
|
||||||
|
* @attr
|
||||||
|
*/
|
||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
current = msg("Currently set to:");
|
current = msg("Currently set to:");
|
||||||
|
|
||||||
|
@ -73,13 +41,19 @@ export class AkFileInput extends AKElement {
|
||||||
help = "";
|
help = "";
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return akFile({
|
const currentMsg =
|
||||||
name: this.name,
|
this.value && this.current
|
||||||
label: this.label,
|
? html` <p class="pf-c-form__helper-text">${this.current} ${this.value}</p> `
|
||||||
value: this.value,
|
: nothing;
|
||||||
current: this.current,
|
|
||||||
required: this.required,
|
return html`<ak-form-element-horizontal
|
||||||
help: this.help.trim() !== "" ? this.help : undefined,
|
?required="${this.required}"
|
||||||
});
|
label=${this.label}
|
||||||
|
name=${this.name}
|
||||||
|
>
|
||||||
|
<input type="file" value="" class="pf-c-form-control" />
|
||||||
|
${currentMsg}
|
||||||
|
${this.help.trim() ? html`<p class="pf-c-form__helper-text">${this.help}</p>` : nothing}
|
||||||
|
</ak-form-element-horizontal>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,38 +4,6 @@ import { html, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
import { ifDefined } from "lit/directives/if-defined.js";
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
type AkNumberArgs = {
|
|
||||||
// The name of the field, snake-to-camel'd if necessary.
|
|
||||||
name: string;
|
|
||||||
// The label of the field.
|
|
||||||
label: string;
|
|
||||||
value?: number;
|
|
||||||
required: boolean;
|
|
||||||
// The help message, shown at the bottom.
|
|
||||||
help?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const akNumberDefaults = {
|
|
||||||
required: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function akNumber(args: AkNumberArgs) {
|
|
||||||
const { name, label, value, required, help } = {
|
|
||||||
...akNumberDefaults,
|
|
||||||
...args,
|
|
||||||
};
|
|
||||||
|
|
||||||
return html`<ak-form-element-horizontal label=${label} ?required=${required} name=${name}>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value=${ifDefined(value)}
|
|
||||||
class="pf-c-form-control"
|
|
||||||
?required=${required}
|
|
||||||
/>
|
|
||||||
${help ? html`<p class="pf-c-form__helper-text">${help}</p>` : nothing}
|
|
||||||
</ak-form-element-horizontal> `;
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("ak-number-input")
|
@customElement("ak-number-input")
|
||||||
export class AkNumberInput extends AKElement {
|
export class AkNumberInput extends AKElement {
|
||||||
// Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but
|
// Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but
|
||||||
|
@ -55,7 +23,7 @@ export class AkNumberInput extends AKElement {
|
||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
label = "";
|
label = "";
|
||||||
|
|
||||||
@property({ type: Number })
|
@property({ type: Number, reflect: true })
|
||||||
value = 0;
|
value = 0;
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
|
@ -65,12 +33,18 @@ export class AkNumberInput extends AKElement {
|
||||||
help = "";
|
help = "";
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return akNumber({
|
return html`<ak-form-element-horizontal
|
||||||
name: this.name,
|
label=${this.label}
|
||||||
label: this.label,
|
?required=${this.required}
|
||||||
value: this.value,
|
name=${this.name}
|
||||||
required: this.required,
|
>
|
||||||
help: this.help.trim() !== "" ? this.help : undefined,
|
<input
|
||||||
});
|
type="number"
|
||||||
|
value=${ifDefined(this.value)}
|
||||||
|
class="pf-c-form-control"
|
||||||
|
?required=${this.required}
|
||||||
|
/>
|
||||||
|
${this.help ? html`<p class="pf-c-form__helper-text">${this.help}</p>` : nothing}
|
||||||
|
</ak-form-element-horizontal> `;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,35 +4,6 @@ import { RadioOption } from "@goauthentik/elements/forms/Radio";
|
||||||
import { html, nothing } from "lit";
|
import { html, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
|
||||||
type AkRadioArgs<T> = {
|
|
||||||
// The name of the field, snake-to-camel'd if necessary.
|
|
||||||
name: string;
|
|
||||||
// The label of the field.
|
|
||||||
label: string;
|
|
||||||
value?: T;
|
|
||||||
required?: boolean;
|
|
||||||
options: RadioOption<T>[];
|
|
||||||
// The help message, shown at the bottom.
|
|
||||||
help?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const akRadioDefaults = {
|
|
||||||
required: false,
|
|
||||||
options: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export function akRadioInput<T>(args: AkRadioArgs<T>) {
|
|
||||||
const { name, label, help, required, options, value } = {
|
|
||||||
...akRadioDefaults,
|
|
||||||
...args,
|
|
||||||
};
|
|
||||||
|
|
||||||
return html`<ak-form-element-horizontal label=${label} ?required=${required} name=${name}>
|
|
||||||
<ak-radio .options=${options} .value=${value}></ak-radio>
|
|
||||||
${help ? html`<p class="pf-c-form__helper-radio">${help}</p>` : nothing}
|
|
||||||
</ak-form-element-horizontal> `;
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("ak-radio-input")
|
@customElement("ak-radio-input")
|
||||||
export class AkRadioInput<T> extends AKElement {
|
export class AkRadioInput<T> extends AKElement {
|
||||||
// Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but
|
// Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but
|
||||||
|
@ -65,14 +36,16 @@ export class AkRadioInput<T> extends AKElement {
|
||||||
options: RadioOption<T>[] = [];
|
options: RadioOption<T>[] = [];
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return akRadioInput({
|
return html`<ak-form-element-horizontal
|
||||||
name: this.name,
|
label=${this.label}
|
||||||
label: this.label,
|
?required=${this.required}
|
||||||
value: this.value,
|
name=${this.name}
|
||||||
options: this.options,
|
>
|
||||||
required: this.required,
|
<ak-radio .options=${this.options} .value=${this.value}></ak-radio>
|
||||||
help: this.help.trim() !== "" ? this.help : undefined,
|
${this.help.trim()
|
||||||
});
|
? html`<p class="pf-c-form__helper-radio">${this.help}</p>`
|
||||||
|
: nothing}
|
||||||
|
</ak-form-element-horizontal> `;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,44 +3,6 @@ import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import { html, nothing } from "lit";
|
import { html, nothing } from "lit";
|
||||||
import { customElement, property, query } from "lit/decorators.js";
|
import { customElement, property, query } from "lit/decorators.js";
|
||||||
|
|
||||||
type AkSwitchArgs = {
|
|
||||||
// The name of the field, snake-to-camel'd if necessary.
|
|
||||||
name: string;
|
|
||||||
// The label of the field.
|
|
||||||
label: string;
|
|
||||||
checked: boolean;
|
|
||||||
required: boolean;
|
|
||||||
// The help message, shown at the bottom.
|
|
||||||
help?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const akSwitchDefaults = {
|
|
||||||
checked: false,
|
|
||||||
required: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function akSwitch(args: AkSwitchArgs) {
|
|
||||||
const { name, label, checked, required, help } = {
|
|
||||||
...akSwitchDefaults,
|
|
||||||
...args,
|
|
||||||
};
|
|
||||||
|
|
||||||
const doCheck = checked ? checked : undefined;
|
|
||||||
|
|
||||||
return html` <ak-form-element-horizontal name=${name} ?required=${required}>
|
|
||||||
<label class="pf-c-switch">
|
|
||||||
<input class="pf-c-switch__input" type="checkbox" ?checked=${doCheck} />
|
|
||||||
<span class="pf-c-switch__toggle">
|
|
||||||
<span class="pf-c-switch__toggle-icon">
|
|
||||||
<i class="fas fa-check" aria-hidden="true"></i>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span class="pf-c-switch__label">${label}</span>
|
|
||||||
</label>
|
|
||||||
${help ? html`<p class="pf-c-form__helper-text">${help}</p>` : nothing}
|
|
||||||
</ak-form-element-horizontal>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("ak-switch-input")
|
@customElement("ak-switch-input")
|
||||||
export class AkSwitchInput extends AKElement {
|
export class AkSwitchInput extends AKElement {
|
||||||
// Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but
|
// Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but
|
||||||
|
@ -73,12 +35,21 @@ export class AkSwitchInput extends AKElement {
|
||||||
checkbox!: HTMLInputElement;
|
checkbox!: HTMLInputElement;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return akSwitch({
|
const doCheck = this.checked ? this.checked : undefined;
|
||||||
name: this.name,
|
|
||||||
label: this.label,
|
return html` <ak-form-element-horizontal name=${this.name} ?required=${this.required}>
|
||||||
checked: this.checked,
|
<label class="pf-c-switch">
|
||||||
required: this.required,
|
<input class="pf-c-switch__input" type="checkbox" ?checked=${doCheck} />
|
||||||
help: this.help.trim() !== "" ? this.help : undefined,
|
<span class="pf-c-switch__toggle">
|
||||||
});
|
<span class="pf-c-switch__toggle-icon">
|
||||||
|
<i class="fas fa-check" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="pf-c-switch__label">${this.label}</span>
|
||||||
|
</label>
|
||||||
|
${this.help.trim() ? html`<p class="pf-c-form__helper-text">${this.help}</p>` : nothing}
|
||||||
|
</ak-form-element-horizontal>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default AkSwitchInput;
|
||||||
|
|
|
@ -1,41 +1,9 @@
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
|
|
||||||
import { html, nothing } from "lit";
|
import { TemplateResult, html, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
import { ifDefined } from "lit/directives/if-defined.js";
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
type AkTextArgs = {
|
|
||||||
// The name of the field, snake-to-camel'd if necessary.
|
|
||||||
name: string;
|
|
||||||
// The label of the field.
|
|
||||||
label: string;
|
|
||||||
value?: string;
|
|
||||||
required: boolean;
|
|
||||||
// The help message, shown at the bottom.
|
|
||||||
help?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const akTextDefaults = {
|
|
||||||
required: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function akText(args: AkTextArgs) {
|
|
||||||
const { name, label, value, required, help } = {
|
|
||||||
...akTextDefaults,
|
|
||||||
...args,
|
|
||||||
};
|
|
||||||
|
|
||||||
return html`<ak-form-element-horizontal label=${label} ?required=${required} name=${name}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value=${ifDefined(value)}
|
|
||||||
class="pf-c-form-control"
|
|
||||||
?required=${required}
|
|
||||||
/>
|
|
||||||
${help ? html`<p class="pf-c-form__helper-text">${help}</p>` : nothing}
|
|
||||||
</ak-form-element-horizontal> `;
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("ak-text-input")
|
@customElement("ak-text-input")
|
||||||
export class AkTextInput extends AKElement {
|
export class AkTextInput extends AKElement {
|
||||||
// Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but
|
// Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but
|
||||||
|
@ -55,7 +23,7 @@ export class AkTextInput extends AKElement {
|
||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
label = "";
|
label = "";
|
||||||
|
|
||||||
@property({ type: String })
|
@property({ type: String, reflect: true })
|
||||||
value = "";
|
value = "";
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
|
@ -64,13 +32,33 @@ export class AkTextInput extends AKElement {
|
||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
help = "";
|
help = "";
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
hidden = false;
|
||||||
|
|
||||||
|
@property({ type: Object })
|
||||||
|
bighelp!: TemplateResult | TemplateResult[];
|
||||||
|
|
||||||
|
renderHelp() {
|
||||||
|
return [
|
||||||
|
this.help ? html`<p class="pf-c-form__helper-text">${this.help}</p>` : nothing,
|
||||||
|
this.bighelp ? this.bighelp : nothing,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return akText({
|
return html`<ak-form-element-horizontal
|
||||||
name: this.name,
|
label=${this.label}
|
||||||
label: this.label,
|
?required=${this.required}
|
||||||
value: this.value,
|
?hidden=${this.hidden}
|
||||||
required: this.required,
|
name=${this.name}
|
||||||
help: this.help.trim() !== "" ? this.help : undefined,
|
>
|
||||||
});
|
<input
|
||||||
|
type="text"
|
||||||
|
value=${ifDefined(this.value)}
|
||||||
|
class="pf-c-form-control"
|
||||||
|
?required=${this.required}
|
||||||
|
/>
|
||||||
|
${this.renderHelp()}
|
||||||
|
</ak-form-element-horizontal> `;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,8 @@
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
|
|
||||||
import { html, nothing } from "lit";
|
import { TemplateResult, html, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
|
||||||
type AkTextareaArgs = {
|
|
||||||
// The name of the field, snake-to-camel'd if necessary.
|
|
||||||
name: string;
|
|
||||||
// The label of the field.
|
|
||||||
label: string;
|
|
||||||
value?: string;
|
|
||||||
required: boolean;
|
|
||||||
// The help message, shown at the bottom.
|
|
||||||
help?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const akTextareaDefaults = {
|
|
||||||
required: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function akTextarea(args: AkTextareaArgs) {
|
|
||||||
const { name, label, value, required, help } = {
|
|
||||||
...akTextareaDefaults,
|
|
||||||
...args,
|
|
||||||
};
|
|
||||||
|
|
||||||
// `<textarea>` is highly sensitive to whitespace. When editing this control, take care that the
|
|
||||||
// provided value has no whitespace between the `textarea` open and close tags.
|
|
||||||
//
|
|
||||||
// prettier-ignore
|
|
||||||
return html`<ak-form-element-horizontal label=${label} ?required=${required} name=${name}>
|
|
||||||
<textarea class="pf-c-form-control" ?required=${required}
|
|
||||||
name=${name}>${value !== undefined ? value : ""}</textarea>
|
|
||||||
${help ? html`<p class="pf-c-form__helper-textarea">${help}</p>` : nothing}
|
|
||||||
</ak-form-element-horizontal> `;
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("ak-textarea-input")
|
@customElement("ak-textarea-input")
|
||||||
export class AkTextareaInput extends AKElement {
|
export class AkTextareaInput extends AKElement {
|
||||||
// Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but
|
// Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but
|
||||||
|
@ -63,13 +31,26 @@ export class AkTextareaInput extends AKElement {
|
||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
help = "";
|
help = "";
|
||||||
|
|
||||||
|
@property({ type: Object })
|
||||||
|
bighelp!: TemplateResult | TemplateResult[];
|
||||||
|
|
||||||
|
renderHelp() {
|
||||||
|
return [
|
||||||
|
this.help ? html`<p class="pf-c-form__helper-textarea">${this.help}</p>` : nothing,
|
||||||
|
this.bighelp ? this.bighelp : nothing,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return akTextarea({
|
return html`<ak-form-element-horizontal
|
||||||
name: this.name,
|
label=${this.label}
|
||||||
label: this.label,
|
?required=${this.required}
|
||||||
value: this.value,
|
name=${this.name}
|
||||||
required: this.required,
|
>
|
||||||
help: this.help.trim() !== "" ? this.help : undefined,
|
<textarea class="pf-c-form-control" ?required=${this.required} name=${this.name}>
|
||||||
});
|
${this.value !== undefined ? this.value : ""}</textarea
|
||||||
|
>
|
||||||
|
${this.renderHelp()}
|
||||||
|
</ak-form-element-horizontal> `;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,15 +11,19 @@ export function CustomEmitterElement<T extends Constructor<LitElement>>(supercla
|
||||||
return class EmmiterElementHandler extends superclass {
|
return class EmmiterElementHandler extends superclass {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
dispatchCustomEvent(eventName: string, detail: any = {}, options = {}) {
|
dispatchCustomEvent(eventName: string, detail: any = {}, options = {}) {
|
||||||
|
const fullDetail =
|
||||||
|
typeof detail === "object"
|
||||||
|
? {
|
||||||
|
target: this,
|
||||||
|
...detail,
|
||||||
|
}
|
||||||
|
: detail;
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent(eventName, {
|
new CustomEvent(eventName, {
|
||||||
composed: true,
|
composed: true,
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
...options,
|
...options,
|
||||||
detail: {
|
detail: fullDetail,
|
||||||
target: this,
|
|
||||||
...detail,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue