diff --git a/web/src/admin/admin-overview/cards/RecentEventsCard.ts b/web/src/admin/admin-overview/cards/RecentEventsCard.ts index b02c992b2..11c197dd8 100644 --- a/web/src/admin/admin-overview/cards/RecentEventsCard.ts +++ b/web/src/admin/admin-overview/cards/RecentEventsCard.ts @@ -1,9 +1,9 @@ -import "@goauthentik/admin/events/EventInfo"; import { EventGeo } from "@goauthentik/admin/events/utils"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EventWithContext } from "@goauthentik/common/events"; import { actionToLabel } from "@goauthentik/common/labels"; import { truncate } from "@goauthentik/common/utils"; +import "@goauthentik/components/ak-event-info"; import "@goauthentik/elements/Tabs"; import "@goauthentik/elements/buttons/Dropdown"; import "@goauthentik/elements/buttons/ModalButton"; diff --git a/web/src/admin/events/EventInfo.ts b/web/src/admin/events/EventInfo.ts deleted file mode 100644 index 0a1b32cc4..000000000 --- a/web/src/admin/events/EventInfo.ts +++ /dev/null @@ -1,442 +0,0 @@ -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 { msg, str } from "@lit/localize"; -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 PFCard from "@patternfly/patternfly/components/Card/card.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, - PFCard, - PFList, - PFDescriptionList, - css` - code { - display: block; - white-space: pre-wrap; - word-break: break-all; - } - .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`-`; - } - return html`
-
-
-
- ${msg("UID")} -
-
-
${context.pk}
-
-
-
-
- ${msg("Name")} -
-
-
${context.name}
-
-
-
-
- ${msg("App")} -
-
-
${context.app}
-
-
-
-
- ${msg("Model Name")} -
-
-
${context.model_name}
-
-
-
-
`; - } - - getEmailInfo(context: EventContext): TemplateResult { - if (context === null) { - return html`-`; - } - return html`
-
-
- ${msg("Message")} -
-
-
${context.message}
-
-
-
-
- ${msg("Subject")} -
-
-
${context.subject}
-
-
-
-
- ${msg("From")} -
-
-
${context.from_email}
-
-
-
-
- ${msg("To")} -
-
-
- ${(context.to_email as string[]).map((to) => { - return html`
  • ${to}
  • `; - })} -
    -
    -
    -
    `; - } - - defaultResponse(): TemplateResult { - return html`
    -
    -
    ${msg("Context")}
    -
    - ${JSON.stringify(this.event?.context, null, 4)} -
    -
    -
    -
    ${msg("User")}
    -
    - ${JSON.stringify(this.event?.user, null, 4)} -
    -
    -
    `; - } - - 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** -
    - Stacktrace from authentik - -\`\`\` -${context.message as string} -\`\`\` -
    - - -**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``; - } - switch (this.event?.action) { - case EventActions.ModelCreated: - case EventActions.ModelUpdated: - case EventActions.ModelDeleted: - return html` -
    ${msg("Affected model:")}
    -
    - ${this.getModelInfo(this.event.context?.model as EventModel)} -
    - `; - case EventActions.AuthorizeApplication: - return html`
    -
    -
    ${msg("Authorized application:")}
    -
    - ${this.getModelInfo( - this.event.context.authorized_application as EventModel, - )} -
    -
    -
    -
    ${msg("Using flow")}
    -
    - ${until( - new FlowsApi(DEFAULT_CONFIG) - .flowsInstancesList({ - flowUuid: this.event.context.flow as string, - }) - .then((resp) => { - return html`${resp.results[0].name}`; - }), - html``, - )} - -
    -
    -
    - ${this.defaultResponse()}`; - case EventActions.EmailSent: - return html`
    ${msg("Email info:")}
    -
    ${this.getEmailInfo(this.event.context)}
    - - - `; - case EventActions.SecretView: - return html`
    ${msg("Secret:")}
    - ${this.getModelInfo(this.event.context.secret as EventModel)}`; - case EventActions.SystemException: - return html`
    -
    -
    ${msg("Exception")}
    - -
    -
    ${this.event.context.message}
    -
    -
    -
    - ${this.defaultResponse()}`; - case EventActions.PropertyMappingException: - return html`
    -
    -
    ${msg("Exception")}
    -
    -
    ${this.event.context.message || this.event.context.error}
    -
    -
    -
    -
    ${msg("Expression")}
    -
    - ${this.event.context.expression} -
    -
    -
    - ${this.defaultResponse()}`; - case EventActions.PolicyException: - return html`
    -
    -
    ${msg("Binding")}
    - ${this.getModelInfo(this.event.context.binding as EventModel)} -
    -
    -
    ${msg("Request")}
    -
    -
      -
    • - ${msg("Object")}: - ${this.getModelInfo( - (this.event.context.request as EventContext) - .obj as EventModel, - )} -
    • -
    • - ${msg("Context")}: - ${JSON.stringify( - (this.event.context.request as EventContext) - .context, - null, - 4, - )} -
    • -
    -
    -
    -
    -
    ${msg("Exception")}
    -
    - ${this.event.context.message || this.event.context.error} -
    -
    -
    - ${this.defaultResponse()}`; - case EventActions.PolicyExecution: - return html`
    -
    -
    ${msg("Binding")}
    - ${this.getModelInfo(this.event.context.binding as EventModel)} -
    -
    -
    ${msg("Request")}
    -
    -
      -
    • - ${msg("Object")}: - ${this.getModelInfo( - (this.event.context.request as EventContext) - .obj as EventModel, - )} -
    • -
    • - ${msg("Context")}: - ${JSON.stringify( - (this.event.context.request as EventContext) - .context, - null, - 4, - )} -
    • -
    -
    -
    -
    -
    ${msg("Result")}
    -
    -
      -
    • - ${msg("Passing")}: - ${(this.event.context.result as EventContext).passing} -
    • -
    • - ${msg("Messages")}: -
        - ${( - (this.event.context.result as EventContext) - .messages as string[] - ).map((msg) => { - return html`
      • ${msg}
      • `; - })} -
      -
    • -
    -
    -
    -
    - ${this.defaultResponse()}`; - case EventActions.ConfigurationError: - return html`
    ${this.event.context.message}
    - ${this.defaultResponse()}`; - case EventActions.UpdateAvailable: - return html`
    ${msg("New version available!")}
    - - ${this.event.context.new_version} - `; - // 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`
    -
    -
    ${msg("Using source")}
    - ${this.getModelInfo(this.event.context.using_source as EventModel)} -
    -
    `; - } - return this.defaultResponse(); - case EventActions.LoginFailed: - return html`
    - ${msg(str`Attempted to log in as ${this.event.context.username}`)} -
    - ${this.defaultResponse()}`; - case EventActions.Logout: - if (Object.keys(this.event.context).length === 0) { - return html`${msg("No additional data available.")}`; - } - return this.defaultResponse(); - case EventActions.SystemTaskException: - return html`
    -
    -
    ${msg("Exception")}
    -
    -
    ${this.event.context.message}
    -
    -
    -
    `; - default: - return this.defaultResponse(); - } - } -} diff --git a/web/src/admin/events/EventListPage.ts b/web/src/admin/events/EventListPage.ts index 38b310106..4d42e8c74 100644 --- a/web/src/admin/events/EventListPage.ts +++ b/web/src/admin/events/EventListPage.ts @@ -1,9 +1,9 @@ -import "@goauthentik/admin/events/EventInfo"; import { EventGeo } from "@goauthentik/admin/events/utils"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EventWithContext } from "@goauthentik/common/events"; import { actionToLabel } from "@goauthentik/common/labels"; import { uiConfig } from "@goauthentik/common/ui/config"; +import "@goauthentik/components/ak-event-info"; import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { TableColumn } from "@goauthentik/elements/table/Table"; import { TablePage } from "@goauthentik/elements/table/TablePage"; diff --git a/web/src/admin/events/EventViewPage.ts b/web/src/admin/events/EventViewPage.ts index ad23514eb..b5351840a 100644 --- a/web/src/admin/events/EventViewPage.ts +++ b/web/src/admin/events/EventViewPage.ts @@ -1,8 +1,8 @@ -import "@goauthentik/admin/events/EventInfo"; import { EventGeo } from "@goauthentik/admin/events/utils"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EventWithContext } from "@goauthentik/common/events"; import { actionToLabel } from "@goauthentik/common/labels"; +import "@goauthentik/components/ak-event-info"; import { AKElement } from "@goauthentik/elements/Base"; import "@goauthentik/elements/PageHeader"; diff --git a/web/src/components/ak-event-info.ts b/web/src/components/ak-event-info.ts new file mode 100644 index 000000000..6901f31a8 --- /dev/null +++ b/web/src/components/ak-event-info.ts @@ -0,0 +1,491 @@ +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 { msg, str } from "@lit/localize"; +import { CSSResult, TemplateResult, css, html } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { map } from "lit/directives/map.js"; +import { until } from "lit/directives/until.js"; + +import PFButton from "@patternfly/patternfly/components/Button/button.css"; +import PFCard from "@patternfly/patternfly/components/Card/card.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"; + +type Pair = [string, string | number | EventContext | EventModel | string[] | TemplateResult]; + +// https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-issues/about-automation-for-issues-and-pull-requests-with-query-parameters + +// This is the template message body with our stacktrace passed to github via a querystring. It is +// 702 bytes long in UTF-8. [As of July +// 2023](https://saturncloud.io/blog/what-is-the-maximum-length-of-a-url-in-different-browsers/), +// the longest URL (not query string, **URL**) passable via this method is 2048 bytes. This is a bit +// of a hack, but it will get the top of the context across even if it exceeds the limit of the more +// restrictive browsers. + +const githubIssueMessageBody = (context: EventContext) => ` +**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** +
    + Stacktrace from authentik + +\`\`\` +${context.message as string} +\`\`\` +
    + + +**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. + `; + +@customElement("ak-event-info") +export class EventInfo extends AKElement { + @property({ attribute: false }) + event!: EventWithContext; + + static get styles(): CSSResult[] { + return [ + PFBase, + PFButton, + PFFlex, + PFCard, + PFList, + PFDescriptionList, + css` + code { + display: block; + white-space: pre-wrap; + word-break: break-all; + } + .pf-l-flex { + justify-content: space-between; + } + .pf-l-flex__item { + min-width: 25%; + } + iframe { + width: 100%; + height: 50rem; + } + `, + ]; + } + + renderDescriptionGroup([term, description]: Pair) { + return html`
    +
    + ${term} +
    +
    +
    ${description}
    +
    +
    `; + } + + getModelInfo(context: EventModel): TemplateResult { + if (context === null) { + return html`-`; + } + + const modelFields: Pair[] = [ + [msg("UID"), context.pk], + [msg("Name"), context.name], + [msg("App"), context.app], + [msg("Model Name"), context.model_name], + ]; + + return html`
    +
    + ${map(modelFields, this.renderDescriptionGroup)} +
    +
    `; + } + + getEmailInfo(context: EventContext): TemplateResult { + if (context === null) { + return html`-`; + } + + // prettier-ignore + const emailFields: Pair[] = [ + [msg("Message"), context.message], + [msg("Subject"), context.subject], + [msg("From"), context.from_email], + [msg("To"), html`${(context.to_email as string[]).map((to) => { + return html`
  • ${to}
  • `; + })}`], + ]; + + return html`
    + ${map(emailFields, this.renderDescriptionGroup)} +
    `; + } + + renderDefaultResponse(): TemplateResult { + return html`
    +
    +
    ${msg("Context")}
    +
    + ${JSON.stringify(this.event?.context, null, 4)} +
    +
    +
    +
    ${msg("User")}
    +
    + ${JSON.stringify(this.event?.user, null, 4)} +
    +
    +
    `; + } + + buildGitHubIssueUrl(context: EventContext): string { + const httpRequest = this.event.context.http_request as EventContext; + const title = httpRequest ? `${httpRequest?.method} ${httpRequest?.path}` : ""; + + return [ + "https://github.com/goauthentik/authentik/issues/new", + "?labels=bug,from_authentik", + `&title=${encodeURIComponent(title)}`, + `&body=${encodeURIComponent(githubIssueMessageBody(context))}`, + ] + .join("") + .trim(); + } + + // It's commonplace not to put the return type on most functions in Typescript. In this case, + // however, putting this return type creates a virtuous check of *all* the subrenderers to + // ensure that all of them return what we're expecting. + + render(): TemplateResult { + if (!this.event) { + return html``; + } + + switch (this.event?.action) { + case EventActions.ModelCreated: + case EventActions.ModelUpdated: + case EventActions.ModelDeleted: + return this.renderModelChanged(); + + case EventActions.AuthorizeApplication: + return this.renderAuthorizeApplication(); + + case EventActions.EmailSent: + return this.renderEmailSent(); + + case EventActions.SecretView: + return this.renderSecretView(); + + case EventActions.SystemException: + return this.renderSystemException(); + + case EventActions.PropertyMappingException: + return this.renderPropertyMappingException(); + + case EventActions.PolicyException: + return this.renderPolicyException(); + + case EventActions.PolicyExecution: + return this.renderPolicyExecution(); + + case EventActions.ConfigurationError: + return this.renderConfigurationError(); + + case EventActions.UpdateAvailable: + return this.renderUpdateAvailable(); + + // Action types which typically don't record any extra context. + // If context is not empty, we fall to the default response. + case EventActions.Login: + return this.renderLogin(); + + case EventActions.LoginFailed: + return this.renderLoginFailed(); + + case EventActions.Logout: + return this.renderLogout(); + + case EventActions.SystemTaskException: + return this.renderSystemTaskException(); + + default: + return this.renderDefaultResponse(); + } + } + + renderModelChanged() { + return html` +
    ${msg("Affected model:")}
    +
    + ${this.getModelInfo(this.event.context?.model as EventModel)} +
    + `; + } + + renderAuthorizeApplication() { + return html`
    +
    +
    ${msg("Authorized application:")}
    +
    + ${this.getModelInfo( + this.event.context.authorized_application as EventModel, + )} +
    +
    +
    +
    ${msg("Using flow")}
    +
    + ${until( + new FlowsApi(DEFAULT_CONFIG) + .flowsInstancesList({ + flowUuid: this.event.context.flow as string, + }) + .then((resp) => { + return html`${resp.results[0].name}`; + }), + html``, + )} + +
    +
    +
    + ${this.renderDefaultResponse()}`; + } + + renderEmailSent() { + return html`
    ${msg("Email info:")}
    +
    ${this.getEmailInfo(this.event.context)}
    + + + `; + } + + renderSecretView() { + return html`
    ${msg("Secret:")}
    + ${this.getModelInfo(this.event.context.secret as EventModel)}`; + } + + renderSystemException() { + return html`
    +
    +
    ${msg("Exception")}
    + +
    +
    ${this.event.context.message}
    +
    +
    +
    + ${this.renderDefaultResponse()}`; + } + + renderPropertyMappingException() { + return html`
    +
    +
    ${msg("Exception")}
    +
    +
    ${this.event.context.message || this.event.context.error}
    +
    +
    +
    +
    ${msg("Expression")}
    +
    + ${this.event.context.expression} +
    +
    +
    + ${this.renderDefaultResponse()}`; + } + + renderPolicyException() { + return html`
    +
    +
    ${msg("Binding")}
    + ${this.getModelInfo(this.event.context.binding as EventModel)} +
    +
    +
    ${msg("Request")}
    +
    +
      +
    • + ${msg("Object")}: + ${this.getModelInfo( + (this.event.context.request as EventContext).obj as EventModel, + )} +
    • +
    • + ${msg("Context")}: + ${JSON.stringify( + (this.event.context.request as EventContext).context, + null, + 4, + )} +
    • +
    +
    +
    +
    +
    ${msg("Exception")}
    +
    + ${this.event.context.message || this.event.context.error} +
    +
    +
    + ${this.renderDefaultResponse()}`; + } + + renderPolicyExecution() { + return html`
    +
    +
    ${msg("Binding")}
    + ${this.getModelInfo(this.event.context.binding as EventModel)} +
    +
    +
    ${msg("Request")}
    +
    +
      +
    • + ${msg("Object")}: + ${this.getModelInfo( + (this.event.context.request as EventContext).obj as EventModel, + )} +
    • +
    • + ${msg("Context")}: + ${JSON.stringify( + (this.event.context.request as EventContext).context, + null, + 4, + )} +
    • +
    +
    +
    +
    +
    ${msg("Result")}
    +
    +
      +
    • + ${msg("Passing")}: + ${(this.event.context.result as EventContext).passing} +
    • +
    • + ${msg("Messages")}: +
        + ${( + (this.event.context.result as EventContext) + .messages as string[] + ).map((msg) => { + return html`
      • ${msg}
      • `; + })} +
      +
    • +
    +
    +
    +
    + ${this.renderDefaultResponse()}`; + } + + renderConfigurationError() { + return html`
    ${this.event.context.message}
    + ${this.renderDefaultResponse()}`; + } + + renderUpdateAvailable() { + return html`
    ${msg("New version available")}
    + + ${this.event.context.new_version} + `; + // Action types which typically don't record any extra context. + // If context is not empty, we fall to the default response. + } + + renderLogin() { + if ("using_source" in this.event.context) { + return html`
    +
    +
    ${msg("Using source")}
    + ${this.getModelInfo(this.event.context.using_source as EventModel)} +
    +
    `; + } + return this.renderDefaultResponse(); + } + + renderLoginFailed() { + return html`
    + ${msg(str`Attempted to log in as ${this.event.context.username}`)} +
    + ${this.renderDefaultResponse()}`; + } + + renderLogout() { + if (Object.keys(this.event.context).length === 0) { + return html`${msg("No additional data available.")}`; + } + return this.renderDefaultResponse(); + } + + renderSystemTaskException() { + return html`
    +
    +
    ${msg("Exception")}
    +
    +
    ${this.event.context.message}
    +
    +
    +
    `; + } +} diff --git a/web/src/components/events/ObjectChangelog.ts b/web/src/components/events/ObjectChangelog.ts index d9e53000f..160a98d73 100644 --- a/web/src/components/events/ObjectChangelog.ts +++ b/web/src/components/events/ObjectChangelog.ts @@ -1,7 +1,7 @@ -import "@goauthentik/admin/events/EventInfo"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EventWithContext } from "@goauthentik/common/events"; import { uiConfig } from "@goauthentik/common/ui/config"; +import "@goauthentik/components/ak-event-info"; import "@goauthentik/elements/Tabs"; import "@goauthentik/elements/buttons/Dropdown"; import "@goauthentik/elements/buttons/ModalButton"; diff --git a/web/src/components/events/UserEvents.ts b/web/src/components/events/UserEvents.ts index 6820953d2..8b8065792 100644 --- a/web/src/components/events/UserEvents.ts +++ b/web/src/components/events/UserEvents.ts @@ -1,15 +1,13 @@ -import "@goauthentik/admin/events/EventInfo"; -import "@goauthentik/admin/events/EventInfo"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EventWithContext } from "@goauthentik/common/events"; import { actionToLabel } from "@goauthentik/common/labels"; import { uiConfig } from "@goauthentik/common/ui/config"; +import "@goauthentik/components/ak-event-info"; import "@goauthentik/elements/Tabs"; import "@goauthentik/elements/buttons/Dropdown"; import "@goauthentik/elements/buttons/ModalButton"; import "@goauthentik/elements/buttons/SpinnerButton"; -import { PaginatedResponse } from "@goauthentik/elements/table/Table"; -import { Table, TableColumn } from "@goauthentik/elements/table/Table"; +import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table"; import { msg, str } from "@lit/localize"; import { TemplateResult, html } from "lit";