web: re-organise frontend and cleanup common code (#3572)

* fix repo in api client

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web: re-organise files to match their interface

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* core: include version in script tags

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* cleanup maybe broken

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* revert rename

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web: get rid of Client.ts

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* move more to common

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* more moving

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* format

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* unfuck files that vscode fucked, thanks

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* move more

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* finish moving (maybe)

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* ok more moving

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* fix more stuff that vs code destroyed

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* get rid "web" prefix for virtual package

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* fix locales

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* use custom base element

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* fix css file

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* don't run autoDetectLanguage when importing locale

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* fix circular dependencies

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web: fix build

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens L
2022-09-15 00:05:21 +02:00
committed by GitHub
parent 369440652c
commit 4a91a7d2e2
291 changed files with 2062 additions and 1921 deletions

View File

@ -0,0 +1,399 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { VERSION } from "@goauthentik/common/constants";
import { EventContext, EventModel, EventWithContext } from "@goauthentik/common/events";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/Expand";
import "@goauthentik/elements/Spinner";
import { PFSize } from "@goauthentik/elements/Spinner";
import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { until } from "lit/directives/until.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import PFList from "@patternfly/patternfly/components/List/list.css";
import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { EventActions, FlowsApi } from "@goauthentik/api";
@customElement("ak-event-info")
export class EventInfo extends AKElement {
@property({ attribute: false })
event!: EventWithContext;
static get styles(): CSSResult[] {
return [
PFBase,
PFButton,
PFFlex,
PFList,
PFDescriptionList,
css`
code {
display: block;
white-space: pre-wrap;
}
.pf-l-flex {
justify-content: space-between;
}
.pf-l-flex__item {
min-width: 25%;
}
iframe {
width: 100%;
height: 50rem;
}
`,
];
}
getModelInfo(context: EventModel): TemplateResult {
if (context === null) {
return html`<span>-</span>`;
}
return html`<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`UID`}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${context.pk}</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`Name`}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${context.name}</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`App`}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${context.app}</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`Model Name`}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${context.model_name}</div>
</dd>
</div>
</dl>`;
}
getEmailInfo(context: EventContext): TemplateResult {
if (context === null) {
return html`<span>-</span>`;
}
return html`<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`Message`}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${context.message}</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`Subject`}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${context.subject}</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`From`}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${context.from_email}</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`To`}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${(context.to_email as string[]).map((to) => {
return html`<li>${to}</li>`;
})}
</div>
</dd>
</div>
</dl>`;
}
defaultResponse(): TemplateResult {
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<h3>${t`Context`}</h3>
<code>${JSON.stringify(this.event?.context, null, 4)}</code>
</div>
<div class="pf-l-flex__item">
<h3>${t`User`}</h3>
<code>${JSON.stringify(this.event?.user, null, 4)}</code>
</div>
</div>`;
}
buildGitHubIssueUrl(context: EventContext): string {
const httpRequest = this.event.context.http_request as EventContext;
let title = "";
if (httpRequest) {
title = `${httpRequest?.method} ${httpRequest?.path}`;
}
// https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-issues/about-automation-for-issues-and-pull-requests-with-query-parameters
const fullBody = `
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Logs**
<details>
<summary>Stacktrace from authentik</summary>
\`\`\`
${context.message as string}
\`\`\`
</details>
**Version and Deployment (please complete the following information):**
- authentik version: ${VERSION}
- Deployment: [e.g. docker-compose, helm]
**Additional context**
Add any other context about the problem here.
`;
return `https://github.com/goauthentik/authentik/issues/
new?labels=bug,from_authentik&title=${encodeURIComponent(title)}
&body=${encodeURIComponent(fullBody)}`.trim();
}
render(): TemplateResult {
if (!this.event) {
return html`<ak-spinner size=${PFSize.Medium}></ak-spinner>`;
}
switch (this.event?.action) {
case EventActions.ModelCreated:
case EventActions.ModelUpdated:
case EventActions.ModelDeleted:
return html`
<h3>${t`Affected model:`}</h3>
${this.getModelInfo(this.event.context?.model as EventModel)}
`;
case EventActions.AuthorizeApplication:
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<h3>${t`Authorized application:`}</h3>
${this.getModelInfo(
this.event.context.authorized_application as EventModel,
)}
</div>
<div class="pf-l-flex__item">
<h3>${t`Using flow`}</h3>
<span
>${until(
new FlowsApi(DEFAULT_CONFIG)
.flowsInstancesList({
flowUuid: this.event.context.flow as string,
})
.then((resp) => {
return html`<a
href="#/flow/flows/${resp.results[0].slug}"
>${resp.results[0].name}</a
>`;
}),
html`<ak-spinner size=${PFSize.Medium}></ak-spinner>`,
)}
</span>
</div>
</div>
<ak-expand>${this.defaultResponse()}</ak-expand>`;
case EventActions.EmailSent:
return html`<h3>${t`Email info:`}</h3>
${this.getEmailInfo(this.event.context)}
<ak-expand>
<iframe srcdoc=${this.event.context.body}></iframe>
</ak-expand>`;
case EventActions.SecretView:
return html` <h3>${t`Secret:`}</h3>
${this.getModelInfo(this.event.context.secret as EventModel)}`;
case EventActions.SystemException:
return html` <a
class="pf-c-button pf-m-primary"
target="_blank"
href=${this.buildGitHubIssueUrl(this.event.context)}
>
${t`Open issue on GitHub...`}
</a>
<div class="pf-l-flex">
<div class="pf-l-flex__item">
<h3>${t`Exception`}</h3>
<code>${this.event.context.message}</code>
</div>
</div>
<ak-expand>${this.defaultResponse()}</ak-expand>`;
case EventActions.PropertyMappingException:
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<h3>${t`Exception`}</h3>
<code>${this.event.context.message || this.event.context.error}</code>
</div>
<div class="pf-l-flex__item">
<h3>${t`Expression`}</h3>
<code>${this.event.context.expression}</code>
</div>
</div>
<ak-expand>${this.defaultResponse()}</ak-expand>`;
case EventActions.PolicyException:
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<h3>${t`Binding`}</h3>
${this.getModelInfo(this.event.context.binding as EventModel)}
</div>
<div class="pf-l-flex__item">
<h3>${t`Request`}</h3>
<ul class="pf-c-list">
<li>
${t`Object`}:
${this.getModelInfo(
(this.event.context.request as EventContext)
.obj as EventModel,
)}
</li>
<li>
<span
>${t`Context`}:
<code
>${JSON.stringify(
(this.event.context.request as EventContext)
.context,
null,
4,
)}</code
></span
>
</li>
</ul>
</div>
<div class="pf-l-flex__item">
<h3>${t`Exception`}</h3>
<code>${this.event.context.message || this.event.context.error}</code>
</div>
</div>
<ak-expand>${this.defaultResponse()}</ak-expand>`;
case EventActions.PolicyExecution:
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<h3>${t`Binding`}</h3>
${this.getModelInfo(this.event.context.binding as EventModel)}
</div>
<div class="pf-l-flex__item">
<h3>${t`Request`}</h3>
<ul class="pf-c-list">
<li>
${t`Object`}:
${this.getModelInfo(
(this.event.context.request as EventContext)
.obj as EventModel,
)}
</li>
<li>
<span
>${t`Context`}:
<code
>${JSON.stringify(
(this.event.context.request as EventContext)
.context,
null,
4,
)}</code
></span
>
</li>
</ul>
</div>
<div class="pf-l-flex__item">
<h3>${t`Result`}</h3>
<ul class="pf-c-list">
<li>
${t`Passing`}:
${(this.event.context.result as EventContext).passing}
</li>
<li>
${t`Messages`}:
<ul class="pf-c-list">
${(
(this.event.context.result as EventContext)
.messages as string[]
).map((msg) => {
return html`<li>${msg}</li>`;
})}
</ul>
</li>
</ul>
</div>
</div>
<ak-expand>${this.defaultResponse()}</ak-expand>`;
case EventActions.ConfigurationError:
return html`<h3>${this.event.context.message}</h3>
<ak-expand>${this.defaultResponse()}</ak-expand>`;
case EventActions.UpdateAvailable:
return html`<h3>${t`New version available!`}</h3>
<a
target="_blank"
href="https://github.com/goauthentik/authentik/releases/tag/version%2F${this
.event.context.new_version}"
>
${this.event.context.new_version}
</a>`;
// Action types which typically don't record any extra context.
// If context is not empty, we fall to the default response.
case EventActions.Login:
if ("using_source" in this.event.context) {
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<h3>${t`Using source`}</h3>
${this.getModelInfo(this.event.context.using_source as EventModel)}
</div>
</div>`;
}
return this.defaultResponse();
case EventActions.LoginFailed:
return html` <h3>${t`Attempted to log in as ${this.event.context.username}`}</h3>
<ak-expand>${this.defaultResponse()}</ak-expand>`;
case EventActions.Logout:
if (Object.keys(this.event.context).length === 0) {
return html`<span>${t`No additional data available.`}</span>`;
}
return this.defaultResponse();
default:
return this.defaultResponse();
}
}
}

View File

@ -0,0 +1,55 @@
import "@goauthentik/admin/events/EventInfo";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EventWithContext } from "@goauthentik/common/events";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/PageHeader";
import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import AKGlobal from "@goauthentik/common/styles/authentik.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { EventsApi } from "@goauthentik/api";
@customElement("ak-event-info-page")
export class EventInfoPage extends AKElement {
@property()
set eventID(value: string) {
new EventsApi(DEFAULT_CONFIG)
.eventsEventsRetrieve({
eventUuid: value,
})
.then((ev) => {
this.event = ev as EventWithContext;
});
}
@property({ attribute: false })
event!: EventWithContext;
static get styles(): CSSResult[] {
return [PFBase, PFPage, PFContent, PFCard, AKGlobal];
}
render(): TemplateResult {
return html`<ak-page-header
icon="pf-icon pf-icon-catalog"
header=${t`Event ${this.event?.pk || ""}`}
>
</ak-page-header>
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
<div class="pf-c-card__title">${t`Event info`}</div>
<div class="pf-c-card__body">
<ak-event-info .event=${this.event}></ak-event-info>
</div>
</div>
</section>`;
}
}

View File

@ -0,0 +1,88 @@
import "@goauthentik/admin/events/EventInfo";
import { ActionToLabel } from "@goauthentik/admin/events/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EventWithContext } from "@goauthentik/common/events";
import { uiConfig } from "@goauthentik/common/ui/config";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Event, EventsApi } from "@goauthentik/api";
@customElement("ak-event-list")
export class EventListPage extends TablePage<Event> {
expandable = true;
pageTitle(): string {
return t`Event Log`;
}
pageDescription(): string | undefined {
return;
}
pageIcon(): string {
return "pf-icon pf-icon-catalog";
}
searchEnabled(): boolean {
return true;
}
@property()
order = "-created";
async apiEndpoint(page: number): Promise<PaginatedResponse<Event>> {
return new EventsApi(DEFAULT_CONFIG).eventsEventsList({
ordering: this.order,
page: page,
pageSize: (await uiConfig()).pagination.perPage,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [
new TableColumn(t`Action`, "action"),
new TableColumn(t`User`, "user"),
new TableColumn(t`Creation Date`, "created"),
new TableColumn(t`Client IP`, "client_ip"),
new TableColumn(t`Tenant`, "tenant_name"),
new TableColumn(t`Actions`),
];
}
row(item: EventWithContext): TemplateResult[] {
return [
html`<div>${ActionToLabel(item.action)}</div>
<small>${item.app}</small>`,
item.user?.username
? html`<a href="#/identity/users/${item.user.pk}"> ${item.user?.username} </a>
${item.user.on_behalf_of
? html`<small>
${t`On behalf of ${item.user.on_behalf_of.username}`}
</small>`
: html``}`
: html`-`,
html`<span>${item.created?.toLocaleString()}</span>`,
html`<span>${item.clientIp || t`-`}</span>`,
html`<span>${item.tenant?.name || t`-`}</span>`,
html`<a href="#/events/log/${item.pk}">
<i class="fas fa-share-square"></i>
</a>`,
];
}
renderExpanded(item: Event): TemplateResult {
return html` <td role="cell" colspan="3">
<div class="pf-c-table__expandable-row-content">
<ak-event-info .event=${item as EventWithContext}></ak-event-info>
</div>
</td>
<td></td>
<td></td>
<td></td>`;
}
}

View File

@ -0,0 +1,133 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { CoreApi, EventsApi, NotificationRule, SeverityEnum } from "@goauthentik/api";
@customElement("ak-event-rule-form")
export class RuleForm extends ModelForm<NotificationRule, string> {
loadInstance(pk: string): Promise<NotificationRule> {
return new EventsApi(DEFAULT_CONFIG).eventsRulesRetrieve({
pbmUuid: pk,
});
}
getSuccessMessage(): string {
if (this.instance) {
return t`Successfully updated rule.`;
} else {
return t`Successfully created rule.`;
}
}
send = (data: NotificationRule): Promise<NotificationRule> => {
if (this.instance) {
return new EventsApi(DEFAULT_CONFIG).eventsRulesUpdate({
pbmUuid: this.instance.pk || "",
notificationRuleRequest: data,
});
} else {
return new EventsApi(DEFAULT_CONFIG).eventsRulesCreate({
notificationRuleRequest: data,
});
}
};
renderSeverity(): TemplateResult {
return html`
<option
value=${SeverityEnum.Alert}
?selected=${this.instance?.severity === SeverityEnum.Alert}
>
${t`Alert`}
</option>
<option
value=${SeverityEnum.Warning}
?selected=${this.instance?.severity === SeverityEnum.Warning}
>
${t`Warning`}
</option>
<option
value=${SeverityEnum.Notice}
?selected=${this.instance?.severity === SeverityEnum.Notice}
>
${t`Notice`}
</option>
`;
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-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
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Group`} name="group">
<select class="pf-c-form-control">
<option value="" ?selected=${this.instance?.group === undefined}>
---------
</option>
${until(
new CoreApi(DEFAULT_CONFIG).coreGroupsList({}).then((groups) => {
return groups.results.map((group) => {
return html`<option
value=${ifDefined(group.pk)}
?selected=${this.instance?.group === group.pk}
>
${group.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Transports`} ?required=${true} name="transports">
<select name="users" class="pf-c-form-control" multiple>
${until(
new EventsApi(DEFAULT_CONFIG)
.eventsTransportsList({})
.then((transports) => {
return transports.results.map((transport) => {
const selected = Array.from(
this.instance?.transports || [],
).some((su) => {
return su == transport.pk;
});
return html`<option
value=${ifDefined(transport.pk)}
?selected=${selected}
>
${transport.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select>
<p class="pf-c-form__helper-text">
${t`Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI.`}
</p>
<p class="pf-c-form__helper-text">
${t`Hold control/command to select multiple items.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Severity`} ?required=${true} name="severity">
<select class="pf-c-form-control">
${this.renderSeverity()}
</select>
</ak-form-element-horizontal>
</form>`;
}
}

View File

@ -0,0 +1,118 @@
import "@goauthentik/admin/events/RuleForm";
import "@goauthentik/admin/policies/BoundPoliciesList";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { EventsApi, NotificationRule } from "@goauthentik/api";
@customElement("ak-event-rule-list")
export class RuleListPage extends TablePage<NotificationRule> {
expandable = true;
checkbox = true;
searchEnabled(): boolean {
return true;
}
pageTitle(): string {
return t`Notification Rules`;
}
pageDescription(): string {
return t`Send notifications whenever a specific Event is created and matched by policies.`;
}
pageIcon(): string {
return "pf-icon pf-icon-attention-bell";
}
@property()
order = "name";
async apiEndpoint(page: number): Promise<PaginatedResponse<NotificationRule>> {
return new EventsApi(DEFAULT_CONFIG).eventsRulesList({
ordering: this.order,
page: page,
pageSize: (await uiConfig()).pagination.perPage,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [
new TableColumn(t`Name`, "name"),
new TableColumn(t`Severity`, "severity"),
new TableColumn(t`Sent to group`, "group"),
new TableColumn(t`Actions`),
];
}
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${t`Notification rule(s)`}
.objects=${this.selectedElements}
.usedBy=${(item: NotificationRule) => {
return new EventsApi(DEFAULT_CONFIG).eventsRulesUsedByList({
pbmUuid: item.pk,
});
}}
.delete=${(item: NotificationRule) => {
return new EventsApi(DEFAULT_CONFIG).eventsRulesDestroy({
pbmUuid: item.pk,
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${t`Delete`}
</button>
</ak-forms-delete-bulk>`;
}
row(item: NotificationRule): TemplateResult[] {
return [
html`${item.name}`,
html`${item.severity}`,
html`${item.groupObj?.name || t`None (rule disabled)`}`,
html`<ak-forms-modal>
<span slot="submit"> ${t`Update`} </span>
<span slot="header"> ${t`Update Notification Rule`} </span>
<ak-event-rule-form slot="form" .instancePk=${item.pk}> </ak-event-rule-form>
<button slot="trigger" class="pf-c-button pf-m-plain">
<i class="fas fa-edit"></i>
</button>
</ak-forms-modal>`,
];
}
renderObjectCreate(): TemplateResult {
return html`
<ak-forms-modal>
<span slot="submit"> ${t`Create`} </span>
<span slot="header"> ${t`Create Notification Rule`} </span>
<ak-event-rule-form slot="form"> </ak-event-rule-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${t`Create`}</button>
</ak-forms-modal>
`;
}
renderExpanded(item: NotificationRule): TemplateResult {
return html` <td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<p>
${t`These bindings control upon which events this rule triggers. Bindings to
groups/users are checked against the user of the event.`}
</p>
<ak-bound-policies-list .target=${item.pk}> </ak-bound-policies-list>
</div>
</td>`;
}
}

View File

@ -0,0 +1,171 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import {
EventsApi,
NotificationTransport,
NotificationTransportModeEnum,
PropertymappingsApi,
} from "@goauthentik/api";
@customElement("ak-event-transport-form")
export class TransportForm extends ModelForm<NotificationTransport, string> {
loadInstance(pk: string): Promise<NotificationTransport> {
return new EventsApi(DEFAULT_CONFIG)
.eventsTransportsRetrieve({
uuid: pk,
})
.then((transport) => {
this.onModeChange(transport.mode);
return transport;
});
}
@property({ type: Boolean })
showWebhook = false;
getSuccessMessage(): string {
if (this.instance) {
return t`Successfully updated transport.`;
} else {
return t`Successfully created transport.`;
}
}
send = (data: NotificationTransport): Promise<NotificationTransport> => {
if (this.instance) {
return new EventsApi(DEFAULT_CONFIG).eventsTransportsUpdate({
uuid: this.instance.pk || "",
notificationTransportRequest: data,
});
} else {
return new EventsApi(DEFAULT_CONFIG).eventsTransportsCreate({
notificationTransportRequest: data,
});
}
};
renderTransportModes(): TemplateResult {
return html`
<option
value=${NotificationTransportModeEnum.Local}
?selected=${this.instance?.mode === NotificationTransportModeEnum.Local}
>
${t`Local (notifications will be created within authentik)`}
</option>
<option
value=${NotificationTransportModeEnum.Email}
?selected=${this.instance?.mode === NotificationTransportModeEnum.Email}
>
${t`Email`}
</option>
<option
value=${NotificationTransportModeEnum.Webhook}
?selected=${this.instance?.mode === NotificationTransportModeEnum.Webhook}
>
${t`Webhook (generic)`}
</option>
<option
value=${NotificationTransportModeEnum.WebhookSlack}
?selected=${this.instance?.mode === NotificationTransportModeEnum.WebhookSlack}
>
${t`Webhook (Slack/Discord)`}
</option>
`;
}
onModeChange(mode: string | undefined): void {
if (
mode === NotificationTransportModeEnum.Webhook ||
mode === NotificationTransportModeEnum.WebhookSlack
) {
this.showWebhook = true;
} else {
this.showWebhook = false;
}
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-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
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Mode`} ?required=${true} name="mode">
<select
class="pf-c-form-control"
@change=${(ev: Event) => {
const current = (ev.target as HTMLInputElement).value;
this.onModeChange(current);
}}
>
${this.renderTransportModes()}
</select>
</ak-form-element-horizontal>
<ak-form-element-horizontal
?hidden=${!this.showWebhook}
label=${t`Webhook URL`}
name="webhookUrl"
?required=${true}
>
<input
type="text"
value="${ifDefined(this.instance?.webhookUrl)}"
class="pf-c-form-control"
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
?hidden=${!this.showWebhook}
label=${t`Webhook Mapping`}
name="webhookMapping"
>
<select class="pf-c-form-control">
<option value="" ?selected=${this.instance?.webhookMapping === undefined}>
---------
</option>
${until(
new PropertymappingsApi(DEFAULT_CONFIG)
.propertymappingsNotificationList({})
.then((mappings) => {
return mappings.results.map((mapping) => {
return html`<option
value=${ifDefined(mapping.pk)}
?selected=${this.instance?.webhookMapping === mapping.pk}
>
${mapping.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="sendOnce">
<div class="pf-c-check">
<input
type="checkbox"
class="pf-c-check__input"
?checked=${first(this.instance?.sendOnce, false)}
/>
<label class="pf-c-check__label"> ${t`Send once`} </label>
</div>
<p class="pf-c-form__helper-text">
${t`Only send notification once, for example when sending a webhook into a chat channel.`}
</p>
</ak-form-element-horizontal>
</form>`;
}
}

View File

@ -0,0 +1,114 @@
import "@goauthentik/admin/events/TransportForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { EventsApi, NotificationTransport } from "@goauthentik/api";
@customElement("ak-event-transport-list")
export class TransportListPage extends TablePage<NotificationTransport> {
searchEnabled(): boolean {
return true;
}
pageTitle(): string {
return t`Notification Transports`;
}
pageDescription(): string {
return t`Define how notifications are sent to users, like Email or Webhook.`;
}
pageIcon(): string {
return "pf-icon pf-icon-export";
}
checkbox = true;
@property()
order = "name";
async apiEndpoint(page: number): Promise<PaginatedResponse<NotificationTransport>> {
return new EventsApi(DEFAULT_CONFIG).eventsTransportsList({
ordering: this.order,
page: page,
pageSize: (await uiConfig()).pagination.perPage,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [
new TableColumn(t`Name`, "name"),
new TableColumn(t`Mode`, "mode"),
new TableColumn(t`Actions`),
];
}
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${t`Notification transport(s)`}
.objects=${this.selectedElements}
.usedBy=${(item: NotificationTransport) => {
return new EventsApi(DEFAULT_CONFIG).eventsTransportsUsedByList({
uuid: item.pk,
});
}}
.delete=${(item: NotificationTransport) => {
return new EventsApi(DEFAULT_CONFIG).eventsTransportsDestroy({
uuid: item.pk,
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${t`Delete`}
</button>
</ak-forms-delete-bulk>`;
}
row(item: NotificationTransport): TemplateResult[] {
return [
html`${item.name}`,
html`${item.modeVerbose}`,
html`<ak-forms-modal>
<span slot="submit"> ${t`Update`} </span>
<span slot="header"> ${t`Update Notification Transport`} </span>
<ak-event-transport-form slot="form" .instancePk=${item.pk}>
</ak-event-transport-form>
<button slot="trigger" class="pf-c-button pf-m-plain">
<i class="fas fa-edit"></i>
</button>
</ak-forms-modal>
<ak-action-button
class="pf-m-plain"
.apiRequest=${() => {
return new EventsApi(DEFAULT_CONFIG).eventsTransportsTestCreate({
uuid: item.pk || "",
});
}}
>
<i class="fas fa-vial" aria-hidden="true"></i>
</ak-action-button>`,
];
}
renderObjectCreate(): TemplateResult {
return html`
<ak-forms-modal>
<span slot="submit"> ${t`Create`} </span>
<span slot="header"> ${t`Create Notification Transport`} </span>
<ak-event-transport-form slot="form"> </ak-event-transport-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${t`Create`}</button>
</ak-forms-modal>
`;
}
}

View File

@ -0,0 +1,63 @@
import { t } from "@lingui/macro";
import { EventActions } from "@goauthentik/api";
export function ActionToLabel(action?: EventActions): string {
if (!action) return "";
switch (action) {
case EventActions.Login:
return t`Login`;
case EventActions.LoginFailed:
return t`Failed login`;
case EventActions.Logout:
return t`Logout`;
case EventActions.UserWrite:
return t`User was written to`;
case EventActions.SuspiciousRequest:
return t`Suspicious request`;
case EventActions.PasswordSet:
return t`Password set`;
case EventActions.SecretView:
return t`Secret was viewed`;
case EventActions.SecretRotate:
return t`Secret was rotated`;
case EventActions.InvitationUsed:
return t`Invitation used`;
case EventActions.AuthorizeApplication:
return t`Application authorized`;
case EventActions.SourceLinked:
return t`Source linked`;
case EventActions.ImpersonationStarted:
return t`Impersonation started`;
case EventActions.ImpersonationEnded:
return t`Impersonation ended`;
case EventActions.FlowExecution:
return t`Flow execution`;
case EventActions.PolicyExecution:
return t`Policy execution`;
case EventActions.PolicyException:
return t`Policy exception`;
case EventActions.PropertyMappingException:
return t`Property Mapping exception`;
case EventActions.SystemTaskExecution:
return t`System task execution`;
case EventActions.SystemTaskException:
return t`System task exception`;
case EventActions.SystemException:
return t`General system exception`;
case EventActions.ConfigurationError:
return t`Configuration error`;
case EventActions.ModelCreated:
return t`Model created`;
case EventActions.ModelUpdated:
return t`Model updated`;
case EventActions.ModelDeleted:
return t`Model deleted`;
case EventActions.EmailSent:
return t`Email sent`;
case EventActions.UpdateAvailable:
return t`Update available`;
default:
return action;
}
}