From fd28f37c0d7df307e5b250b299558c476c047fb9 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 19 Feb 2021 18:43:57 +0100 Subject: [PATCH] web: migrate User list to web --- .../templates/administration/user/list.html | 125 ------------------ authentik/admin/urls.py | 1 - authentik/admin/views/users.py | 38 +----- authentik/core/api/users.py | 11 +- swagger.yaml | 10 ++ web/src/api/Users.ts | 16 ++- web/src/elements/buttons/SpinnerButton.ts | 24 ++-- web/src/interfaces/AdminInterface.ts | 2 +- web/src/pages/users/UserListPage.ts | 113 ++++++++++++++++ web/src/routes.ts | 2 + 10 files changed, 170 insertions(+), 172 deletions(-) delete mode 100644 authentik/admin/templates/administration/user/list.html create mode 100644 web/src/pages/users/UserListPage.ts diff --git a/authentik/admin/templates/administration/user/list.html b/authentik/admin/templates/administration/user/list.html deleted file mode 100644 index fe291cb51..000000000 --- a/authentik/admin/templates/administration/user/list.html +++ /dev/null @@ -1,125 +0,0 @@ -{% extends "administration/base.html" %} - -{% load i18n %} -{% load authentik_utils %} - -{% block content %} -
-
-

- - {% trans 'Users' %} -

-
-
-
-
- {% if object_list %} -
-
- {% include 'partials/toolbar_search.html' %} -
- - - {% trans 'Create' %} - -
-
- -
- {% include 'partials/pagination.html' %} -
-
- - - - - - - - - - - {% for user in object_list %} - - - - - - - {% endfor %} - -
{% trans 'Name' %}{% trans 'Active' %}{% trans 'Last Login' %}
-
-
{{ user.username }}
- {{ user.name }} -
-
- - {{ user.is_active }} - - - - {{ user.last_login }} - - - - - {% trans 'Edit' %} - -
-
- {% if user.is_active %} - - - {% trans 'Disable' %} - -
-
- {% else %} - - - {% trans 'Enable' %} - -
-
- {% endif %} - {% trans 'Reset Password' %} - {% trans 'Impersonate' %} -
-
- {% include 'partials/pagination.html' %} -
- {% else %} -
-
- {% include 'partials/toolbar_search.html' %} -
-
-
-
- -

- {% trans 'No Users.' %} -

