web: migrate User list to web

This commit is contained in:
Jens Langhammer 2021-02-19 18:43:57 +01:00
parent d219f65e7a
commit fd28f37c0d
10 changed files with 170 additions and 172 deletions

View File

@ -1,125 +0,0 @@
{% extends "administration/base.html" %}
{% load i18n %}
{% load authentik_utils %}
{% block content %}
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>
<i class="pf-icon pf-icon-user"></i>
{% trans 'Users' %}
</h1>
</div>
</section>
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<div class="pf-c-toolbar__bulk-select">
<ak-modal-button href="{% url 'authentik_admin:user-create' %}">
<ak-spinner-button slot="trigger" class="pf-m-primary">
{% trans 'Create' %}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<button role="ak-refresh" class="pf-c-button pf-m-primary">
{% trans 'Refresh' %}
</button>
</div>
{% 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 'Name' %}</th>
<th role="columnheader" scope="col">{% trans 'Active' %}</th>
<th role="columnheader" scope="col">{% trans 'Last Login' %}</th>
<th role="cell"></th>
</tr>
</thead>
<tbody role="rowgroup">
{% for user in object_list %}
<tr role="row">
<th role="columnheader">
<div>
<div>{{ user.username }}</div>
<small>{{ user.name }}</small>
</div>
</th>
<td role="cell">
<span>
{{ user.is_active }}
</span>
</td>
<td role="cell">
<span>
{{ user.last_login }}
</span>
</td>
<td>
<ak-modal-button href="{% url 'authentik_admin:user-update' pk=user.pk %}">
<ak-spinner-button slot="trigger" class="pf-m-secondary">
{% trans 'Edit' %}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
{% if user.is_active %}
<ak-modal-button href="{% url 'authentik_admin:user-disable' pk=user.pk %}">
<ak-spinner-button slot="trigger" class="pf-m-warning">
{% trans 'Disable' %}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
{% else %}
<ak-modal-button href="{% url 'authentik_admin:user-delete' pk=user.pk %}">
<ak-spinner-button slot="trigger" class="pf-m-primary">
{% trans 'Enable' %}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
{% endif %}
<a class="pf-c-button pf-m-tertiary ak-root-link" href="{% url 'authentik_admin:user-password-reset' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Reset Password' %}</a>
<a class="pf-c-button pf-m-tertiary ak-root-link" href="{% url 'authentik_core:impersonate-init' user_id=user.pk %}">{% trans 'Impersonate' %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="pf-icon pf-icon-user pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Users.' %}
</h1>
<div class="pf-c-empty-state__body">
{% 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 %}
</div>
<ak-modal-button href="{% url 'authentik_admin:user-create' %}">
<ak-spinner-button slot="trigger" class="pf-m-primary">
{% trans 'Create' %}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
</div>
</div>
{% endif %}
</div>
</section>
{% endblock %}

View File

@ -244,7 +244,6 @@ urlpatterns = [
name="property-mapping-test", name="property-mapping-test",
), ),
# Users # Users
path("users/", users.UserListView.as_view(), name="users"),
path("users/create/", users.UserCreateView.as_view(), name="user-create"), path("users/create/", users.UserCreateView.as_view(), name="user-create"),
path("users/<int:pk>/update/", users.UserUpdateView.as_view(), name="user-update"), path("users/<int:pk>/update/", users.UserUpdateView.as_view(), name="user-update"),
path("users/<int:pk>/delete/", users.UserDeleteView.as_view(), name="user-delete"), path("users/<int:pk>/delete/", users.UserDeleteView.as_view(), name="user-delete"),

View File

