events: replace list view with SPA Page
This commit is contained in:
		| @ -48,6 +48,15 @@ class EventViewSet(ReadOnlyModelViewSet): | ||||
|  | ||||
|     queryset = Event.objects.all() | ||||
|     serializer_class = EventSerializer | ||||
|     ordering = ["-created"] | ||||
|     search_fields = [ | ||||
|         "user", | ||||
|         "action", | ||||
|         "app", | ||||
|         "context", | ||||
|         "client_ip", | ||||
|     ] | ||||
|     filterset_fields = ["action"] | ||||
|  | ||||
|     @swagger_auto_schema( | ||||
|         method="GET", responses={200: EventTopPerUserSerialier(many=True)} | ||||
|  | ||||
| @ -10,7 +10,6 @@ class AuthentikEventsConfig(AppConfig): | ||||
|     name = "authentik.events" | ||||
|     label = "authentik_events" | ||||
|     verbose_name = "authentik Events" | ||||
|     mountpoint = "events/" | ||||
|  | ||||
|     def ready(self): | ||||
|         import_module("authentik.events.signals") | ||||
|  | ||||
| @ -1,90 +0,0 @@ | ||||
| {% extends "base/page.html" %} | ||||
|  | ||||
| {% load i18n %} | ||||
| {% load authentik_utils %} | ||||
|  | ||||
| {% block page_content %} | ||||
| <main role="main" class="pf-c-page__main" tabindex="-1" id="main-content"> | ||||
|     <section class="pf-c-page__main-section pf-m-light"> | ||||
|         <div class="pf-c-content"> | ||||
|             <h1> | ||||
|                 <i class="pf-icon pf-icon-catalog"></i> | ||||
|                 {% trans 'Event Log' %} | ||||
|             </h1> | ||||
|         </div> | ||||
|     </section> | ||||
|     <section class="pf-c-page__main-section pf-m-no-padding-mobile"> | ||||
|         <div class="pf-c-card"> | ||||
|             <div class="pf-c-toolbar"> | ||||
|                 <div class="pf-c-toolbar__content"> | ||||
|                     {% include 'partials/toolbar_search.html' %} | ||||
|                     <button role="ak-refresh" class="pf-c-button pf-m-primary"> | ||||
|                         {% trans 'Refresh' %} | ||||
|                     </button> | ||||
|                     {% include 'partials/pagination.html' %} | ||||
|                 </div> | ||||
|             </div> | ||||
|             <table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid"> | ||||
|                 <thead> | ||||
|                     <tr role="row"> | ||||
|                         <th role="columnheader" scope="col">{% trans 'Action' %}</th> | ||||
|                         <th role="columnheader" scope="col">{% trans 'Context' %}</th> | ||||
|                         <th role="columnheader" scope="col">{% trans 'User' %}</th> | ||||
|                         <th role="columnheader" scope="col">{% trans 'Creation Date' %}</th> | ||||
|                         <th role="columnheader" scope="col">{% trans 'Client IP' %}</th> | ||||
|                     </tr> | ||||
|                 </thead> | ||||
|                 <tbody role="rowgroup"> | ||||
|                     {% for entry in object_list %} | ||||
|                     <tr role="row"> | ||||
|                         <th role="columnheader"> | ||||
|                             <div> | ||||
|                                 <div>{{ entry.action }}</div> | ||||
|                                 <small>{{ entry.app|default:'-' }}</small> | ||||
|                             </div> | ||||
|                         </th> | ||||
|                         <td role="cell"> | ||||
|                             <div> | ||||
|                                 <div> | ||||
|                                     <code>{{ entry.context }}</code> | ||||
|                                 </div> | ||||
|                                 {% if entry.user.on_behalf_of %} | ||||
|                                 <small> | ||||
|                                     {% blocktrans with username=entry.user.on_behalf_of.username %} | ||||
|                                     On behalf of {{ username }} | ||||
|                                     {% endblocktrans %} | ||||
|                                 </small> | ||||
|                                 {% endif %} | ||||
|                             </div> | ||||
|                         </td> | ||||
|                         <td role="cell"> | ||||
|                             <div> | ||||
|                                 <div>{{ entry.user.username }}</div> | ||||
|                                 <small> | ||||
|                                     {% blocktrans with pk=entry.user.pk %} | ||||
|                                     ID: {{ pk }} | ||||
|                                     {% endblocktrans %} | ||||
|                                 </small> | ||||
|                             </div> | ||||
|                         </td> | ||||
|                         <td role="cell"> | ||||
|                             <span> | ||||
|                                 {{ entry.created }} | ||||
|                             </span> | ||||
|                         </td> | ||||
|                         <td role="cell"> | ||||
|                             <span> | ||||
|                                 {{ entry.client_ip }} | ||||
|                             </span> | ||||
|                         </td> | ||||
|                     </tr> | ||||
|                     {% endfor %} | ||||
|                 </tbody> | ||||
|             </table> | ||||
|             <div class="pf-c-pagination pf-m-bottom"> | ||||
|                 {% include 'partials/pagination.html' %} | ||||
|             </div> | ||||
|         </div> | ||||
|     </section> | ||||
| </main> | ||||
| {% endblock %} | ||||
| @ -1,9 +0,0 @@ | ||||
| """authentik events urls""" | ||||
| from django.urls import path | ||||
|  | ||||
| from authentik.events.views import EventListView | ||||
|  | ||||
| urlpatterns = [ | ||||
|     # Event Log | ||||
|     path("log/", EventListView.as_view(), name="log"), | ||||
| ] | ||||
| @ -1,30 +0,0 @@ | ||||
| """authentik Event administration""" | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||||
| from django.views.generic import ListView | ||||
| from guardian.mixins import PermissionListMixin | ||||
|  | ||||
| from authentik.admin.views.utils import SearchListMixin, UserPaginateListMixin | ||||
| from authentik.events.models import Event | ||||
|  | ||||
|  | ||||
| class EventListView( | ||||
|     PermissionListMixin, | ||||
|     LoginRequiredMixin, | ||||
|     SearchListMixin, | ||||
|     UserPaginateListMixin, | ||||
|     ListView, | ||||
| ): | ||||
|     """Show list of all invitations""" | ||||
|  | ||||
|     model = Event | ||||
|     template_name = "events/list.html" | ||||
|     permission_required = "authentik_events.view_event" | ||||
|     ordering = "-created" | ||||
|  | ||||
|     search_fields = [ | ||||
|         "user", | ||||
|         "action", | ||||
|         "app", | ||||
|         "context", | ||||
|         "client_ip", | ||||
|     ] | ||||
| @ -78,6 +78,8 @@ class FlowViewSet(ModelViewSet): | ||||
|     queryset = Flow.objects.all() | ||||
|     serializer_class = FlowSerializer | ||||
|     lookup_field = "slug" | ||||
|     search_fields = ["name", "slug", "designation", "title"] | ||||
|     filterset_fields = ["flow_uuid", "name", "slug", "designation"] | ||||
|  | ||||
|     @swagger_auto_schema(responses={200: FlowDiagramSerializer()}) | ||||
|     @action(detail=True, methods=["get"]) | ||||
|  | ||||
| @ -142,7 +142,7 @@ SWAGGER_SETTINGS = { | ||||
| REST_FRAMEWORK = { | ||||
|     "DEFAULT_PAGINATION_CLASS": "authentik.api.pagination.Pagination", | ||||
|     "PAGE_SIZE": 100, | ||||
|     'DATETIME_FORMAT': '%s', | ||||
|     "DATETIME_FORMAT": "%s", | ||||
|     "DEFAULT_FILTER_BACKENDS": [ | ||||
|         "rest_framework_guardian.filters.ObjectPermissionsFilter", | ||||
|         "django_filters.rest_framework.DjangoFilterBackend", | ||||
|  | ||||
							
								
								
									
										30
									
								
								swagger.yaml
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								swagger.yaml
									
									
									
									
									
								
							| @ -868,6 +868,11 @@ paths: | ||||
|       operationId: events_events_list | ||||
|       description: Event Read-Only Viewset | ||||
|       parameters: | ||||
|         - name: action | ||||
|           in: query | ||||
|           description: '' | ||||
|           required: false | ||||
|           type: string | ||||
|         - name: ordering | ||||
|           in: query | ||||
|           description: Which field to use when ordering the results. | ||||
| @ -919,6 +924,11 @@ paths: | ||||
|       operationId: events_events_top_per_user | ||||
|       description: Get the top_n events grouped by user count | ||||
|       parameters: | ||||
|         - name: action | ||||
|           in: query | ||||
|           description: '' | ||||
|           required: false | ||||
|           type: string | ||||
|         - name: ordering | ||||
|           in: query | ||||
|           description: Which field to use when ordering the results. | ||||
| @ -1194,6 +1204,26 @@ paths: | ||||
|       operationId: flows_instances_list | ||||
|       description: Flow Viewset | ||||
|       parameters: | ||||
|         - name: flow_uuid | ||||
|           in: query | ||||
|           description: '' | ||||
|           required: false | ||||
|           type: string | ||||
|         - name: name | ||||
|           in: query | ||||
|           description: '' | ||||
|           required: false | ||||
|           type: string | ||||
|         - name: slug | ||||
|           in: query | ||||
|           description: '' | ||||
|           required: false | ||||
|           type: string | ||||
|         - name: designation | ||||
|           in: query | ||||
|           description: '' | ||||
|           required: false | ||||
|           type: string | ||||
|         - name: ordering | ||||
|           in: query | ||||
|           description: Which field to use when ordering the results. | ||||
|  | ||||
| @ -1,6 +1,37 @@ | ||||
| import { DefaultClient } from "./Client"; | ||||
| import { DefaultClient, PBResponse, QueryArguments } from "./Client"; | ||||
|  | ||||
| export interface EventUser { | ||||
|     pk: number; | ||||
|     email?: string; | ||||
|     username: string; | ||||
|     on_behalf_of?: EventUser; | ||||
| } | ||||
|  | ||||
| export interface EventContext { | ||||
|     [key: string]: EventContext | string | number; | ||||
| } | ||||
|  | ||||
| export class Event { | ||||
|     pk: string; | ||||
|     user: EventUser; | ||||
|     action: string; | ||||
|     app: string; | ||||
|     context: EventContext; | ||||
|     client_ip: string; | ||||
|     created: string; | ||||
|  | ||||
|     constructor() { | ||||
|         throw Error(); | ||||
|     } | ||||
|  | ||||
|     static get(pk: string): Promise<Event> { | ||||
|         return DefaultClient.fetch<Event>(["events", "events", pk]); | ||||
|     } | ||||
|  | ||||
|     static list(filter?: QueryArguments): Promise<PBResponse<Event>> { | ||||
|         return DefaultClient.fetch<PBResponse<Event>>(["events", "events"], filter); | ||||
|     } | ||||
|  | ||||
|     // events/events/top_per_user/?filter_action=authorize_application | ||||
|     static topForUser(action: string): Promise<TopNEvent[]> { | ||||
|         return DefaultClient.fetch<TopNEvent[]>(["events", "events", "top_per_user"], { | ||||
|  | ||||
| @ -9,7 +9,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [ | ||||
|     new SidebarItem("Monitor").children( | ||||
|         new SidebarItem("Overview", "/administration/overview"), | ||||
|         new SidebarItem("System Tasks", "/administration/tasks/"), | ||||
|         new SidebarItem("Events", "/events/log/"), | ||||
|         new SidebarItem("Events", "/events"), | ||||
|     ).when((): Promise<boolean> => { | ||||
|         return User.me().then(u => u.is_superuser); | ||||
|     }), | ||||
|  | ||||
							
								
								
									
										116
									
								
								web/src/pages/events/EventInfo.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								web/src/pages/events/EventInfo.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,116 @@ | ||||
| import { gettext } from "django"; | ||||
| import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; | ||||
| import { until } from "lit-html/directives/until"; | ||||
| import { Event, EventContext } from "../../api/Events"; | ||||
| import { Flow } from "../../api/Flows"; | ||||
| import { COMMON_STYLES } from "../../common/styles"; | ||||
| import "../../elements/Spinner"; | ||||
| import { SpinnerSize } from "../../elements/Spinner"; | ||||
|  | ||||
| @customElement("ak-event-info") | ||||
| export class EventInfo extends LitElement { | ||||
|  | ||||
|     @property({attribute: false}) | ||||
|     event?: Event; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return COMMON_STYLES.concat( | ||||
|             css` | ||||
|                 code { | ||||
|                     display: block; | ||||
|                     white-space: pre-wrap; | ||||
|                 } | ||||
|             ` | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     getModelInfo(context: EventContext): TemplateResult { | ||||
|         return html`<ul class="pf-c-list"> | ||||
|             <li>${gettext("UID")}: ${context.pk as string}</li> | ||||
|             <li>${gettext("Name")}: ${context.name as string}</li> | ||||
|             <li>${gettext("App")}: ${context.app as string}</li> | ||||
|             <li>${gettext("Model Name")}: ${context.model_name as string}</li> | ||||
|         </ul>`; | ||||
|     } | ||||
|  | ||||
|     defaultResponse(): TemplateResult { | ||||
|         return html`<div class="pf-l-flex"> | ||||
|                     <div class="pf-l-flex__item"> | ||||
|                         <h3>${gettext("Context")}</h3> | ||||
|                         <code>${JSON.stringify(this.event?.context)}</code> | ||||
|                     </div> | ||||
|                     <div class="pf-l-flex__item"> | ||||
|                         <h3>${gettext("User")}</h3> | ||||
|                         <code>${JSON.stringify(this.event?.user)}</code> | ||||
|                     </div> | ||||
|                 </div>`; | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         if (!this.event) { | ||||
|             return html`<ak-spinner size=${SpinnerSize.Medium}></ak-spinner>`; | ||||
|         } | ||||
|         switch (this.event?.action) { | ||||
|         case "model_created": | ||||
|         case "model_updated": | ||||
|         case "model_deleted": | ||||
|             return html` | ||||
|                 <h3>${gettext("Affected model:")}</h3><hr> | ||||
|                 ${this.getModelInfo(this.event.context.model as EventContext)} | ||||
|                 `; | ||||
|         case "authorize_application": | ||||
|             return html`<div class="pf-l-flex"> | ||||
|                     <div class="pf-l-flex__item"> | ||||
|                         <h3>${gettext("Authorized application:")}</h3><hr> | ||||
|                         ${this.getModelInfo(this.event.context.authorized_application as EventContext)} | ||||
|                     </div> | ||||
|                     <div class="pf-l-flex__item"> | ||||
|                         <h3>${gettext("Using flow")}</h3> | ||||
|                         <span>${until(Flow.list({ | ||||
|         flow_uuid: this.event.context.flow as string, | ||||
|     }).then(resp => { | ||||
|         return html`<a href="#/flows/${resp.results[0].slug}">${resp.results[0].name}</a>`; | ||||
|     }), html`<ak-spinner size=${SpinnerSize.Medium}></ak-spinner>`)}</span> | ||||
|                     </div> | ||||
|                 </div>`; | ||||
|         case "login_failed": | ||||
|             return html` | ||||
|                 <h3>${gettext(`Attempted to log in as ${this.event.context.username}`)}</h3> | ||||
|                 `; | ||||
|         case "token_view": | ||||
|             return html` | ||||
|                 <h3>${gettext("Token:")}</h3><hr> | ||||
|                 ${this.getModelInfo(this.event.context.token as EventContext)} | ||||
|                 `; | ||||
|         case "property_mapping_exception": | ||||
|         case "policy_exception": | ||||
|             return html`<div class="pf-l-flex"> | ||||
|                     <div class="pf-l-flex__item"> | ||||
|                         <h3>${gettext("Exception")}</h3> | ||||
|                         <code>${this.event.context.error}</code> | ||||
|                     </div> | ||||
|                     <div class="pf-l-flex__item"> | ||||
|                         <h3>${gettext("Expression")}</h3> | ||||
|                         <code>${this.event.context.expression}</code> | ||||
|                     </div> | ||||
|                 </div>`; | ||||
|         case "configuration_error": | ||||
|             return html`<h3>${this.event.context.message}</h3>`; | ||||
|         case "update_available": | ||||
|             return html`<h3>${gettext("New version available!")}</h3> | ||||
|                 <a target="_blank" href="https://github.com/BeryJu/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 "login": | ||||
|         case "logout": | ||||
|             if (this.event.context === {}) { | ||||
|                 return html`<span>${gettext("No additional data available.")}</span>`; | ||||
|             } | ||||
|             return this.defaultResponse(); | ||||
|         default: | ||||
|             return this.defaultResponse(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										71
									
								
								web/src/pages/events/EventListPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								web/src/pages/events/EventListPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,71 @@ | ||||
| import { gettext } from "django"; | ||||
| import { customElement, html, property, TemplateResult } from "lit-element"; | ||||
| import { PBResponse } from "../../api/Client"; | ||||
| import { Event } from "../../api/Events"; | ||||
| import { TableColumn } from "../../elements/table/Table"; | ||||
| import { TablePage } from "../../elements/table/TablePage"; | ||||
| import { time } from "../../utils"; | ||||
| import "./EventInfo"; | ||||
|  | ||||
| @customElement("ak-event-list") | ||||
| export class EventListPage extends TablePage<Event> { | ||||
|     expandable = true; | ||||
|  | ||||
|     pageTitle(): string { | ||||
|         return "Event Log"; | ||||
|     } | ||||
|     pageDescription(): string | undefined { | ||||
|         return; | ||||
|     } | ||||
|     pageIcon(): string { | ||||
|         return "pf-icon pf-icon-catalog"; | ||||
|     } | ||||
|     searchEnabled(): boolean { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     @property() | ||||
|     order = "-created"; | ||||
|  | ||||
|     apiEndpoint(page: number): Promise<PBResponse<Event>> { | ||||
|         return Event.list({ | ||||
|             ordering: this.order, | ||||
|             page: page, | ||||
|             search: this.search || "", | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     columns(): TableColumn[] { | ||||
|         return [ | ||||
|             new TableColumn("Action", "action"), | ||||
|             new TableColumn("User", "user"), | ||||
|             new TableColumn("Creation Date", "created"), | ||||
|             new TableColumn("Client IP", "client_ip"), | ||||
|         ]; | ||||
|     } | ||||
|     row(item: Event): TemplateResult[] { | ||||
|         return [ | ||||
|             html`<div>${item.action}</div> | ||||
|             <small>${item.app}</small>`, | ||||
|             html`<div>${item.user.username}</div> | ||||
|             ${item.user.on_behalf_of ? html`<small> | ||||
|                 ${gettext(`On behalf of ${item.user.on_behalf_of.username}`)} | ||||
|             </small>` : html``}`, | ||||
|             html`<span>${time(item.created).toLocaleString()}</span>`, | ||||
|             html`<span>${item.client_ip}</span>`, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderExpanded(item: Event): TemplateResult { | ||||
|         return html` | ||||
|         <td role="cell" colspan="4"> | ||||
|             <div class="pf-c-table__expandable-row-content"> | ||||
|                 <ak-event-info .event=${item}></ak-event-info> | ||||
|             </div> | ||||
|         </td> | ||||
|         <td></td> | ||||
|         <td></td> | ||||
|         <td></td>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @ -7,6 +7,7 @@ import "./pages/applications/ApplicationListPage"; | ||||
| import "./pages/applications/ApplicationViewPage"; | ||||
| import "./pages/sources/SourceViewPage"; | ||||
| import "./pages/flows/FlowViewPage"; | ||||
| import "./pages/events/EventListPage"; | ||||
|  | ||||
| export const ROUTES: Route[] = [ | ||||
|     // Prevent infinite Shell loops | ||||
| @ -24,4 +25,5 @@ export const ROUTES: Route[] = [ | ||||
|     new Route(new RegExp(`^/flows/(?<slug>${SLUG_REGEX})$`)).then((args) => { | ||||
|         return html`<ak-flow-view .args=${args}></ak-flow-view>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/events$"), html`<ak-event-list></ak-event-list>`), | ||||
| ]; | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer