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/wizard/ApplicationWizard";
|
||||
import { PFSize } from "@goauthentik/app/elements/Spinner";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||
|
@ -89,11 +88,14 @@ export class ApplicationListPage extends TablePage<Application> {
|
|||
renderSidebarAfter(): TemplateResult {
|
||||
// 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
|
||||
|
||||
/* Re-enable the wizard later:
|
||||
<ak-application-wizard
|
||||
.open=${getURLParam("createWizard", false)}
|
||||
.showButton=${false}
|
||||
></ak-application-wizard>
|
||||
<div class="pf-c-sidebar__panel pf-m-width-25">
|
||||
></ak-application-wizard>*/
|
||||
|
||||
return html` <div class="pf-c-sidebar__panel pf-m-width-25">
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__body">
|
||||
<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-context";
|
||||
import "../ldap/ak-application-wizard-authentication-by-ldap";
|
||||
import "../oauth/ak-application-wizard-authentication-by-oauth";
|
||||
import "./ak-application-context-display-for-test";
|
||||
import {
|
||||
dummyAuthenticationFlowsSearch,
|
||||
dummyAuthorizationFlowsSearch,
|
||||
dummyCoreGroupsSearch,
|
||||
dummyCryptoCertsSearch,
|
||||
dummyHasJwks,
|
||||
dummyPropertyMappings,
|
||||
dummyProviderTypesList,
|
||||
} from "./samples";
|
||||
|
||||
|
@ -50,6 +54,26 @@ const metadata: Meta<AkApplicationWizardApplicationDetails> = {
|
|||
status: 200,
|
||||
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)) {
|
||||
document.body.classList.add(LIGHT);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export default metadata;
|
||||
|
||||
const container = (testItem: TemplateResult) => {
|
||||
|
@ -79,7 +102,7 @@ const container = (testItem: TemplateResult) => {
|
|||
</style>
|
||||
${testItem}
|
||||
</div>`;
|
||||
}
|
||||
};
|
||||
|
||||
export const PageOne = () => {
|
||||
return container(
|
||||
|
@ -110,3 +133,14 @@ export const PageThreeLdap = () => {
|
|||
</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 = {
|
||||
pagination: {
|
||||
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
|
||||
export const dummyProviderTypesList = [
|
||||
["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 { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
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/HorizontalFormElement";
|
||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||
|
@ -9,15 +13,12 @@ import "@goauthentik/elements/forms/SearchSelect";
|
|||
import "@goauthentik/elements/utils/TimeDeltaHelp";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { TemplateResult, html, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
CertificateKeyPair,
|
||||
ClientTypeEnum,
|
||||
CryptoApi,
|
||||
CryptoCertificatekeypairsListRequest,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
IssuerModeEnum,
|
||||
OAuth2Provider,
|
||||
|
@ -29,6 +30,91 @@ import {
|
|||
SubModeEnum,
|
||||
} 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")
|
||||
export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
|
||||
propertyMappings?: PaginatedScopeMappingList;
|
||||
|
@ -79,22 +165,23 @@ export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
|
|||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
const provider = this.instance;
|
||||
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.name)}"
|
||||
class="pf-c-form-control"
|
||||
<ak-text-input
|
||||
name="name"
|
||||
label=${msg("Name")}
|
||||
value=${ifDefined(provider?.name)}
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
></ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authentication flow")}
|
||||
name="authenticationFlow"
|
||||
label=${msg("Authentication flow")}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authenticationFlow}
|
||||
.currentFlow=${provider?.authenticationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
|
@ -102,13 +189,13 @@ export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
|
|||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
name="authorizationFlow"
|
||||
label=${msg("Authorization flow")}
|
||||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${this.instance?.authorizationFlow}
|
||||
.currentFlow=${provider?.authorizationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
|
@ -119,129 +206,52 @@ export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
|
|||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Client type")}
|
||||
?required=${true}
|
||||
<ak-radio-input
|
||||
name="clientType"
|
||||
>
|
||||
<ak-radio
|
||||
label=${msg("Client type")}
|
||||
.value=${provider?.clientType}
|
||||
required
|
||||
@change=${(ev: CustomEvent<ClientTypeEnum>) => {
|
||||
if (ev.detail === ClientTypeEnum.Public) {
|
||||
this.showClientSecret = false;
|
||||
} else {
|
||||
this.showClientSecret = true;
|
||||
}
|
||||
this.showClientSecret = ev.detail !== ClientTypeEnum.Public;
|
||||
}}
|
||||
.options=${[
|
||||
{
|
||||
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}
|
||||
.options=${clientTypeOptions}
|
||||
>
|
||||
</ak-radio>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Client ID")}
|
||||
?required=${true}
|
||||
</ak-radio-input>
|
||||
<ak-text-input
|
||||
name="clientId"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
label=${msg("Client ID")}
|
||||
value="${first(
|
||||
this.instance?.clientId,
|
||||
provider?.clientId,
|
||||
randomString(40, ascii_letters + digits),
|
||||
)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
?hidden=${!this.showClientSecret}
|
||||
label=${msg("Client Secret")}
|
||||
name="clientSecret"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
</ak-text-input>
|
||||
<ak-text-input
|
||||
name="clientSecret"
|
||||
label=${msg("Client Secret")}
|
||||
value="${first(
|
||||
this.instance?.clientSecret,
|
||||
provider?.clientSecret,
|
||||
randomString(128, ascii_letters + digits),
|
||||
)}"
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Redirect URIs/Origins (RegEx)")}
|
||||
?hidden=${!this.showClientSecret}
|
||||
>
|
||||
</ak-text-input>
|
||||
<ak-textarea-input
|
||||
name="redirectUris"
|
||||
label=${msg("Redirect URIs/Origins (RegEx)")}
|
||||
.value=${provider?.redirectUris}
|
||||
.bighelp=${redirectUriHelp}
|
||||
>
|
||||
<textarea class="pf-c-form-control">
|
||||
${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-textarea-input>
|
||||
|
||||
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (
|
||||
query?: string,
|
||||
): Promise<CertificateKeyPair[]> => {
|
||||
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-crypto-certificate-search
|
||||
certificate=${ifDefined(provider?.signingKey ?? nothing)}
|
||||
name="certificate"
|
||||
singleton
|
||||
>
|
||||
</ak-search-select>
|
||||
</ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">${msg("Key used to sign the tokens.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
|
@ -250,69 +260,53 @@ ${this.instance?.redirectUris}</textarea
|
|||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Access code validity")}
|
||||
?required=${true}
|
||||
<ak-text-input
|
||||
name="accessCodeValidity"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${first(this.instance?.accessCodeValidity, "minutes=1")}"
|
||||
class="pf-c-form-control"
|
||||
label=${msg("Access code validity")}
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
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-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Access Token validity")}
|
||||
?required=${true}
|
||||
name="accessTokenValidity"
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${first(this.instance?.accessTokenValidity, "minutes=5")}"
|
||||
class="pf-c-form-control"
|
||||
</ak-text-input>
|
||||
<ak-text-input
|
||||
name="accessTokenValidity"
|
||||
label=${msg("Access Token validity")}
|
||||
value="${first(provider?.accessTokenValidity, "minutes=5")}"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
.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-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Refresh Token validity")}
|
||||
?required=${true}
|
||||
name="refreshTokenValidity"
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${first(this.instance?.refreshTokenValidity, "days=30")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
</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-form-element-horizontal>
|
||||
<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 (!this.instance?.propertyMappings) {
|
||||
if (!provider?.propertyMappings) {
|
||||
selected =
|
||||
scope.managed?.startsWith(
|
||||
"goauthentik.io/providers/oauth2/scope-",
|
||||
) || false;
|
||||
} else {
|
||||
selected = Array.from(this.instance?.propertyMappings).some(
|
||||
(su) => {
|
||||
selected = Array.from(provider?.propertyMappings).some((su) => {
|
||||
return su == scope.pk;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
return html`<option
|
||||
value=${ifDefined(scope.pk)}
|
||||
|
@ -331,104 +325,35 @@ ${this.instance?.redirectUris}</textarea
|
|||
${msg("Hold control/command to select multiple items.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Subject mode")}
|
||||
?required=${true}
|
||||
<ak-radio-input
|
||||
name="subMode"
|
||||
>
|
||||
<ak-radio
|
||||
.options=${[
|
||||
{
|
||||
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.",
|
||||
)}`,
|
||||
},
|
||||
]}
|
||||
.value=${this.instance?.subMode}
|
||||
>
|
||||
</ak-radio>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
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.",
|
||||
)}
|
||||
</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>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
</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.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Issuer mode")}
|
||||
?required=${true}
|
||||
)}></ak-switch-input
|
||||
>
|
||||
<ak-radio-input
|
||||
name="issuerMode"
|
||||
>
|
||||
<ak-radio
|
||||
.options=${[
|
||||
{
|
||||
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,
|
||||
},
|
||||
]}
|
||||
.value=${this.instance?.issuerMode}
|
||||
>
|
||||
</ak-radio>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
label=${msg("Issuer mode")}
|
||||
required
|
||||
.options=${issuerModeOptions}
|
||||
.value=${provider?.issuerMode}
|
||||
help=${msg(
|
||||
"Configure how the issuer field of the ID Token should be filled.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
>
|
||||
</ak-radio-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
|
@ -441,7 +366,7 @@ ${this.instance?.redirectUris}</textarea
|
|||
>
|
||||
<select class="pf-c-form-control" multiple>
|
||||
${this.oauthSources?.results.map((source) => {
|
||||
const selected = (this.instance?.jwksSources || []).some((su) => {
|
||||
const selected = (provider?.jwksSources || []).some((su) => {
|
||||
return su == source.pk;
|
||||
});
|
||||
return html`<option value=${source.pk} ?selected=${selected}>
|
||||
|
|
|
@ -4,43 +4,6 @@ import { msg } from "@lit/localize";
|
|||
import { html, nothing } from "lit";
|
||||
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")
|
||||
export class AkFileInput extends AKElement {
|
||||
// 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 })
|
||||
label = "";
|
||||
|
||||
/*
|
||||
* The message to show next to the "current icon".
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: String })
|
||||
current = msg("Currently set to:");
|
||||
|
||||
|
@ -73,13 +41,19 @@ export class AkFileInput extends AKElement {
|
|||
help = "";
|
||||
|
||||
render() {
|
||||
return akFile({
|
||||
name: this.name,
|
||||
label: this.label,
|
||||
value: this.value,
|
||||
current: this.current,
|
||||
required: this.required,
|
||||
help: this.help.trim() !== "" ? this.help : undefined,
|
||||
});
|
||||
const currentMsg =
|
||||
this.value && this.current
|
||||
? html` <p class="pf-c-form__helper-text">${this.current} ${this.value}</p> `
|
||||
: nothing;
|
||||
|
||||
return html`<ak-form-element-horizontal
|
||||
?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 { 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")
|
||||
export class AkNumberInput extends AKElement {
|
||||
// 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 })
|
||||
label = "";
|
||||
|
||||
@property({ type: Number })
|
||||
@property({ type: Number, reflect: true })
|
||||
value = 0;
|
||||
|
||||
@property({ type: Boolean })
|
||||
|
@ -65,12 +33,18 @@ export class AkNumberInput extends AKElement {
|
|||
help = "";
|
||||
|
||||
render() {
|
||||
return akNumber({
|
||||
name: this.name,
|
||||
label: this.label,
|
||||
value: this.value,
|
||||
required: this.required,
|
||||
help: this.help.trim() !== "" ? this.help : undefined,
|
||||
});
|
||||
return html`<ak-form-element-horizontal
|
||||
label=${this.label}
|
||||
?required=${this.required}
|
||||
name=${this.name}
|
||||
>
|
||||
<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 { 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")
|
||||
export class AkRadioInput<T> extends AKElement {
|
||||
// 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>[] = [];
|
||||
|
||||
render() {
|
||||
return akRadioInput({
|
||||
name: this.name,
|
||||
label: this.label,
|
||||
value: this.value,
|
||||
options: this.options,
|
||||
required: this.required,
|
||||
help: this.help.trim() !== "" ? this.help : undefined,
|
||||
});
|
||||
return html`<ak-form-element-horizontal
|
||||
label=${this.label}
|
||||
?required=${this.required}
|
||||
name=${this.name}
|
||||
>
|
||||
<ak-radio .options=${this.options} .value=${this.value}></ak-radio>
|
||||
${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 { 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")
|
||||
export class AkSwitchInput extends AKElement {
|
||||
// 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;
|
||||
|
||||
render() {
|
||||
return akSwitch({
|
||||
name: this.name,
|
||||
label: this.label,
|
||||
checked: this.checked,
|
||||
required: this.required,
|
||||
help: this.help.trim() !== "" ? this.help : undefined,
|
||||
});
|
||||
const doCheck = this.checked ? this.checked : undefined;
|
||||
|
||||
return html` <ak-form-element-horizontal name=${this.name} ?required=${this.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">${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 { html, nothing } from "lit";
|
||||
import { TemplateResult, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.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")
|
||||
export class AkTextInput extends AKElement {
|
||||
// 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 })
|
||||
label = "";
|
||||
|
||||
@property({ type: String })
|
||||
@property({ type: String, reflect: true })
|
||||
value = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
|
@ -64,13 +32,33 @@ export class AkTextInput extends AKElement {
|
|||
@property({ type: String })
|
||||
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() {
|
||||
return akText({
|
||||
name: this.name,
|
||||
label: this.label,
|
||||
value: this.value,
|
||||
required: this.required,
|
||||
help: this.help.trim() !== "" ? this.help : undefined,
|
||||
});
|
||||
return html`<ak-form-element-horizontal
|
||||
label=${this.label}
|
||||
?required=${this.required}
|
||||
?hidden=${this.hidden}
|
||||
name=${this.name}
|
||||
>
|
||||
<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 { html, nothing } from "lit";
|
||||
import { TemplateResult, html, nothing } from "lit";
|
||||
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")
|
||||
export class AkTextareaInput extends AKElement {
|
||||
// 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 })
|
||||
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() {
|
||||
return akTextarea({
|
||||
name: this.name,
|
||||
label: this.label,
|
||||
value: this.value,
|
||||
required: this.required,
|
||||
help: this.help.trim() !== "" ? this.help : undefined,
|
||||
});
|
||||
return html`<ak-form-element-horizontal
|
||||
label=${this.label}
|
||||
?required=${this.required}
|
||||
name=${this.name}
|
||||
>
|
||||
<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 {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
dispatchCustomEvent(eventName: string, detail: any = {}, options = {}) {
|
||||
const fullDetail =
|
||||
typeof detail === "object"
|
||||
? {
|
||||
target: this,
|
||||
...detail,
|
||||
}
|
||||
: detail;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(eventName, {
|
||||
composed: true,
|
||||
bubbles: true,
|
||||
...options,
|
||||
detail: {
|
||||
target: this,
|
||||
...detail,
|
||||
},
|
||||
detail: fullDetail,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
Reference in a new issue