-
- {% if request.GET.search != "" %} - {% trans "Your search query doesn't match any users." %} - {% else %} - {% trans 'Currently no users exist. How did you even get here.' %} - {% endif %} -
- - - {% trans 'Create' %} - -
-
-
-
- {% endif %} -
-
-{% endblock %} diff --git a/authentik/admin/urls.py b/authentik/admin/urls.py index 559550204..dc6516f18 100644 --- a/authentik/admin/urls.py +++ b/authentik/admin/urls.py @@ -244,7 +244,6 @@ urlpatterns = [ name="property-mapping-test", ), # Users - path("users/", users.UserListView.as_view(), name="users"), path("users/create/", users.UserCreateView.as_view(), name="user-create"), path("users//update/", users.UserUpdateView.as_view(), name="user-update"), path("users//delete/", users.UserDeleteView.as_view(), name="user-delete"), diff --git a/authentik/admin/views/users.py b/authentik/admin/views/users.py index 434a8c0c2..bc6d673cc 100644 --- a/authentik/admin/views/users.py +++ b/authentik/admin/views/users.py @@ -8,46 +8,22 @@ from django.contrib.messages.views import SuccessMessageMixin from django.http import HttpRequest, HttpResponse from django.http.response import HttpResponseRedirect from django.shortcuts import redirect -from django.urls import reverse, reverse_lazy from django.utils.http import urlencode from django.utils.translation import gettext as _ -from django.views.generic import DetailView, ListView, UpdateView +from django.views.generic import DetailView, UpdateView from guardian.mixins import ( - PermissionListMixin, PermissionRequiredMixin, - get_anonymous_user, ) from authentik.admin.forms.users import UserForm from authentik.admin.views.utils import ( BackSuccessUrlMixin, DeleteMessageView, - SearchListMixin, - UserPaginateListMixin, ) from authentik.core.models import Token, User from authentik.lib.views import CreateAssignPermView -class UserListView( - LoginRequiredMixin, - PermissionListMixin, - UserPaginateListMixin, - SearchListMixin, - ListView, -): - """Show list of all users""" - - model = User - permission_required = "authentik_core.view_user" - ordering = "username" - template_name = "administration/user/list.html" - search_fields = ["username", "name", "attributes"] - - def get_queryset(self): - return super().get_queryset().exclude(pk=get_anonymous_user().pk) - - class UserCreateView( SuccessMessageMixin, BackSuccessUrlMixin, @@ -62,7 +38,7 @@ class UserCreateView( permission_required = "authentik_core.add_user" template_name = "generic/create.html" - success_url = reverse_lazy("authentik_admin:users") + success_url = "/" success_message = _("Successfully created User") @@ -82,7 +58,7 @@ class UserUpdateView( # By default the object's name is user which is used by other checks context_object_name = "object" template_name = "generic/update.html" - success_url = reverse_lazy("authentik_admin:users") + success_url = "/" success_message = _("Successfully updated User") @@ -95,7 +71,7 @@ class UserDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageV # By default the object's name is user which is used by other checks context_object_name = "object" template_name = "generic/delete.html" - success_url = reverse_lazy("authentik_admin:users") + success_url = "/" success_message = _("Successfully deleted User") @@ -112,7 +88,7 @@ class UserDisableView( # By default the object's name is user which is used by other checks context_object_name = "object" template_name = "administration/user/disable.html" - success_url = reverse_lazy("authentik_admin:users") + success_url = "/" success_message = _("Successfully disabled User") def delete(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: @@ -135,7 +111,7 @@ class UserEnableView( # By default the object's name is user which is used by other checks context_object_name = "object" - success_url = reverse_lazy("authentik_admin:users") + success_url = "/" success_message = _("Successfully enabled User") def get(self, request: HttpRequest, *args, **kwargs): @@ -165,4 +141,4 @@ class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailV messages.success( request, _("Password reset link:
%(link)s
" % {"link": link}) ) - return redirect("authentik_admin:users") + return redirect("/") diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index e75d451da..1159b3a7d 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -28,7 +28,16 @@ class UserSerializer(ModelSerializer): class Meta: model = User - fields = ["pk", "username", "name", "is_superuser", "email", "avatar"] + fields = [ + "pk", + "username", + "name", + "is_active", + "last_login", + "is_superuser", + "email", + "avatar", + ] class UserViewSet(ModelViewSet): diff --git a/swagger.yaml b/swagger.yaml index ad050bf70..d4d762204 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -8156,6 +8156,16 @@ definitions: description: User's display name. type: string minLength: 1 + is_active: + title: Active + description: Designates whether this user should be treated as active. Unselect + this instead of deleting accounts. + type: boolean + last_login: + title: Last login + type: string + format: date-time + x-nullable: true is_superuser: title: Is superuser type: boolean diff --git a/web/src/api/Users.ts b/web/src/api/Users.ts index 196702bf2..6bf1a7604 100644 --- a/web/src/api/Users.ts +++ b/web/src/api/Users.ts @@ -1,4 +1,4 @@ -import { DefaultClient, AKResponse } from "./Client"; +import { DefaultClient, AKResponse, QueryArguments } from "./Client"; let _globalMePromise: Promise; @@ -9,11 +9,25 @@ export class User { is_superuser: boolean; email: boolean; avatar: string; + is_active: boolean; + last_login: number; constructor() { throw Error(); } + static get(pk: string): Promise { + return DefaultClient.fetch(["core", "users", pk]); + } + + static list(filter?: QueryArguments): Promise> { + return DefaultClient.fetch>(["core", "users"], filter); + } + + static adminUrl(rest: string): string { + return `/administration/users/${rest}`; + } + static me(): Promise { if (!_globalMePromise) { _globalMePromise = DefaultClient.fetch(["core", "users", "me"]); diff --git a/web/src/elements/buttons/SpinnerButton.ts b/web/src/elements/buttons/SpinnerButton.ts index 99aca32fa..eb80c083a 100644 --- a/web/src/elements/buttons/SpinnerButton.ts +++ b/web/src/elements/buttons/SpinnerButton.ts @@ -70,18 +70,18 @@ export class SpinnerButton extends LitElement { @click=${() => this.callAction()} > ${this.isRunning - ? html` - - - - - - ` - : ""} + ? html` + + + + + + ` + : ""} `; } diff --git a/web/src/interfaces/AdminInterface.ts b/web/src/interfaces/AdminInterface.ts index d56670720..8068e7d49 100644 --- a/web/src/interfaces/AdminInterface.ts +++ b/web/src/interfaces/AdminInterface.ts @@ -47,7 +47,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [ return User.me().then(u => u.is_superuser); }), new SidebarItem("Identity & Cryptography").children( - new SidebarItem("User", "/administration/users/"), + new SidebarItem("User", "/users"), new SidebarItem("Groups", "/groups"), new SidebarItem("Certificates", "/crypto/certificates"), new SidebarItem("Tokens", "/administration/tokens/"), diff --git a/web/src/pages/users/UserListPage.ts b/web/src/pages/users/UserListPage.ts new file mode 100644 index 000000000..340a32600 --- /dev/null +++ b/web/src/pages/users/UserListPage.ts @@ -0,0 +1,113 @@ +import { gettext } from "django"; +import { customElement, html, property, TemplateResult } from "lit-element"; +import { AKResponse } from "../../api/Client"; +import { TablePage } from "../../elements/table/TablePage"; + +import "../../elements/buttons/ModalButton"; +import "../../elements/buttons/Dropdown"; +import { TableColumn } from "../../elements/table/Table"; +import { User } from "../../api/Users"; + +@customElement("ak-user-list") +export class UserListPage extends TablePage { + searchEnabled(): boolean { + return true; + } + pageTitle(): string { + return gettext("Users"); + } + pageDescription(): string { + return ""; + } + pageIcon(): string { + return gettext("pf-icon pf-icon-user"); + } + + @property() + order = "username"; + + apiEndpoint(page: number): Promise> { + return User.list({ + ordering: this.order, + page: page, + search: this.search || "", + }); + } + + columns(): TableColumn[] { + return [ + new TableColumn("Name", "username"), + new TableColumn("Active", "active"), + new TableColumn("Last login", "last_login"), + new TableColumn(""), + ]; + } + + row(item: User): TemplateResult[] { + return [ + html`
+
${item.username}
+ ${item.name} +
`, + html`${item.is_active ? "Yes" : "No"}`, + html`${new Date(item.last_login * 1000).toLocaleString()}`, + html` + + + ${gettext("Edit")} + +
+
+ + + + + + ${gettext("Reset Password")} + + + ${gettext("Impersonate")} + `, + ]; + } + + renderToolbar(): TemplateResult { + return html` + + + ${gettext("Create")} + +
+
+ ${super.renderToolbar()} + `; + } +} diff --git a/web/src/routes.ts b/web/src/routes.ts index 0bcf43ced..fe7fd922b 100644 --- a/web/src/routes.ts +++ b/web/src/routes.ts @@ -21,6 +21,7 @@ import "./pages/providers/ProviderViewPage"; import "./pages/sources/SourcesListPage"; import "./pages/sources/SourceViewPage"; import "./pages/groups/GroupListPage"; +import "./pages/users/UserListPage"; import "./pages/system-tasks/SystemTaskListPage"; export const ROUTES: Route[] = [ @@ -44,6 +45,7 @@ export const ROUTES: Route[] = [ }), new Route(new RegExp("^/policies$"), html``), new Route(new RegExp("^/groups$"), html``), + new Route(new RegExp("^/users$"), html``), new Route(new RegExp("^/flows$"), html``), new Route(new RegExp(`^/flows/(?${SLUG_REGEX})$`)).then((args) => { return html``;