web/maintenance: remove writeOnly
hacks from Form and HorizontalFormElement (#14649)
* web: Add InvalidationFlow to Radius Provider dialogues
## What
- Bugfix: adds the InvalidationFlow to the Radius Provider dialogues
- Repairs: `{"invalidation_flow":["This field is required."]}` message, which was *not* propagated
to the Notification.
- Nitpick: Pretties `?foo=${true}` expressions: `s/\?([^=]+)=\$\{true\}/\1/`
## Note
Yes, I know I'm going to have to do more magic when we harmonize the forms, and no, I didn't add the
Property Mappings to the wizard, and yes, I know I'm going to have pain with the *new* version of
the wizard. But this is a serious bug; you can't make Radius servers with *either* of the current
dialogues at the moment.
* This (temporary) change is needed to prevent the unit tests from failing.
\# What
\# Why
\# How
\# Designs
\# Test Steps
\# Other Notes
* Revert "This (temporary) change is needed to prevent the unit tests from failing."
This reverts commit dddde09be5
.
* web/standards: use attribute naming scheme for attributes
## What
Renamed the 'inputHint' attribute to 'input-hint', because it is an attribute, not a property.
Properties are camelCased, but attributes are kebab-cased.
Updated all instances where this appears with the usual magic:
```
$ perl -pi.bak -e 's/inputHint="code"/input-hint="code"/' $(rg -l 'inputHint="code"')
```
This fix is in preparation for both the Patternfly 5 project and the Schema-Driven Forms project.
* web/maintenance: remove `writeOnly` hacks from Form and HorizontalFormElement
## What
The `writeOnly` hack substituted an obscuring, read-only field for secret keys and passwords that an
admin should never be able to see/read, only *write*, but allowed the user to click on and replace
the key or password. The hack performed this substitution within `HorizontalFormElement` and
dispersed a flag throughout the code to enforce it. Another hack within `Form` directed the API to
not update / write changes to that field if the field had never been activated.
This commit replaces the `writeOnly` hack with a pair of purpose-built components,
`ak-private-text-input` and `ak-private-textarea-input`, that perform the exact same functionality
but without having to involve the HorizontalFormElement, which really should just be layout and
generic functionality. It also replaces all the `writeOnly` hackery in Form with a simple
`doNotProcess` flag, which extends and genericizes this capability to any and all input fields.
The only major protocol change is that `?writeOnly` was a *positive* flag; you controlled it by
saying `this.instance !== undefined`; `?revealed` is a *positive* flog; you reveal the working input
field when `this.instance === undefined`.
It is not necessary to specify the monospace, autocomplete, and spell-check features; those are
enabled or disabled automatically when the `input-hint="code"` flag is passed.
## Why
Removing special cases from processing code is an important step toward the Authentik Elements NPM
package, as well as the Schema-Driven Forms update.
## Note
This is actually a very significant change; this is important functionality that I have hand-tested
quite a bit, but could wish for automated testing that also checks the database back-end to ensure
the fixes made write the keys and passwords as required. Checking the back-end directly is important
since these fields are never re-sent to the front-end after being saved!
Things like `placeholder`, `required`, and getting the `name`, `label` or `help` are all issues very
subject to Last-Line Effect, so give this the hairiest eyeball you've got, please.
* Found a few small things, like a missing import that might have broken something.
* web/admin: Update `private-text` field to pass new linting requirement.
\# What
\# Why
\# How
\# Designs
\# Test Steps
\# Other Notes
This commit is contained in:
@ -1,4 +1,5 @@
|
|||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
|
import "@goauthentik/components/ak-private-textarea-input.js";
|
||||||
import "@goauthentik/elements/CodeMirror";
|
import "@goauthentik/elements/CodeMirror";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||||
@ -45,37 +46,24 @@ export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
<ak-private-textarea-input
|
||||||
label=${msg("Certificate")}
|
label=${msg("Certificate")}
|
||||||
name="certificateData"
|
name="certificateData"
|
||||||
?writeOnly=${this.instance !== undefined}
|
input-hint="code"
|
||||||
?required=${true}
|
placeholder="-----BEGIN CERTIFICATE-----"
|
||||||
>
|
required
|
||||||
<textarea
|
?revealed=${this.instance === undefined}
|
||||||
autocomplete="off"
|
help=${msg("PEM-encoded Certificate data.")}
|
||||||
spellcheck="false"
|
></ak-private-textarea-input>
|
||||||
class="pf-c-form-control pf-m-monospace"
|
<ak-private-textarea-input
|
||||||
placeholder="-----BEGIN CERTIFICATE-----"
|
|
||||||
required
|
|
||||||
></textarea>
|
|
||||||
<p class="pf-c-form__helper-text">${msg("PEM-encoded Certificate data.")}</p>
|
|
||||||
</ak-form-element-horizontal>
|
|
||||||
<ak-form-element-horizontal
|
|
||||||
name="keyData"
|
|
||||||
?writeOnly=${this.instance !== undefined}
|
|
||||||
label=${msg("Private Key")}
|
label=${msg("Private Key")}
|
||||||
>
|
name="keyData"
|
||||||
<textarea
|
input-hint="code"
|
||||||
autocomplete="off"
|
?revealed=${this.instance === undefined}
|
||||||
class="pf-c-form-control pf-m-monospace"
|
help=${msg(
|
||||||
spellcheck="false"
|
"Optional Private Key. If this is set, you can use this keypair for encryption.",
|
||||||
></textarea>
|
)}
|
||||||
<p class="pf-c-form__helper-text">
|
></ak-private-textarea-input>`;
|
||||||
${msg(
|
|
||||||
"Optional Private Key. If this is set, you can use this keypair for encryption.",
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</ak-form-element-horizontal>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/common/constants";
|
import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/common/constants";
|
||||||
|
import "@goauthentik/components/ak-private-textarea-input.js";
|
||||||
import "@goauthentik/elements/CodeMirror";
|
import "@goauthentik/elements/CodeMirror";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||||
@ -61,17 +62,13 @@ export class EnterpriseLicenseForm extends ModelForm<License, string> {
|
|||||||
value="${ifDefined(this.installID)}"
|
value="${ifDefined(this.installID)}"
|
||||||
/>
|
/>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
<ak-private-textarea-input
|
||||||
name="key"
|
name="key"
|
||||||
?writeOnly=${this.instance !== undefined}
|
?revealed=${this.instance === undefined}
|
||||||
label=${msg("License key")}
|
label=${msg("License key")}
|
||||||
|
input-hint="code"
|
||||||
>
|
>
|
||||||
<textarea
|
</ak-private-textarea-input>`;
|
||||||
class="pf-c-form-control pf-m-monospace"
|
|
||||||
autocomplete="off"
|
|
||||||
spellcheck="false"
|
|
||||||
></textarea>
|
|
||||||
</ak-form-element-horizontal>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,8 @@ import {
|
|||||||
UserMatchingModeToLabel,
|
UserMatchingModeToLabel,
|
||||||
} from "@goauthentik/admin/sources/oauth/utils";
|
} from "@goauthentik/admin/sources/oauth/utils";
|
||||||
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
|
||||||
|
import "@goauthentik/components/ak-private-text-input.js";
|
||||||
|
import "@goauthentik/components/ak-private-textarea-input.js";
|
||||||
import "@goauthentik/components/ak-switch-input";
|
import "@goauthentik/components/ak-switch-input";
|
||||||
import "@goauthentik/components/ak-text-input";
|
import "@goauthentik/components/ak-text-input";
|
||||||
import "@goauthentik/components/ak-textarea-input";
|
import "@goauthentik/components/ak-textarea-input";
|
||||||
@ -246,30 +248,22 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
|
|||||||
value=${ifDefined(this.instance?.syncPrincipal)}
|
value=${ifDefined(this.instance?.syncPrincipal)}
|
||||||
help=${msg("Principal used to authenticate to the KDC for syncing.")}
|
help=${msg("Principal used to authenticate to the KDC for syncing.")}
|
||||||
></ak-text-input>
|
></ak-text-input>
|
||||||
<ak-form-element-horizontal
|
<ak-private-text-input
|
||||||
name="syncPassword"
|
name="syncPassword"
|
||||||
label=${msg("Sync password")}
|
label=${msg("Sync password")}
|
||||||
?writeOnly=${this.instance !== undefined}
|
?revealed=${this.instance === undefined}
|
||||||
>
|
help=${msg(
|
||||||
<input type="text" value="" class="pf-c-form-control" />
|
"Password used to authenticate to the KDC for syncing. Optional if Sync keytab or Sync credentials cache is provided.",
|
||||||
<p class="pf-c-form__helper-text">
|
)}
|
||||||
${msg(
|
></ak-private-text-input>
|
||||||
"Password used to authenticate to the KDC for syncing. Optional if Sync keytab or Sync credentials cache is provided.",
|
<ak-private-textarea-input
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</ak-form-element-horizontal>
|
|
||||||
<ak-form-element-horizontal
|
|
||||||
name="syncKeytab"
|
name="syncKeytab"
|
||||||
label=${msg("Sync keytab")}
|
label=${msg("Sync keytab")}
|
||||||
?writeOnly=${this.instance !== undefined}
|
?revealed=${this.instance === undefined}
|
||||||
>
|
help=${msg(
|
||||||
<textarea class="pf-c-form-control"></textarea>
|
"Keytab used to authenticate to the KDC for syncing. Optional if Sync password or Sync credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.",
|
||||||
<p class="pf-c-form__helper-text">
|
)}
|
||||||
${msg(
|
></ak-private-textarea-input>
|
||||||
"Keytab used to authenticate to the KDC for syncing. Optional if Sync password or Sync credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.",
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</ak-form-element-horizontal>
|
|
||||||
<ak-text-input
|
<ak-text-input
|
||||||
name="syncCcache"
|
name="syncCcache"
|
||||||
label=${msg("Sync credentials cache")}
|
label=${msg("Sync credentials cache")}
|
||||||
@ -291,18 +285,14 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
|
|||||||
"Force the use of a specific server name for SPNEGO. Must be in the form HTTP@domain",
|
"Force the use of a specific server name for SPNEGO. Must be in the form HTTP@domain",
|
||||||
)}
|
)}
|
||||||
></ak-text-input>
|
></ak-text-input>
|
||||||
<ak-form-element-horizontal
|
<ak-private-textarea-input
|
||||||
name="spnegoKeytab"
|
name="spnegoKeytab"
|
||||||
label=${msg("SPNEGO keytab")}
|
label=${msg("SPNEGO keytab")}
|
||||||
?writeOnly=${this.instance !== undefined}
|
?revealed=${this.instance === undefined}
|
||||||
>
|
help=${msg(
|
||||||
<textarea class="pf-c-form-control"></textarea>
|
"Keytab used for SPNEGO. Optional if SPNEGO credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.",
|
||||||
<p class="pf-c-form__helper-text">
|
)}
|
||||||
${msg(
|
></ak-private-textarea-input>
|
||||||
"Keytab used for SPNEGO. Optional if SPNEGO credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.",
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</ak-form-element-horizontal>
|
|
||||||
<ak-text-input
|
<ak-text-input
|
||||||
name="spnegoCcache"
|
name="spnegoCcache"
|
||||||
label=${msg("SPNEGO credentials cache")}
|
label=${msg("SPNEGO credentials cache")}
|
||||||
|
@ -2,6 +2,7 @@ import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
|||||||
import { placeholderHelperText } from "@goauthentik/admin/helperText";
|
import { placeholderHelperText } from "@goauthentik/admin/helperText";
|
||||||
import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm";
|
import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
|
import "@goauthentik/components/ak-private-text-input.js";
|
||||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
||||||
import "@goauthentik/elements/forms/FormGroup";
|
import "@goauthentik/elements/forms/FormGroup";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
@ -259,13 +260,11 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
|
|||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
/>
|
/>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
<ak-private-text-input
|
||||||
label=${msg("Bind Password")}
|
label=${msg("Bind Password")}
|
||||||
?writeOnly=${this.instance !== undefined}
|
|
||||||
name="bindPassword"
|
name="bindPassword"
|
||||||
>
|
?revealed=${this.instance === undefined}
|
||||||
<input type="text" value="" class="pf-c-form-control" />
|
></ak-private-text-input>
|
||||||
</ak-form-element-horizontal>
|
|
||||||
<ak-form-element-horizontal
|
<ak-form-element-horizontal
|
||||||
label=${msg("Base DN")}
|
label=${msg("Base DN")}
|
||||||
?required=${true}
|
?required=${true}
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
UserMatchingModeToLabel,
|
UserMatchingModeToLabel,
|
||||||
} from "@goauthentik/admin/sources/oauth/utils";
|
} from "@goauthentik/admin/sources/oauth/utils";
|
||||||
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
|
||||||
|
import "@goauthentik/components/ak-private-textarea-input.js";
|
||||||
import "@goauthentik/components/ak-radio-input";
|
import "@goauthentik/components/ak-radio-input";
|
||||||
import "@goauthentik/elements/CodeMirror";
|
import "@goauthentik/elements/CodeMirror";
|
||||||
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
|
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
|
||||||
@ -439,19 +440,14 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
|
|||||||
/>
|
/>
|
||||||
<p class="pf-c-form__helper-text">${msg("Also known as Client ID.")}</p>
|
<p class="pf-c-form__helper-text">${msg("Also known as Client ID.")}</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
<ak-private-textarea-input
|
||||||
label=${msg("Consumer secret")}
|
label=${msg("Consumer secret")}
|
||||||
?required=${true}
|
|
||||||
?writeOnly=${this.instance !== undefined}
|
|
||||||
name="consumerSecret"
|
name="consumerSecret"
|
||||||
>
|
input-hint="code"
|
||||||
<textarea
|
help=${msg("Also known as Client Secret.")}
|
||||||
class="pf-c-form-control pf-m-monospace"
|
required
|
||||||
autocomplete="off"
|
?revealed=${this.instance === undefined}
|
||||||
spellcheck="false"
|
></ak-private-textarea-input>
|
||||||
></textarea>
|
|
||||||
<p class="pf-c-form__helper-text">${msg("Also known as Client Secret.")}</p>
|
|
||||||
</ak-form-element-horizontal>
|
|
||||||
<ak-form-element-horizontal label=${msg("Scopes")} name="additionalScopes">
|
<ak-form-element-horizontal label=${msg("Scopes")} name="additionalScopes">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||||
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
|
import "@goauthentik/components/ak-private-text-input.js";
|
||||||
import "@goauthentik/elements/forms/FormGroup";
|
import "@goauthentik/elements/forms/FormGroup";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
import "@goauthentik/elements/forms/SearchSelect";
|
import "@goauthentik/elements/forms/SearchSelect";
|
||||||
@ -98,21 +99,13 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
<ak-private-text-input
|
||||||
label=${msg("Secret key")}
|
|
||||||
?required=${true}
|
|
||||||
?writeOnly=${this.instance !== undefined}
|
|
||||||
name="clientSecret"
|
name="clientSecret"
|
||||||
>
|
label=${msg("Secret key")}
|
||||||
<input
|
input-hint="code"
|
||||||
type="text"
|
required
|
||||||
value=""
|
?revealed=${this.instance === undefined}
|
||||||
class="pf-c-form-control pf-m-monospace"
|
></ak-private-text-input>
|
||||||
autocomplete="off"
|
|
||||||
spellcheck="false"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</ak-form-element-horizontal>
|
|
||||||
</div>
|
</div>
|
||||||
</ak-form-group>
|
</ak-form-group>
|
||||||
<ak-form-group>
|
<ak-form-group>
|
||||||
@ -136,19 +129,12 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta
|
|||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
/>
|
/>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
<ak-private-text-input
|
||||||
label=${msg("Secret key")}
|
|
||||||
?writeOnly=${this.instance !== undefined}
|
|
||||||
name="adminSecretKey"
|
name="adminSecretKey"
|
||||||
>
|
label=${msg("Secret key")}
|
||||||
<input
|
input-hint="code"
|
||||||
type="text"
|
?revealed=${this.instance === undefined}
|
||||||
value=""
|
></ak-private-text-input>
|
||||||
class="pf-c-form-control pf-m-monospace"
|
|
||||||
autocomplete="off"
|
|
||||||
spellcheck="false"
|
|
||||||
/>
|
|
||||||
</ak-form-element-horizontal>
|
|
||||||
</div>
|
</div>
|
||||||
</ak-form-group>
|
</ak-form-group>
|
||||||
<ak-form-group .expanded=${true}>
|
<ak-form-group .expanded=${true}>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||||
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
|
import "@goauthentik/components/ak-private-text-input.js";
|
||||||
import "@goauthentik/elements/forms/FormGroup";
|
import "@goauthentik/elements/forms/FormGroup";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
import "@goauthentik/elements/forms/Radio";
|
import "@goauthentik/elements/forms/Radio";
|
||||||
@ -75,13 +76,13 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai
|
|||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
/>
|
/>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
|
||||||
label=${msg("SMTP Password")}
|
<ak-private-text-input
|
||||||
?writeOnly=${this.instance !== undefined}
|
|
||||||
name="password"
|
name="password"
|
||||||
>
|
label=${msg("SMTP Password")}
|
||||||
<input type="text" value="" class="pf-c-form-control" />
|
?revealed=${this.instance === undefined}
|
||||||
</ak-form-element-horizontal>
|
></ak-private-text-input>
|
||||||
|
|
||||||
<ak-form-element-horizontal name="useTls">
|
<ak-form-element-horizontal name="useTls">
|
||||||
<label class="pf-c-switch">
|
<label class="pf-c-switch">
|
||||||
<input
|
<input
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import "@goauthentik/components/ak-number-input";
|
import "@goauthentik/components/ak-number-input";
|
||||||
|
import "@goauthentik/components/ak-private-text-input.js";
|
||||||
import "@goauthentik/components/ak-switch-input";
|
import "@goauthentik/components/ak-switch-input";
|
||||||
import "@goauthentik/elements/forms/FormGroup";
|
import "@goauthentik/elements/forms/FormGroup";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
@ -68,26 +69,18 @@ export class CaptchaStageForm extends BaseStageForm<CaptchaStage> {
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
|
||||||
label=${msg("Private Key")}
|
<ak-private-text-input
|
||||||
?required=${true}
|
|
||||||
?writeOnly=${this.instance !== undefined}
|
|
||||||
name="privateKey"
|
name="privateKey"
|
||||||
>
|
label=${msg("Private Key")}
|
||||||
<input
|
input-hint="code"
|
||||||
type="text"
|
required
|
||||||
value=""
|
?revealed=${this.instance === undefined}
|
||||||
class="pf-c-form-control pf-m-monospace"
|
help=${msg(
|
||||||
autocomplete="off"
|
"Private key, acquired from https://www.google.com/recaptcha/intro/v3.html.",
|
||||||
spellcheck="false"
|
)}
|
||||||
required
|
></ak-private-text-input>
|
||||||
/>
|
|
||||||
<p class="pf-c-form__helper-text">
|
|
||||||
${msg(
|
|
||||||
"Private key, acquired from https://www.google.com/recaptcha/intro/v3.html.",
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</ak-form-element-horizontal>
|
|
||||||
<ak-switch-input
|
<ak-switch-input
|
||||||
name="interactive"
|
name="interactive"
|
||||||
label=${msg("Interactive")}
|
label=${msg("Interactive")}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
|
import "@goauthentik/components/ak-private-text-input.js";
|
||||||
import "@goauthentik/elements/forms/FormGroup";
|
import "@goauthentik/elements/forms/FormGroup";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
import "@goauthentik/elements/utils/TimeDeltaHelp";
|
import "@goauthentik/elements/utils/TimeDeltaHelp";
|
||||||
@ -72,13 +73,11 @@ export class EmailStageForm extends BaseStageForm<EmailStage> {
|
|||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
/>
|
/>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
<ak-private-text-input
|
||||||
label=${msg("SMTP Password")}
|
label=${msg("SMTP Password")}
|
||||||
?writeOnly=${this.instance !== undefined}
|
|
||||||
name="password"
|
name="password"
|
||||||
>
|
?revealed=${this.instance === undefined}
|
||||||
<input type="text" value="" class="pf-c-form-control" />
|
></ak-private-text-input>
|
||||||
</ak-form-element-horizontal>
|
|
||||||
<ak-form-element-horizontal name="useTls">
|
<ak-form-element-horizontal name="useTls">
|
||||||
<label class="pf-c-switch">
|
<label class="pf-c-switch">
|
||||||
<input
|
<input
|
||||||
|
@ -21,13 +21,13 @@ export class HorizontalLightComponent<T> extends AKElement {
|
|||||||
@property({ type: String, reflect: true })
|
@property({ type: String, reflect: true })
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
@property({ type: String })
|
@property({ type: String, reflect: true })
|
||||||
label = "";
|
label = "";
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true })
|
@property({ type: Boolean, reflect: true })
|
||||||
required = false;
|
required = false;
|
||||||
|
|
||||||
@property({ type: String })
|
@property({ type: String, reflect: true })
|
||||||
help = "";
|
help = "";
|
||||||
|
|
||||||
@property({ type: Object })
|
@property({ type: Object })
|
||||||
|
73
web/src/components/ak-private-text-input.ts
Normal file
73
web/src/components/ak-private-text-input.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { ifNotEmpty } from "@goauthentik/elements/utils/ifNotEmpty.js";
|
||||||
|
|
||||||
|
import { msg } from "@lit/localize";
|
||||||
|
import { html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
import { classMap } from "lit/directives/class-map.js";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
|
import { HorizontalLightComponent } from "./HorizontalLightComponent";
|
||||||
|
|
||||||
|
@customElement("ak-private-text-input")
|
||||||
|
export class AkPrivateTextInput extends HorizontalLightComponent<string> {
|
||||||
|
@property({ type: String, reflect: true })
|
||||||
|
public value = "";
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true })
|
||||||
|
public revealed = false;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public placeholder = "";
|
||||||
|
|
||||||
|
#onReveal() {
|
||||||
|
this.revealed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#renderPrivateInput() {
|
||||||
|
return html`<div class="pf-c-form__horizontal-group" @click=${() => this.#onReveal()}>
|
||||||
|
<input
|
||||||
|
class="pf-c-form-control"
|
||||||
|
type="password"
|
||||||
|
disabled
|
||||||
|
data-form-ignore="true"
|
||||||
|
value="**************"
|
||||||
|
/>
|
||||||
|
<input type="text" value="${ifDefined(this.value)}" ?required=${this.required} hidden />
|
||||||
|
<p class="pf-c-form__helper-text" aria-live="polite">${msg("Click to change value")}</p>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderVisibleInput() {
|
||||||
|
const code = this.inputHint === "code";
|
||||||
|
const setValue = (ev: InputEvent) => {
|
||||||
|
this.value = (ev.target as HTMLInputElement).value;
|
||||||
|
};
|
||||||
|
const classes = {
|
||||||
|
"pf-c-form-control": true,
|
||||||
|
"pf-m-monospace": code,
|
||||||
|
};
|
||||||
|
|
||||||
|
return html` <input
|
||||||
|
type="text"
|
||||||
|
@input=${setValue}
|
||||||
|
value=${ifDefined(this.value)}
|
||||||
|
class="${classMap(classes)}"
|
||||||
|
placeholder=${ifNotEmpty(this.placeholder)}
|
||||||
|
autocomplete=${ifDefined(code ? "off" : undefined)}
|
||||||
|
spellcheck=${ifDefined(code ? "false" : undefined)}
|
||||||
|
?required=${this.required}
|
||||||
|
/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override renderControl() {
|
||||||
|
return this.revealed ? this.renderVisibleInput() : this.#renderPrivateInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AkPrivateTextInput;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ak-private-text-input": AkPrivateTextInput;
|
||||||
|
}
|
||||||
|
}
|
43
web/src/components/ak-private-textarea-input.ts
Normal file
43
web/src/components/ak-private-textarea-input.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { ifNotEmpty } from "@goauthentik/elements/utils/ifNotEmpty.js";
|
||||||
|
|
||||||
|
import { html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
import { classMap } from "lit/directives/class-map.js";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
|
import { AkPrivateTextInput } from "./ak-private-text-input.js";
|
||||||
|
|
||||||
|
@customElement("ak-private-textarea-input")
|
||||||
|
export class AkPrivateTextAreaInput extends AkPrivateTextInput {
|
||||||
|
protected override renderVisibleInput() {
|
||||||
|
const code = this.inputHint === "code";
|
||||||
|
const setValue = (ev: InputEvent) => {
|
||||||
|
this.value = (ev.target as HTMLInputElement).value;
|
||||||
|
};
|
||||||
|
const classes = {
|
||||||
|
"pf-c-form-control": true,
|
||||||
|
"pf-m-monospace": code,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prevent the leading spaces added by Prettier's whitespace algo
|
||||||
|
// prettier-ignore
|
||||||
|
return html`<textarea
|
||||||
|
@input=${setValue}
|
||||||
|
class="${classMap(classes)}"
|
||||||
|
?required=${this.required}
|
||||||
|
name=${this.name}
|
||||||
|
placeholder=${ifNotEmpty(this.placeholder)}
|
||||||
|
autocomplete=${ifDefined(code ? "off" : undefined)}
|
||||||
|
spellcheck=${ifDefined(code ? "false" : undefined)}
|
||||||
|
>${this.value !== undefined ? this.value : ""}</textarea
|
||||||
|
> `;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AkPrivateTextAreaInput;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ak-private-textarea-input": AkPrivateTextAreaInput;
|
||||||
|
}
|
||||||
|
}
|
@ -1,20 +1,28 @@
|
|||||||
import { html } from "lit";
|
import { html } 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 { HorizontalLightComponent } from "./HorizontalLightComponent";
|
import { HorizontalLightComponent } from "./HorizontalLightComponent";
|
||||||
|
|
||||||
@customElement("ak-textarea-input")
|
@customElement("ak-textarea-input")
|
||||||
export class AkTextareaInput extends HorizontalLightComponent<string> {
|
export class AkTextareaInput extends HorizontalLightComponent<string> {
|
||||||
@property({ type: String, reflect: true })
|
@property({ type: String, reflect: true })
|
||||||
value = "";
|
public value = "";
|
||||||
|
|
||||||
renderControl() {
|
public override renderControl() {
|
||||||
|
const code = this.inputHint === "code";
|
||||||
|
const setValue = (ev: InputEvent) => {
|
||||||
|
this.value = (ev.target as HTMLInputElement).value;
|
||||||
|
};
|
||||||
// Prevent the leading spaces added by Prettier's whitespace algo
|
// Prevent the leading spaces added by Prettier's whitespace algo
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
return html`<textarea
|
return html`<textarea
|
||||||
|
@input=${setValue}
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
?required=${this.required}
|
?required=${this.required}
|
||||||
name=${this.name}
|
name=${this.name}
|
||||||
|
autocomplete=${ifDefined(code ? "off" : undefined)}
|
||||||
|
spellcheck=${ifDefined(code ? "false" : undefined)}
|
||||||
>${this.value !== undefined ? this.value : ""}</textarea
|
>${this.value !== undefined ? this.value : ""}</textarea
|
||||||
> `;
|
> `;
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,8 @@ type HTMLNamedElement = Pick<HTMLInputElement, "name">;
|
|||||||
|
|
||||||
export type AkControlElement<T = string | string[]> = HTMLInputElement & { json: () => T };
|
export type AkControlElement<T = string | string[]> = HTMLInputElement & { json: () => T };
|
||||||
|
|
||||||
|
const doNotProcess = <T extends HTMLElement>(element: T) => element.dataset.formIgnore === "true";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively assign `value` into `json` while interpreting the dot-path of `element.name`
|
* Recursively assign `value` into `json` while interpreting the dot-path of `element.name`
|
||||||
*/
|
*/
|
||||||
@ -74,7 +76,7 @@ export function serializeForm<T extends KeyUnknown>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const inputElement = element.querySelector<AkControlElement>("[name]");
|
const inputElement = element.querySelector<AkControlElement>("[name]");
|
||||||
if (element.hidden || !inputElement || (element.writeOnly && !element.writeOnlyActivated)) {
|
if (element.hidden || !inputElement || doNotProcess(inputElement)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,11 +29,6 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
|||||||
* 3. Updated() pushes the `name` field down to the children, as if that were necessary; why isn't
|
* 3. Updated() pushes the `name` field down to the children, as if that were necessary; why isn't
|
||||||
* it being written on-demand when the child is written? Because it's slotted... despite there
|
* it being written on-demand when the child is written? Because it's slotted... despite there
|
||||||
* being very few unique uses.
|
* being very few unique uses.
|
||||||
* 4. There is some very specific use-case around the `writeOnly` boolean; this seems to be a case
|
|
||||||
* where the field isn't available for the user to view unless they explicitly request to be able
|
|
||||||
* to see the content; otherwise, a dead password field is shown. There are 10 uses of this
|
|
||||||
* feature.
|
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const isAkControl = (el: unknown): boolean =>
|
const isAkControl = (el: unknown): boolean =>
|
||||||
@ -79,12 +74,6 @@ export class HorizontalFormElement extends AKElement {
|
|||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
required = false;
|
required = false;
|
||||||
|
|
||||||
@property({ type: Boolean })
|
|
||||||
writeOnly = false;
|
|
||||||
|
|
||||||
@property({ type: Boolean })
|
|
||||||
writeOnlyActivated = false;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
errorMessages: string[] | string[][] = [];
|
errorMessages: string[] | string[][] = [];
|
||||||
|
|
||||||
@ -130,7 +119,6 @@ export class HorizontalFormElement extends AKElement {
|
|||||||
this.querySelectorAll("*").forEach((input) => {
|
this.querySelectorAll("*").forEach((input) => {
|
||||||
if (isAkControl(input) && !input.getAttribute("name")) {
|
if (isAkControl(input) && !input.getAttribute("name")) {
|
||||||
input.setAttribute("name", this.name);
|
input.setAttribute("name", this.name);
|
||||||
// This is fine; writeOnly won't apply to anything built this way.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,17 +127,6 @@ export class HorizontalFormElement extends AKElement {
|
|||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.writeOnly && !this.writeOnlyActivated) {
|
|
||||||
const i = input as HTMLInputElement;
|
|
||||||
i.setAttribute("hidden", "true");
|
|
||||||
const handler = () => {
|
|
||||||
i.removeAttribute("hidden");
|
|
||||||
this.writeOnlyActivated = true;
|
|
||||||
i.parentElement?.removeEventListener("click", handler);
|
|
||||||
};
|
|
||||||
i.parentElement?.addEventListener("click", handler);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,23 +142,8 @@ export class HorizontalFormElement extends AKElement {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-form__group-control">
|
<div class="pf-c-form__group-control">
|
||||||
${this.writeOnly && !this.writeOnlyActivated
|
|
||||||
? html`<div class="pf-c-form__horizontal-group">
|
|
||||||
<input
|
|
||||||
class="pf-c-form-control"
|
|
||||||
type="password"
|
|
||||||
disabled
|
|
||||||
value="**************"
|
|
||||||
/>
|
|
||||||
</div>`
|
|
||||||
: html``}
|
|
||||||
<slot class="pf-c-form__horizontal-group"></slot>
|
<slot class="pf-c-form__horizontal-group"></slot>
|
||||||
<div class="pf-c-form__horizontal-group">
|
<div class="pf-c-form__horizontal-group">
|
||||||
${this.writeOnly
|
|
||||||
? html`<p class="pf-c-form__helper-text" aria-live="polite">
|
|
||||||
${msg("Click to change value")}
|
|
||||||
</p>`
|
|
||||||
: html``}
|
|
||||||
${this.errorMessages.map((message) => {
|
${this.errorMessages.map((message) => {
|
||||||
if (message instanceof Object) {
|
if (message instanceof Object) {
|
||||||
return html`${Object.entries(message).map(([field, errMsg]) => {
|
return html`${Object.entries(message).map(([field, errMsg]) => {
|
||||||
|
5
web/src/elements/utils/ifNotEmpty.ts
Normal file
5
web/src/elements/utils/ifNotEmpty.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
|
// A variant of `ifDefined` that also doesn't do anything if the string is empty.
|
||||||
|
export const ifNotEmpty = <T>(value: T) =>
|
||||||
|
ifDefined(value === "" ? undefined : (value ?? undefined));
|
Reference in New Issue
Block a user