stages/user_login: stay logged in (#4958)

* add initial remember me offset

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add to go executor

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add ui for user login stage

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* update docs

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L
2023-03-15 20:21:05 +01:00
committed by GitHub
parent fd9293e3e8
commit eaf56f4f3f
21 changed files with 311 additions and 18 deletions

View File

@ -81,6 +81,22 @@ export class UserLoginStageForm extends ModelForm<UserLoginStage, string> {
</a>
</ak-alert>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Stay signed in offset`}
?required=${true}
name="rememberMeOffset"
>
<input
type="text"
value="${first(this.instance?.rememberMeOffset, "seconds=0")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${t`If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.`}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="terminateOtherSessions">
<label class="pf-c-switch">
<input

View File

@ -372,6 +372,12 @@ export class FlowExecutor extends Interface implements StageHost {
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-authenticator-validate>`;
case "ak-stage-user-login":
await import("@goauthentik/flow/stages/user_login/UserLoginStage");
return html`<ak-stage-user-login
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-user-login>`;
// Sources
case "ak-source-plex":
await import("@goauthentik/flow/sources/plex/PlexLoginInit");

View File

@ -32,7 +32,7 @@ export class BaseStage<Tin, Tout> extends AKElement {
@property({ attribute: false })
challenge!: Tin;
async submitForm(e: Event, defaults?: KeyUnknown): Promise<boolean> {
async submitForm(e: Event, defaults?: Tout): Promise<boolean> {
e.preventDefault();
const object: KeyUnknown = defaults || {};
const form = new FormData(this.shadowRoot?.querySelector("form") || undefined);

View File

@ -41,7 +41,9 @@ export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeRe
renderNoPrevious(): TemplateResult {
return html`
<div class="pf-c-form__group">
<p id="header-text" class="pf-u-mb-xl">${this.challenge.headerText}</p>
<h3 id="header-text" class="pf-c-title pf-m-xl pf-u-mb-xl">
${this.challenge.headerText}
</h3>
${this.challenge.permissions.length > 0
? html`
<p class="pf-u-mb-sm">
@ -59,7 +61,9 @@ export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeRe
renderAdditional(): TemplateResult {
return html`
<div class="pf-c-form__group">
<p id="header-text" class="pf-u-mb-xl">${this.challenge.headerText}</p>
<h3 id="header-text" class="pf-c-title pf-m-xl pf-u-mb-xl">
${this.challenge.headerText}
</h3>
${this.challenge.permissions.length > 0
? html`
<p class="pf-u-mb-sm">

View File

@ -0,0 +1,88 @@
import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/forms/FormElement";
import "@goauthentik/flow/FormStatic";
import { BaseStage } from "@goauthentik/flow/stages/base";
import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFSpacing from "@patternfly/patternfly/utilities/Spacing/spacing.css";
import { UserLoginChallenge, UserLoginChallengeResponseRequest } from "@goauthentik/api";
@customElement("ak-stage-user-login")
export class PasswordStage extends BaseStage<
UserLoginChallenge,
UserLoginChallengeResponseRequest
> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFSpacing, PFButton, PFTitle];
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form">
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${t`Not you?`}</a
>
</div>
</ak-form-static>
<div class="pf-c-form__group">
<h3 id="header-text" class="pf-c-title pf-m-xl pf-u-mb-xl">
${t`Stay signed in?`}
</h3>
<p class="pf-u-mb-sm">
${t`Select Yes to reduce the number of times you're asked to sign in.`}
</p>
</div>
<div class="pf-c-form__group pf-m-action">
<button
@click=${(e: Event) => {
this.submitForm(e, {
rememberMe: true,
});
}}
class="pf-c-button pf-m-primary"
>
${t`Yes`}
</button>
<button
@click=${(e: Event) => {
this.submitForm(e, {
rememberMe: false,
});
}}
class="pf-c-button pf-m-secondary"
>
${t`No`}
</button>
</div>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}