@ -8,46 +8,22 @@ from django.contrib.messages.views import SuccessMessageMixin
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.http.response import HttpResponseRedirect from django.http.response import HttpResponseRedirect
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse, reverse_lazy
from django.utils.http import urlencode from django.utils.http import urlencode
from django.utils.translation import gettext as _ 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 ( from guardian.mixins import (
PermissionListMixin,
PermissionRequiredMixin, PermissionRequiredMixin,
get_anonymous_user,
) )
from authentik.admin.forms.users import UserForm from authentik.admin.forms.users import UserForm
from authentik.admin.views.utils import ( from authentik.admin.views.utils import (
BackSuccessUrlMixin, BackSuccessUrlMixin,
DeleteMessageView, DeleteMessageView,
SearchListMixin,
UserPaginateListMixin,
) )
from authentik.core.models import Token, User from authentik.core.models import Token, User
from authentik.lib.views import CreateAssignPermView 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( class UserCreateView(
SuccessMessageMixin, SuccessMessageMixin,
BackSuccessUrlMixin, BackSuccessUrlMixin,
@ -62,7 +38,7 @@ class UserCreateView(
permission_required = "authentik_core.add_user" permission_required = "authentik_core.add_user"
template_name = "generic/create.html" template_name = "generic/create.html"
success_url = reverse_lazy("authentik_admin:users") success_url = "/"
success_message = _("Successfully created User") 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 # By default the object's name is user which is used by other checks
context_object_name = "object" context_object_name = "object"
template_name = "generic/update.html" template_name = "generic/update.html"
success_url = reverse_lazy("authentik_admin:users") success_url = "/"
success_message = _("Successfully updated User") 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 # By default the object's name is user which is used by other checks
context_object_name = "object" context_object_name = "object"
template_name = "generic/delete.html" template_name = "generic/delete.html"
success_url = reverse_lazy("authentik_admin:users") success_url = "/"
success_message = _("Successfully deleted User") 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 # By default the object's name is user which is used by other checks
context_object_name = "object" context_object_name = "object"
template_name = "administration/user/disable.html" template_name = "administration/user/disable.html"
success_url = reverse_lazy("authentik_admin:users") success_url = "/"
success_message = _("Successfully disabled User") success_message = _("Successfully disabled User")
def delete(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: 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 # By default the object's name is user which is used by other checks
context_object_name = "object" context_object_name = "object"
success_url = reverse_lazy("authentik_admin:users") success_url = "/"
success_message = _("Successfully enabled User") success_message = _("Successfully enabled User")
def get(self, request: HttpRequest, *args, **kwargs): def get(self, request: HttpRequest, *args, **kwargs):
@ -165,4 +141,4 @@ class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
messages.success( messages.success(
request, _("Password reset link: <pre>%(link)s</pre>" % {"link": link}) request, _("Password reset link: <pre>%(link)s</pre>" % {"link": link})
) )
return redirect("authentik_admin:users") return redirect("/")

View File

@ -28,7 +28,16 @@ class UserSerializer(ModelSerializer):
class Meta: class Meta:
model = User 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): class UserViewSet(ModelViewSet):

View File

@ -8156,6 +8156,16 @@ definitions:
description: User's display name. description: User's display name.
type: string type: string
minLength: 1 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: is_superuser:
title: Is superuser title: Is superuser
type: boolean type: boolean

View File

@ -1,4 +1,4 @@
import { DefaultClient, AKResponse } from "./Client"; import { DefaultClient, AKResponse, QueryArguments } from "./Client";
let _globalMePromise: Promise<User>; let _globalMePromise: Promise<User>;
@ -9,11 +9,25 @@ export class User {
is_superuser: boolean; is_superuser: boolean;
email: boolean; email: boolean;
avatar: string; avatar: string;
is_active: boolean;
last_login: number;
constructor() { constructor() {
throw Error(); throw Error();
} }
static get(pk: string): Promise<User> {
return DefaultClient.fetch<User>(["core", "users", pk]);
}
static list(filter?: QueryArguments): Promise<AKResponse<User>> {
return DefaultClient.fetch<AKResponse<User>>(["core", "users"], filter);
}
static adminUrl(rest: string): string {
return `/administration/users/${rest}`;
}
static me(): Promise<User> { static me(): Promise<User> {
if (!_globalMePromise) { if (!_globalMePromise) {
_globalMePromise = DefaultClient.fetch<User>(["core", "users", "me"]); _globalMePromise = DefaultClient.fetch<User>(["core", "users", "me"]);

View File

@ -70,18 +70,18 @@ export class SpinnerButton extends LitElement {
@click=${() => this.callAction()} @click=${() => this.callAction()}
> >
${this.isRunning ${this.isRunning
? html` <span class="pf-c-button__progress"> ? html` <span class="pf-c-button__progress">
<span <span
class="pf-c-spinner pf-m-md" class="pf-c-spinner pf-m-md"
role="progressbar" role="progressbar"
aria-valuetext="Loading..." aria-valuetext="Loading..."
> >
<span class="pf-c-spinner__clipper"></span> <span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span> <span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span> <span class="pf-c-spinner__tail-ball"></span>
</span> </span>
</span>` </span>`
: ""} : ""}
<slot></slot> <slot></slot>
</button>`; </button>`;
} }

View File

@ -47,7 +47,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
return User.me().then(u => u.is_superuser); return User.me().then(u => u.is_superuser);
}), }),
new SidebarItem("Identity & Cryptography").children( new SidebarItem("Identity & Cryptography").children(
new SidebarItem("User", "/administration/users/"), new SidebarItem("User", "/users"),
new SidebarItem("Groups", "/groups"), new SidebarItem("Groups", "/groups"),
new SidebarItem("Certificates", "/crypto/certificates"), new SidebarItem("Certificates", "/crypto/certificates"),
new SidebarItem("Tokens", "/administration/tokens/"), new SidebarItem("Tokens", "/administration/tokens/"),

View File

@ -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<User> {
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<AKResponse<User>> {
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`<div>
<div>${item.username}</div>
<small>${item.name}</small>
</div>`,
html`${item.is_active ? "Yes" : "No"}`,
html`${new Date(item.last_login * 1000).toLocaleString()}`,
html`
<ak-modal-button href="${User.adminUrl(`${item.pk}/update/`)}">
<ak-spinner-button slot="trigger" class="pf-m-secondary">
${gettext("Edit")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-dropdown class="pf-c-dropdown">
<button class="pf-c-dropdown__toggle pf-m-primary" type="button">
<span class="pf-c-dropdown__toggle-text">${gettext(item.is_active ? "Disable" : "Enable")}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
<li>
${item.is_active ?
html`<ak-modal-button href="${User.adminUrl(`${item.pk}/disable/`)}">
<button slot="trigger" class="pf-c-dropdown__menu-item">
${gettext("Disable")}
</button>
<div slot="modal"></div>
</ak-modal-button>`:
html`<ak-modal-button href="${User.adminUrl(`${item.pk}/enable/`)}">
<button slot="trigger" class="pf-c-dropdown__menu-item">
${gettext("Enable")}
</button>
<div slot="modal"></div>
</ak-modal-button>`}
</li>
<li class="pf-c-divider" role="separator"></li>
<li>
<ak-modal-button href="${User.adminUrl(`${item.pk}/delete/`)}">
<button slot="trigger" class="pf-c-dropdown__menu-item">
${gettext("Delete")}
</button>
<div slot="modal"></div>
</ak-modal-button>
</li>
</ul>
</ak-dropdown>
<a class="pf-c-button pf-m-tertiary" href="${User.adminUrl(`${item.pk}/reset/`)}">
${gettext("Reset Password")}
</a>
<a class="pf-c-button pf-m-tertiary" href="${`-/impersonation/${item.pk}/`}">
${gettext("Impersonate")}
</a>`,
];
}
renderToolbar(): TemplateResult {
return html`
<ak-modal-button href=${User.adminUrl("create/")}>
<ak-spinner-button slot="trigger" class="pf-m-primary">
${gettext("Create")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
${super.renderToolbar()}
`;
}
}

View File

@ -21,6 +21,7 @@ import "./pages/providers/ProviderViewPage";
import "./pages/sources/SourcesListPage"; import "./pages/sources/SourcesListPage";
import "./pages/sources/SourceViewPage"; import "./pages/sources/SourceViewPage";
import "./pages/groups/GroupListPage"; import "./pages/groups/GroupListPage";
import "./pages/users/UserListPage";
import "./pages/system-tasks/SystemTaskListPage"; import "./pages/system-tasks/SystemTaskListPage";
export const ROUTES: Route[] = [ export const ROUTES: Route[] = [
@ -44,6 +45,7 @@ export const ROUTES: Route[] = [
}), }),
new Route(new RegExp("^/policies$"), html`<ak-policy-list></ak-policy-list>`), new Route(new RegExp("^/policies$"), html`<ak-policy-list></ak-policy-list>`),
new Route(new RegExp("^/groups$"), html`<ak-group-list></ak-group-list>`), new Route(new RegExp("^/groups$"), html`<ak-group-list></ak-group-list>`),
new Route(new RegExp("^/users$"), html`<ak-user-list></ak-user-list>`),
new Route(new RegExp("^/flows$"), html`<ak-flow-list></ak-flow-list>`), new Route(new RegExp("^/flows$"), html`<ak-flow-list></ak-flow-list>`),
new Route(new RegExp(`^/flows/(?<slug>${SLUG_REGEX})$`)).then((args) => { new Route(new RegExp(`^/flows/(?<slug>${SLUG_REGEX})$`)).then((args) => {
return html`<ak-flow-view .flowSlug=${args.slug}></ak-flow-view>`; return html`<ak-flow-view .flowSlug=${args.slug}></ak-flow-view>`;