interface split (#943)

This commit is contained in:
Jens L
2021-09-16 17:30:16 +02:00
committed by GitHub
parent d7ab2a362a
commit 9441be1ee2
38 changed files with 1804 additions and 243 deletions

170
web/src/user/LibraryPage.ts Normal file
View File

@ -0,0 +1,170 @@
import { t } from "@lingui/macro";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { ifDefined } from "lit-html/directives/if-defined";
import { until } from "lit-html/directives/until";
import { Application, CoreApi } from "@goauthentik/api";
import { AKResponse } from "../api/Client";
import { DEFAULT_CONFIG } from "../api/Config";
import { me } from "../api/Users";
import { loading, truncate } from "../utils";
import "../elements/PageHeader";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import AKGlobal from "../authentik.css";
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
import PFGallery from "@patternfly/patternfly/layouts/Gallery/gallery.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@customElement("ak-library-app")
export class LibraryApplication extends LitElement {
@property({ attribute: false })
application?: Application;
static get styles(): CSSResult[] {
return [
PFBase,
PFCard,
PFButton,
PFAvatar,
AKGlobal,
css`
.pf-c-card {
height: 100%;
}
i.pf-icon {
height: 36px;
display: flex;
flex-direction: column;
justify-content: center;
}
.pf-c-avatar {
--pf-c-avatar--BorderRadius: 0;
}
.pf-c-card__header {
min-height: 60px;
justify-content: space-between;
}
.pf-c-card__header a {
display: flex;
flex-direction: column;
justify-content: center;
margin-right: 0.25em;
}
`,
];
}
render(): TemplateResult {
if (!this.application) {
return html`<ak-spinner></ak-spinner>`;
}
return html` <div class="pf-c-card pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
${this.application.metaIcon
? html`<a href="${ifDefined(this.application.launchUrl ?? "")}"
><img
class="app-icon pf-c-avatar"
src="${ifDefined(this.application.metaIcon)}"
alt="Application Icon"
/></a>`
: html`<i class="fas fas fa-share-square"></i>`}
${until(
me().then((u) => {
if (!u.user.isSuperuser) return html``;
return html`
<a
class="pf-c-button pf-m-control pf-m-small"
href="#/core/applications/${this.application?.slug}"
>
<i class="fas fa-pencil-alt"></i>
</a>
`;
}),
)}
</div>
<div class="pf-c-card__title">
<p id="card-1-check-label">
<a href="${ifDefined(this.application.launchUrl ?? "")}"
>${this.application.name}</a
>
</p>
<div class="pf-c-content">
<small>${this.application.metaPublisher}</small>
</div>
</div>
<div class="pf-c-card__body">${truncate(this.application.metaDescription, 35)}</div>
</div>`;
}
}
@customElement("ak-library")
export class LibraryPage extends LitElement {
@property({ attribute: false })
apps?: AKResponse<Application>;
pageTitle(): string {
return t`My Applications`;
}
static get styles(): CSSResult[] {
return [PFBase, PFEmptyState, PFTitle, PFPage, PFContent, PFGallery, AKGlobal].concat(css`
:host,
main {
height: 100%;
}
`);
}
firstUpdated(): void {
new CoreApi(DEFAULT_CONFIG).coreApplicationsList({}).then((apps) => {
this.apps = apps;
});
}
renderEmptyState(): TemplateResult {
return html` <div class="pf-c-empty-state pf-m-full-height">
<div class="pf-c-empty-state__content">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">${t`No Applications available.`}</h1>
<div class="pf-c-empty-state__body">
${t`Either no applications are defined, or you don't have access to any.`}
</div>
</div>
</div>`;
}
renderApps(): TemplateResult {
return html`<div class="pf-l-gallery pf-m-gutter">
${this.apps?.results.map(
(app) => html`<ak-library-app .application=${app}></ak-library-app>`,
)}
</div>`;
}
render(): TemplateResult {
return html`<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
<ak-page-header icon="pf-icon pf-icon-applications" header=${t`Applications`}>
</ak-page-header>
<section class="pf-c-page__main-section">
${loading(
this.apps,
html`${(this.apps?.results.length || 0) > 0
? this.renderApps()
: this.renderEmptyState()}`,
)}
</section>
</main>`;
}
}

View File

@ -0,0 +1,100 @@
import { t } from "@lingui/macro";
import { customElement, html, TemplateResult } from "lit-element";
import { CoreApi, UserSelf } from "@goauthentik/api";
import { ifDefined } from "lit-html/directives/if-defined";
import { DEFAULT_CONFIG, tenant } from "../../api/Config";
import "../../elements/forms/FormElement";
import "../../elements/EmptyState";
import "../../elements/forms/Form";
import "../../elements/forms/HorizontalFormElement";
import { until } from "lit-html/directives/until";
import { ModelForm } from "../../elements/forms/ModelForm";
@customElement("ak-user-self-form")
export class UserSelfForm extends ModelForm<UserSelf, number> {
viewportCheck = false;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
loadInstance(pk: number): Promise<UserSelf> {
return new CoreApi(DEFAULT_CONFIG).coreUsersMeRetrieve().then((su) => {
return su.user;
});
}
getSuccessMessage(): string {
return t`Successfully updated details.`;
}
send = (data: UserSelf): Promise<UserSelf> => {
return new CoreApi(DEFAULT_CONFIG)
.coreUsersUpdateSelfUpdate({
userSelfRequest: data,
})
.then((su) => {
return su.user;
});
};
renderForm(): TemplateResult {
if (!this.instance) {
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${t`Username`} ?required=${true} name="username">
<input
type="text"
value="${ifDefined(this.instance?.username)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${t`Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name">
<input
type="text"
value="${ifDefined(this.instance?.name)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">${t`User's display name.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Email`} name="email">
<input
type="email"
value="${ifDefined(this.instance?.email)}"
class="pf-c-form-control"
/>
</ak-form-element-horizontal>
<div class="pf-c-form__group pf-m-action">
<div class="pf-c-form__horizontal-group">
<div class="pf-c-form__actions">
<button
@click=${(ev: Event) => {
return this.submit(ev);
}}
class="pf-c-button pf-m-primary"
>
${t`Update`}
</button>
${until(
tenant().then((tenant) => {
if (tenant.flowUnenrollment) {
return html`<a
class="pf-c-button pf-m-danger"
href="/if/flow/${tenant.flowUnenrollment}"
>
${t`Delete account`}
</a>`;
}
return html``;
}),
)}
</div>
</div>
</div>
</form>`;
}
}

View File

@ -0,0 +1,186 @@
import { t } from "@lingui/macro";
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFGallery from "@patternfly/patternfly/layouts/Gallery/gallery.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import PFSizing from "@patternfly/patternfly/utilities/Sizing/sizing.css";
import PFFlex from "@patternfly/patternfly/utilities/Flex/flex.css";
import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
import AKGlobal from "../../authentik.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import { SourcesApi, StagesApi, UserSetting } from "@goauthentik/api";
import { DEFAULT_CONFIG } from "../../api/Config";
import { until } from "lit-html/directives/until";
import { ifDefined } from "lit-html/directives/if-defined";
import "../../elements/Tabs";
import "../../elements/PageHeader";
import "./tokens/UserTokenList";
import "./UserSelfForm";
import "./settings/UserSettingsAuthenticatorDuo";
import "./settings/UserSettingsAuthenticatorStatic";
import "./settings/UserSettingsAuthenticatorTOTP";
import "./settings/UserSettingsAuthenticatorWebAuthn";
import "./settings/UserSettingsPassword";
import "./settings/SourceSettingsOAuth";
import "./settings/SourceSettingsPlex";
import { EVENT_REFRESH } from "../../constants";
@customElement("ak-user-settings")
export class UserSettingsPage extends LitElement {
static get styles(): CSSResult[] {
return [
PFBase,
PFPage,
PFFlex,
PFDisplay,
PFGallery,
PFContent,
PFCard,
PFDescriptionList,
PFSizing,
PFForm,
PFFormControl,
AKGlobal,
];
}
@property({ attribute: false })
userSettings?: Promise<UserSetting[]>;
@property({ attribute: false })
sourceSettings?: Promise<UserSetting[]>;
constructor() {
super();
this.addEventListener(EVENT_REFRESH, () => {
this.firstUpdated();
});
}
firstUpdated(): void {
this.userSettings = new StagesApi(DEFAULT_CONFIG).stagesAllUserSettingsList();
this.sourceSettings = new SourcesApi(DEFAULT_CONFIG).sourcesAllUserSettingsList();
}
renderStageSettings(stage: UserSetting): TemplateResult {
switch (stage.component) {
case "ak-user-settings-authenticator-webauthn":
return html`<ak-user-settings-authenticator-webauthn
objectId=${stage.objectUid}
.configureUrl=${stage.configureUrl}
>
</ak-user-settings-authenticator-webauthn>`;
case "ak-user-settings-password":
return html`<ak-user-settings-password
objectId=${stage.objectUid}
.configureUrl=${stage.configureUrl}
>
</ak-user-settings-password>`;
case "ak-user-settings-authenticator-totp":
return html`<ak-user-settings-authenticator-totp
objectId=${stage.objectUid}
.configureUrl=${stage.configureUrl}
>
</ak-user-settings-authenticator-totp>`;
case "ak-user-settings-authenticator-static":
return html`<ak-user-settings-authenticator-static
objectId=${stage.objectUid}
.configureUrl=${stage.configureUrl}
>
</ak-user-settings-authenticator-static>`;
case "ak-user-settings-authenticator-duo":
return html`<ak-user-settings-authenticator-duo
objectId=${stage.objectUid}
.configureUrl=${stage.configureUrl}
>
</ak-user-settings-authenticator-duo>`;
default:
return html`<p>${t`Error: unsupported stage settings: ${stage.component}`}</p>`;
}
}
renderSourceSettings(source: UserSetting): TemplateResult {
switch (source.component) {
case "ak-user-settings-source-oauth":
return html`<ak-user-settings-source-oauth
objectId=${source.objectUid}
title=${source.title}
.configureUrl=${source.configureUrl}
>
</ak-user-settings-source-oauth>`;
case "ak-user-settings-source-plex":
return html`<ak-user-settings-source-plex
objectId=${source.objectUid}
title=${source.title}
>
</ak-user-settings-source-plex>`;
default:
return html`<p>${t`Error: unsupported source settings: ${source.component}`}</p>`;
}
}
render(): TemplateResult {
return html`<div class="pf-c-page">
<main role="main" class="pf-c-page__main" tabindex="-1">
<ak-page-header
icon="pf-icon pf-icon-user"
header=${t`User Settings`}
description=${t`Configure settings relevant to your user profile.`}
>
</ak-page-header>
<ak-tabs ?vertical="${true}" style="height: 100%;">
<section
slot="page-details"
data-tab-title="${t`User details`}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-c-card">
<div class="pf-c-card__title">${t`Update details`}</div>
<div class="pf-c-card__body">
<ak-user-self-form .instancePk=${1}></ak-user-self-form>
</div>
</div>
</section>
<section
slot="page-tokens"
data-tab-title="${t`Tokens and App passwords`}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<ak-user-token-list></ak-user-token-list>
</section>
${until(
this.userSettings?.then((stages) => {
return stages.map((stage) => {
return html`<section
slot="page-${stage.objectUid}"
data-tab-title="${ifDefined(stage.title)}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
${this.renderStageSettings(stage)}
</section>`;
});
}),
)}
${until(
this.sourceSettings?.then((source) => {
return source.map((stage) => {
return html`<section
slot="page-${stage.objectUid}"
data-tab-title="${ifDefined(stage.title)}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
${this.renderSourceSettings(stage)}
</section>`;
});
}),
)}
</ak-tabs>
</main>
</div>`;
}
}

View File

@ -0,0 +1,19 @@
import { CSSResult, LitElement, property } from "lit-element";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import AKGlobal from "../../../authentik.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
export abstract class BaseUserSettings extends LitElement {
@property()
objectId!: string;
@property()
configureUrl?: string;
static get styles(): CSSResult[] {
return [PFBase, PFCard, PFButton, PFForm, PFFormControl, AKGlobal];
}
}

View File

@ -0,0 +1,50 @@
import { customElement, html, property, TemplateResult } from "lit-element";
import { BaseUserSettings } from "./BaseUserSettings";
import { SourcesApi } from "@goauthentik/api";
import { until } from "lit-html/directives/until";
import { DEFAULT_CONFIG } from "../../../api/Config";
import { t } from "@lingui/macro";
import { ifDefined } from "lit-html/directives/if-defined";
@customElement("ak-user-settings-source-oauth")
export class SourceSettingsOAuth extends BaseUserSettings {
@property()
title!: string;
render(): TemplateResult {
return html`<div class="pf-c-card">
<div class="pf-c-card__title">${t`Source ${this.title}`}</div>
<div class="pf-c-card__body">${this.renderInner()}</div>
</div>`;
}
renderInner(): TemplateResult {
return html`${until(
new SourcesApi(DEFAULT_CONFIG)
.sourcesUserConnectionsOauthList({
sourceSlug: this.objectId,
})
.then((connection) => {
if (connection.results.length > 0) {
return html`<p>${t`Connected.`}</p>
<button
class="pf-c-button pf-m-danger"
@click=${() => {
return new SourcesApi(
DEFAULT_CONFIG,
).sourcesUserConnectionsOauthDestroy({
id: connection.results[0].pk || 0,
});
}}
>
${t`Disconnect`}
</button>`;
}
return html`<p>${t`Not connected.`}</p>
<a class="pf-c-button pf-m-primary" href=${ifDefined(this.configureUrl)}>
${t`Connect`}
</a>`;
}),
)}`;
}
}

View File

@ -0,0 +1,46 @@
import { customElement, html, property, TemplateResult } from "lit-element";
import { BaseUserSettings } from "./BaseUserSettings";
import { SourcesApi } from "@goauthentik/api";
import { until } from "lit-html/directives/until";
import { DEFAULT_CONFIG } from "../../../api/Config";
import { t } from "@lingui/macro";
@customElement("ak-user-settings-source-plex")
export class SourceSettingsPlex extends BaseUserSettings {
@property()
title!: string;
render(): TemplateResult {
return html`<div class="pf-c-card">
<div class="pf-c-card__title">${t`Source ${this.title}`}</div>
<div class="pf-c-card__body">${this.renderInner()}</div>
</div>`;
}
renderInner(): TemplateResult {
return html`${until(
new SourcesApi(DEFAULT_CONFIG)
.sourcesUserConnectionsPlexList({
sourceSlug: this.objectId,
})
.then((connection) => {
if (connection.results.length > 0) {
return html`<p>${t`Connected.`}</p>
<button
class="pf-c-button pf-m-danger"
@click=${() => {
return new SourcesApi(
DEFAULT_CONFIG,
).sourcesUserConnectionsPlexDestroy({
id: connection.results[0].pk || 0,
});
}}
>
${t`Disconnect`}
</button>`;
}
return html`<p>${t`Not connected.`}</p>`;
}),
)}`;
}
}

View File

@ -0,0 +1,79 @@
import { AuthenticatorsApi } from "@goauthentik/api";
import { t } from "@lingui/macro";
import { customElement, html, TemplateResult } from "lit-element";
import { until } from "lit-html/directives/until";
import { DEFAULT_CONFIG } from "../../../api/Config";
import { BaseUserSettings } from "./BaseUserSettings";
import { EVENT_REFRESH } from "../../../constants";
@customElement("ak-user-settings-authenticator-duo")
export class UserSettingsAuthenticatorDuo extends BaseUserSettings {
renderEnabled(): TemplateResult {
return html`<div class="pf-c-card__body">
<p>
${t`Status: Enabled`}
<i class="pf-icon pf-icon-ok"></i>
</p>
</div>
<div class="pf-c-card__footer">
<button
class="pf-c-button pf-m-danger"
@click=${() => {
return new AuthenticatorsApi(DEFAULT_CONFIG)
.authenticatorsDuoList({})
.then((devices) => {
if (devices.results.length < 1) {
return;
}
// TODO: Handle multiple devices, currently we assume only one TOTP Device
return new AuthenticatorsApi(DEFAULT_CONFIG)
.authenticatorsDuoDestroy({
id: devices.results[0].pk || 0,
})
.then(() => {
this.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
}),
);
});
});
}}
>
${t`Disable Duo authenticator`}
</button>
</div>`;
}
renderDisabled(): TemplateResult {
return html` <div class="pf-c-card__body">
<p>
${t`Status: Disabled`}
<i class="pf-icon pf-icon-error-circle-o"></i>
</p>
</div>
<div class="pf-c-card__footer">
${this.configureUrl
? html`<a
href="${this.configureUrl}?next=/%23%2Fuser"
class="pf-c-button pf-m-primary"
>${t`Enable Duo authenticator`}
</a>`
: html``}
</div>`;
}
render(): TemplateResult {
return html`<div class="pf-c-card">
<div class="pf-c-card__title">${t`Duo`}</div>
${until(
new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsDuoList({}).then((devices) => {
return devices.results.length > 0
? this.renderEnabled()
: this.renderDisabled();
}),
)}
</div>`;
}
}

View File

@ -0,0 +1,100 @@
import { AuthenticatorsApi } from "@goauthentik/api";
import { t } from "@lingui/macro";
import { CSSResult, customElement, html, TemplateResult } from "lit-element";
import { until } from "lit-html/directives/until";
import { DEFAULT_CONFIG } from "../../../api/Config";
import { STATIC_TOKEN_STYLE } from "../../../flows/stages/authenticator_static/AuthenticatorStaticStage";
import { BaseUserSettings } from "./BaseUserSettings";
import { EVENT_REFRESH } from "../../../constants";
@customElement("ak-user-settings-authenticator-static")
export class UserSettingsAuthenticatorStatic extends BaseUserSettings {
static get styles(): CSSResult[] {
return super.styles.concat(STATIC_TOKEN_STYLE);
}
renderEnabled(): TemplateResult {
return html`<div class="pf-c-card__body">
<p>
${t`Status: Enabled`}
<i class="pf-icon pf-icon-ok"></i>
</p>
<ul class="ak-otp-tokens">
${until(
new AuthenticatorsApi(DEFAULT_CONFIG)
.authenticatorsStaticList({})
.then((devices) => {
if (devices.results.length < 1) {
return;
}
return devices.results[0].tokenSet?.map((token) => {
return html`<li>${token.token}</li>`;
});
}),
)}
</ul>
</div>
<div class="pf-c-card__footer">
<button
class="pf-c-button pf-m-danger"
@click=${() => {
return new AuthenticatorsApi(DEFAULT_CONFIG)
.authenticatorsStaticList({})
.then((devices) => {
if (devices.results.length < 1) {
return;
}
// TODO: Handle multiple devices, currently we assume only one TOTP Device
return new AuthenticatorsApi(DEFAULT_CONFIG)
.authenticatorsStaticDestroy({
id: devices.results[0].pk || 0,
})
.then(() => {
this.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
}),
);
});
});
}}
>
${t`Disable Static Tokens`}
</button>
</div>`;
}
renderDisabled(): TemplateResult {
return html` <div class="pf-c-card__body">
<p>
${t`Status: Disabled`}
<i class="pf-icon pf-icon-error-circle-o"></i>
</p>
</div>
<div class="pf-c-card__footer">
${this.configureUrl
? html`<a
href="${this.configureUrl}?next=/%23%2Fuser"
class="pf-c-button pf-m-primary"
>${t`Enable Static Tokens`}
</a>`
: html``}
</div>`;
}
render(): TemplateResult {
return html`<div class="pf-c-card">
<div class="pf-c-card__title">${t`Static tokens`}</div>
${until(
new AuthenticatorsApi(DEFAULT_CONFIG)
.authenticatorsStaticList({})
.then((devices) => {
return devices.results.length > 0
? this.renderEnabled()
: this.renderDisabled();
}),
)}
</div>`;
}
}

View File

@ -0,0 +1,79 @@
import { AuthenticatorsApi } from "@goauthentik/api";
import { t } from "@lingui/macro";
import { customElement, html, TemplateResult } from "lit-element";
import { until } from "lit-html/directives/until";
import { DEFAULT_CONFIG } from "../../../api/Config";
import { BaseUserSettings } from "./BaseUserSettings";
import { EVENT_REFRESH } from "../../../constants";
@customElement("ak-user-settings-authenticator-totp")
export class UserSettingsAuthenticatorTOTP extends BaseUserSettings {
renderEnabled(): TemplateResult {
return html`<div class="pf-c-card__body">
<p>
${t`Status: Enabled`}
<i class="pf-icon pf-icon-ok"></i>
</p>
</div>
<div class="pf-c-card__footer">
<button
class="pf-c-button pf-m-danger"
@click=${() => {
return new AuthenticatorsApi(DEFAULT_CONFIG)
.authenticatorsTotpList({})
.then((devices) => {
if (devices.results.length < 1) {
return;
}
// TODO: Handle multiple devices, currently we assume only one TOTP Device
return new AuthenticatorsApi(DEFAULT_CONFIG)
.authenticatorsTotpDestroy({
id: devices.results[0].pk || 0,
})
.then(() => {
this.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
}),
);
});
});
}}
>
${t`Disable Time-based OTP`}
</button>
</div>`;
}
renderDisabled(): TemplateResult {
return html` <div class="pf-c-card__body">
<p>
${t`Status: Disabled`}
<i class="pf-icon pf-icon-error-circle-o"></i>
</p>
</div>
<div class="pf-c-card__footer">
${this.configureUrl
? html`<a
href="${this.configureUrl}?next=/%23%2Fuser"
class="pf-c-button pf-m-primary"
>${t`Enable TOTP`}
</a>`
: html``}
</div>`;
}
render(): TemplateResult {
return html`<div class="pf-c-card">
<div class="pf-c-card__title">${t`Time-based One-Time Passwords`}</div>
${until(
new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsTotpList({}).then((devices) => {
return devices.results.length > 0
? this.renderEnabled()
: this.renderDisabled();
}),
)}
</div>`;
}
}

View File

@ -0,0 +1,125 @@
import { CSSResult, customElement, html, TemplateResult } from "lit-element";
import { t } from "@lingui/macro";
import { AuthenticatorsApi, WebAuthnDevice } from "@goauthentik/api";
import { until } from "lit-html/directives/until";
import { DEFAULT_CONFIG } from "../../../api/Config";
import { BaseUserSettings } from "./BaseUserSettings";
import PFDataList from "@patternfly/patternfly/components/DataList/data-list.css";
import "../../../elements/buttons/ModalButton";
import "../../../elements/buttons/SpinnerButton";
import "../../../elements/forms/DeleteForm";
import "../../../elements/forms/Form";
import "../../../elements/forms/ModalForm";
import "../../../elements/forms/HorizontalFormElement";
import { ifDefined } from "lit-html/directives/if-defined";
import { EVENT_REFRESH } from "../../../constants";
@customElement("ak-user-settings-authenticator-webauthn")
export class UserSettingsAuthenticatorWebAuthn extends BaseUserSettings {
static get styles(): CSSResult[] {
return super.styles.concat(PFDataList);
}
renderDelete(device: WebAuthnDevice): TemplateResult {
return html`<ak-forms-delete
.obj=${device}
objectLabel=${t`Authenticator`}
.delete=${() => {
return new AuthenticatorsApi(DEFAULT_CONFIG)
.authenticatorsWebauthnDestroy({
id: device.pk || 0,
})
.then(() => {
this.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
}),
);
});
}}
>
<button slot="trigger" class="pf-c-button pf-m-danger">${t`Delete`}</button>
</ak-forms-delete>`;
}
renderUpdate(device: WebAuthnDevice): TemplateResult {
return html`<ak-forms-modal>
<span slot="submit"> ${t`Update`} </span>
<span slot="header"> ${t`Update`} </span>
<ak-form
slot="form"
successMessage=${t`Successfully updated device.`}
.send=${(data: unknown) => {
return new AuthenticatorsApi(DEFAULT_CONFIG)
.authenticatorsWebauthnUpdate({
id: device.pk || 0,
webAuthnDeviceRequest: data as WebAuthnDevice,
})
.then(() => {
this.requestUpdate();
});
}}
>
<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal
label=${t`Device name`}
?required=${true}
name="name"
>
<input
type="text"
value="${ifDefined(device.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
</form>
</ak-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${t`Update`}</button>
</ak-forms-modal>`;
}
render(): TemplateResult {
return html`<div class="pf-c-card">
<div class="pf-c-card__title">${t`WebAuthn Devices`}</div>
<div class="pf-c-card__body">
<ul class="pf-c-data-list" role="list">
${until(
new AuthenticatorsApi(DEFAULT_CONFIG)
.authenticatorsWebauthnList({})
.then((devices) => {
return devices.results.map((device) => {
return html`<li class="pf-c-data-list__item">
<div class="pf-c-data-list__item-row">
<div class="pf-c-data-list__item-content">
<div class="pf-c-data-list__cell">
${device.name || "-"}
</div>
<div class="pf-c-data-list__cell">
${t`Created ${device.createdOn?.toLocaleString()}`}
</div>
<div class="pf-c-data-list__cell">
${this.renderUpdate(device)}
${this.renderDelete(device)}
</div>
</div>
</div>
</li>`;
});
}),
)}
</ul>
</div>
<div class="pf-c-card__footer">
${this.configureUrl
? html`<a
href="${this.configureUrl}?next=/%23%2Fuser"
class="pf-c-button pf-m-primary"
>${t`Configure WebAuthn`}
</a>`
: html``}
</div>
</div>`;
}
}

View File

@ -0,0 +1,20 @@
import { customElement, html, TemplateResult } from "lit-element";
import { t } from "@lingui/macro";
import { BaseUserSettings } from "./BaseUserSettings";
import { ifDefined } from "lit-html/directives/if-defined";
@customElement("ak-user-settings-password")
export class UserSettingsPassword extends BaseUserSettings {
render(): TemplateResult {
// For this stage we don't need to check for a configureFlow,
// as the stage won't return any UI Elements if no configureFlow is set.
return html`<div class="pf-c-card">
<div class="pf-c-card__title">${t`Change your password`}</div>
<div class="pf-c-card__body">
<a href="${ifDefined(this.configureUrl)}" class="pf-c-button pf-m-primary">
${t`Change password`}
</a>
</div>
</div>`;
}
}

View File

@ -0,0 +1,62 @@
import { CoreApi, IntentEnum, Token } from "@goauthentik/api";
import { t } from "@lingui/macro";
import { customElement, property } from "lit-element";
import { html, TemplateResult } from "lit-html";
import { DEFAULT_CONFIG } from "../../../api/Config";
import { ifDefined } from "lit-html/directives/if-defined";
import "../../../elements/forms/HorizontalFormElement";
import { ModelForm } from "../../../elements/forms/ModelForm";
@customElement("ak-user-token-form")
export class UserTokenForm extends ModelForm<Token, string> {
@property()
intent: IntentEnum = IntentEnum.Api;
loadInstance(pk: string): Promise<Token> {
return new CoreApi(DEFAULT_CONFIG).coreTokensRetrieve({
identifier: pk,
});
}
getSuccessMessage(): string {
if (this.instance) {
return t`Successfully updated token.`;
} else {
return t`Successfully created token.`;
}
}
send = (data: Token): Promise<Token> => {
if (this.instance) {
return new CoreApi(DEFAULT_CONFIG).coreTokensUpdate({
identifier: this.instance.identifier,
tokenRequest: data,
});
} else {
data.intent = this.intent;
return new CoreApi(DEFAULT_CONFIG).coreTokensCreate({
tokenRequest: data,
});
}
};
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${t`Identifier`} ?required=${true} name="identifier">
<input
type="text"
value="${ifDefined(this.instance?.identifier)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Description`} name="description">
<input
type="text"
value="${ifDefined(this.instance?.description)}"
class="pf-c-form-control"
/>
</ak-form-element-horizontal>
</form>`;
}
}

View File

@ -0,0 +1,156 @@
import { t } from "@lingui/macro";
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
import { AKResponse } from "../../../api/Client";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import "../../../elements/forms/DeleteBulkForm";
import "../../../elements/forms/ModalForm";
import "../../../elements/buttons/ModalButton";
import "../../../elements/buttons/Dropdown";
import "../../../elements/buttons/TokenCopyButton";
import { Table, TableColumn } from "../../../elements/table/Table";
import { PAGE_SIZE } from "../../../constants";
import { CoreApi, IntentEnum, Token } from "@goauthentik/api";
import { DEFAULT_CONFIG } from "../../../api/Config";
import "./UserTokenForm";
import { IntentToLabel } from "../../../pages/tokens/TokenListPage";
@customElement("ak-user-token-list")
export class UserTokenList extends Table<Token> {
searchEnabled(): boolean {
return true;
}
expandable = true;
checkbox = true;
@property()
order = "expires";
apiEndpoint(page: number): Promise<AKResponse<Token>> {
return new CoreApi(DEFAULT_CONFIG).coreTokensList({
ordering: this.order,
page: page,
pageSize: PAGE_SIZE,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [new TableColumn(t`Identifier`, "identifier"), new TableColumn("")];
}
static get styles(): CSSResult[] {
return super.styles.concat(PFDescriptionList);
}
renderToolbar(): TemplateResult {
return html`
<ak-forms-modal>
<span slot="submit"> ${t`Create`} </span>
<span slot="header"> ${t`Create Token`} </span>
<ak-user-token-form intent=${IntentEnum.Api} slot="form"> </ak-user-token-form>
<button slot="trigger" class="pf-c-button pf-m-secondary">
${t`Create Token`}
</button>
</ak-forms-modal>
<ak-forms-modal>
<span slot="submit"> ${t`Create`} </span>
<span slot="header"> ${t`Create App password`} </span>
<ak-user-token-form intent=${IntentEnum.AppPassword} slot="form">
</ak-user-token-form>
<button slot="trigger" class="pf-c-button pf-m-secondary">
${t`Create App password`}
</button>
</ak-forms-modal>
${super.renderToolbar()}
`;
}
renderExpanded(item: Token): TemplateResult {
return html` <td role="cell" colspan="3">
<div class="pf-c-table__expandable-row-content">
<dl class="pf-c-description-list pf-m-horizontal">
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${t`User`}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${item.user?.username}
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${t`Expiring`}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${item.expiring ? t`Yes` : t`No`}
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${t`Expiring`}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${item.expiring ? item.expires?.toLocaleString() : "-"}
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${t`Intent`}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${IntentToLabel(item.intent || IntentEnum.Api)}
</div>
</dd>
</div>
</dl>
</div>
</td>
<td></td>`;
}
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${t`Token(s)`}
.objects=${this.selectedElements}
.delete=${(item: Token) => {
return new CoreApi(DEFAULT_CONFIG).coreTokensDestroy({
identifier: item.identifier,
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${t`Delete`}
</button>
</ak-forms-delete-bulk>`;
}
row(item: Token): TemplateResult[] {
return [
html`${item.identifier}`,
html`
<ak-forms-modal>
<span slot="submit"> ${t`Update`} </span>
<span slot="header"> ${t`Update Token`} </span>
<ak-user-token-form slot="form" .instancePk=${item.identifier}>
</ak-user-token-form>
<button slot="trigger" class="pf-c-button pf-m-plain">
<i class="fas fa-edit"></i>
</button>
</ak-forms-modal>
<ak-token-copy-button identifier="${item.identifier}">
${t`Copy Key`}
</ak-token-copy-button>
`,
];
}
}