web/admin: migrate user forms to web

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-03-29 16:16:27 +02:00
parent fac8d53163
commit 526af26536
11 changed files with 145 additions and 144 deletions

View File

@ -1,22 +0,0 @@
"""authentik administrative user forms"""
from django import forms
from authentik.admin.fields import CodeMirrorWidget, YAMLField
from authentik.core.models import User
class UserForm(forms.ModelForm):
"""Update User Details"""
class Meta:
model = User
fields = ["username", "name", "email", "is_active", "attributes"]
widgets = {
"name": forms.TextInput,
"attributes": CodeMirrorWidget,
}
field_classes = {
"attributes": YAMLField,
}

View File

@ -18,7 +18,6 @@ from authentik.admin.views import (
stages_bindings, stages_bindings,
stages_invitations, stages_invitations,
stages_prompts, stages_prompts,
users,
) )
from authentik.providers.saml.views.metadata import MetadataImportView from authentik.providers.saml.views.metadata import MetadataImportView
@ -152,14 +151,6 @@ urlpatterns = [
property_mappings.PropertyMappingTestView.as_view(), property_mappings.PropertyMappingTestView.as_view(),
name="property-mapping-test", name="property-mapping-test",
), ),
# Users
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>/reset/",
users.UserPasswordResetView.as_view(),
name="user-password-reset",
),
# Certificate-Key Pairs # Certificate-Key Pairs
path( path(
"crypto/certificates/create/", "crypto/certificates/create/",

View File

@ -1,74 +0,0 @@
"""authentik User administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.utils.http import urlencode
from django.utils.translation import gettext as _
from django.views.generic import DetailView, UpdateView
from guardian.mixins import PermissionRequiredMixin
from authentik.admin.forms.users import UserForm
from authentik.core.models import Token, User
from authentik.lib.views import CreateAssignPermView
class UserCreateView(
SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
):
"""Create user"""
model = User
form_class = UserForm
permission_required = "authentik_core.add_user"
template_name = "generic/create.html"
success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully created User")
class UserUpdateView(
SuccessMessageMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
UpdateView,
):
"""Update user"""
model = User
form_class = UserForm
permission_required = "authentik_core.change_user"
# 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_core:if-admin")
success_message = _("Successfully updated User")
class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""Get Password reset link for user"""
model = User
permission_required = "authentik_core.reset_user_password"
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Create token for user and return link"""
super().get(request, *args, **kwargs)
token, __ = Token.objects.get_or_create(
identifier="password-reset-temp", user=self.object
)
querystring = urlencode({"token": token.key})
link = request.build_absolute_uri(
reverse_lazy("authentik_flows:default-recovery") + f"?{querystring}"
)
messages.success(request, _("Password reset link: %(link)s" % {"link": link}))
return redirect("/")

View File

@ -1,11 +1,8 @@
"""authentik admin util views""" """authentik admin util views"""
from typing import Any from typing import Any
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404 from django.http import Http404
from django.urls import reverse_lazy from django.views.generic import UpdateView
from django.views.generic import DeleteView, UpdateView
from authentik.lib.utils.reflection import all_subclasses from authentik.lib.utils.reflection import all_subclasses
from authentik.lib.views import CreateAssignPermView from authentik.lib.views import CreateAssignPermView

View File

@ -68,10 +68,6 @@ export class AdminURLManager {
return `/administration/events/transports/${rest}`; return `/administration/events/transports/${rest}`;
} }
static users(rest: string): string {
return `/administration/users/${rest}`;
}
} }
export class UserURLManager { export class UserURLManager {

View File

@ -23,8 +23,7 @@ export class ActionButton extends SpinnerButton {
this.setLoading(); this.setLoading();
this.apiRequest().then(() => { this.apiRequest().then(() => {
this.setDone(SUCCESS_CLASS); this.setDone(SUCCESS_CLASS);
}) }).catch((e: Error | Response) => {
.catch((e: Error | Response) => {
if (e instanceof Error) { if (e instanceof Error) {
showMessage({ showMessage({
level: MessageLevel.error, level: MessageLevel.error,

View File

@ -18,9 +18,9 @@ export class GroupForm extends Form<Group> {
getSuccessMessage(): string { getSuccessMessage(): string {
if (this.group) { if (this.group) {
return gettext("Successfully updated group"); return gettext("Successfully updated group.");
} else { } else {
return gettext("Successfully created group"); return gettext("Successfully created group.");
} }
} }

View File

@ -66,7 +66,7 @@ export class GroupListPage extends TablePage<Group> {
</span> </span>
<ak-group-form slot="form" .group=${item}> <ak-group-form slot="form" .group=${item}>
</ak-group-form> </ak-group-form>
<button slot="trigger" class="pf-c-button pf-m-primary"> <button slot="trigger" class="pf-c-button pf-m-secondary">
${gettext("Edit")} ${gettext("Edit")}
</button> </button>
</ak-forms-modal> </ak-forms-modal>

View File

@ -0,0 +1,68 @@
import { CoreApi, User } from "authentik-api";
import { gettext } from "django";
import { customElement, property } from "lit-element";
import { html, TemplateResult } from "lit-html";
import { DEFAULT_CONFIG } from "../../api/Config";
import { Form } from "../../elements/forms/Form";
import { ifDefined } from "lit-html/directives/if-defined";
import "../../elements/forms/HorizontalFormElement";
import "../../elements/CodeMirror";
import YAML from "yaml";
@customElement("ak-user-form")
export class UserForm extends Form<User> {
@property({ attribute: false })
user?: User;
getSuccessMessage(): string {
if (this.user) {
return gettext("Successfully updated user.");
} else {
return gettext("Successfully created user.");
}
}
send = (data: User): Promise<User> => {
if (this.user) {
return new CoreApi(DEFAULT_CONFIG).coreUsersUpdate({
id: this.user.pk || 0,
data: data
});
} else {
return new CoreApi(DEFAULT_CONFIG).coreUsersCreate({
data: data
});
}
};
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${gettext("Username")} ?required=${true}>
<input type="text" name="username" value="${ifDefined(this.user?.username)}" class="pf-c-form-control" required="">
<p class="pf-c-form__helper-text">${gettext("Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${gettext("Name")} ?required=${true}>
<input type="text" name="name" value="${ifDefined(this.user?.name)}" class="pf-c-form-control" required="">
<p class="pf-c-form__helper-text">${gettext("User's display name.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${gettext("Email")} ?required=${true}>
<input type="email" name="email" autocomplete="off" value="${ifDefined(this.user?.email)}" class="pf-c-form-control" required="">
</ak-form-element-horizontal>
<ak-form-element-horizontal>
<div class="pf-c-check">
<input type="checkbox" name="is_active" class="pf-c-check__input" ?checked=${this.user?.isActive || false}>
<label class="pf-c-check__label">
${gettext("Is active")}
</label>
</div>
<p class="pf-c-form__helper-text">${gettext("Designates whether this user should be treated as active. Unselect this instead of deleting accounts.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${gettext("Attributes")}>
<ak-codemirror mode="yaml" name="attributes" value="${YAML.stringify(this.user?.attributes)}">
</ak-codemirror>
</ak-form-element-horizontal>
</form>`;
}
}

View File

@ -3,16 +3,18 @@ import { customElement, html, property, TemplateResult } from "lit-element";
import { AKResponse } from "../../api/Client"; import { AKResponse } from "../../api/Client";
import { TablePage } from "../../elements/table/TablePage"; import { TablePage } from "../../elements/table/TablePage";
import "../../elements/buttons/ModalButton"; import "../../elements/forms/ModalForm";
import "../../elements/buttons/Dropdown"; import "../../elements/buttons/Dropdown";
import "../../elements/buttons/ActionButton"; import "../../elements/buttons/ActionButton";
import { TableColumn } from "../../elements/table/Table"; import { TableColumn } from "../../elements/table/Table";
import { PAGE_SIZE } from "../../constants"; import { PAGE_SIZE } from "../../constants";
import { CoreApi, User } from "authentik-api"; import { CoreApi, User } from "authentik-api";
import { DEFAULT_CONFIG } from "../../api/Config"; import { DEFAULT_CONFIG } from "../../api/Config";
import { AdminURLManager } from "../../api/legacy";
import "../../elements/forms/DeleteForm"; import "../../elements/forms/DeleteForm";
import "./UserActiveForm"; import "./UserActiveForm";
import "./UserForm";
import { showMessage } from "../../elements/messages/MessageContainer";
import { MessageLevel } from "../../elements/messages/Message";
@customElement("ak-user-list") @customElement("ak-user-list")
export class UserListPage extends TablePage<User> { export class UserListPage extends TablePage<User> {
@ -59,12 +61,19 @@ export class UserListPage extends TablePage<User> {
html`${item.isActive ? "Yes" : "No"}`, html`${item.isActive ? "Yes" : "No"}`,
html`${item.lastLogin?.toLocaleString()}`, html`${item.lastLogin?.toLocaleString()}`,
html` html`
<ak-modal-button href="${AdminURLManager.users(`${item.pk}/update/`)}"> <ak-forms-modal>
<ak-spinner-button slot="trigger" class="pf-m-secondary"> <span slot="submit">
${gettext("Update")}
</span>
<span slot="header">
${gettext("Update User")}
</span>
<ak-user-form slot="form" .user=${item}>
</ak-user-form>
<button slot="trigger" class="pf-m-secondary pf-c-button">
${gettext("Edit")} ${gettext("Edit")}
</ak-spinner-button> </button>
<div slot="modal"></div> </ak-forms-modal>
</ak-modal-button>
<ak-dropdown class="pf-c-dropdown"> <ak-dropdown class="pf-c-dropdown">
<button class="pf-c-dropdown__toggle pf-m-primary" type="button"> <button class="pf-c-dropdown__toggle pf-m-primary" type="button">
<span class="pf-c-dropdown__toggle-text">${gettext(item.isActive ? "Disable" : "Enable")}</span> <span class="pf-c-dropdown__toggle-text">${gettext(item.isActive ? "Disable" : "Enable")}</span>
@ -107,7 +116,18 @@ export class UserListPage extends TablePage<User> {
</li> </li>
</ul> </ul>
</ak-dropdown> </ak-dropdown>
<ak-action-button method="GET" url="${AdminURLManager.users(`${item.pk}/reset/`)}"> <ak-action-button
.apiRequest=${() => {
return new CoreApi(DEFAULT_CONFIG).coreUsersRecovery({
id: item.pk || 0,
}).then(rec => {
showMessage({
level: MessageLevel.success,
message: gettext("Successfully generated recovery link"),
description: rec.link
});
});
}}>
${gettext("Reset Password")} ${gettext("Reset Password")}
</ak-action-button> </ak-action-button>
<a class="pf-c-button pf-m-tertiary" href="${`/-/impersonation/${item.pk}/`}"> <a class="pf-c-button pf-m-tertiary" href="${`/-/impersonation/${item.pk}/`}">
@ -118,13 +138,21 @@ export class UserListPage extends TablePage<User> {
renderToolbar(): TemplateResult { renderToolbar(): TemplateResult {
return html` return html`
<ak-modal-button href=${AdminURLManager.users("create/")}> <ak-forms-modal>
<ak-spinner-button slot="trigger" class="pf-m-primary"> <span slot="submit">
${gettext("Create")} ${gettext("Create")}
</ak-spinner-button> </span>
<div slot="modal"></div> <span slot="header">
</ak-modal-button> ${gettext("Create User")}
</span>
<ak-user-form slot="form">
</ak-user-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${gettext("Create")}
</button>
</ak-forms-modal>
${super.renderToolbar()} ${super.renderToolbar()}
`; `;
} }
} }

View File

@ -12,7 +12,9 @@ import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import AKGlobal from "../../authentik.css"; import AKGlobal from "../../authentik.css";
import "../../elements/buttons/ModalButton"; import "../../elements/forms/ModalForm";
import "./UserForm";
import "../../elements/buttons/ActionButton";
import "../../elements/buttons/SpinnerButton"; import "../../elements/buttons/SpinnerButton";
import "../../elements/CodeMirror"; import "../../elements/CodeMirror";
import "../../elements/Tabs"; import "../../elements/Tabs";
@ -24,8 +26,9 @@ import "../../elements/charts/UserChart";
import { Page } from "../../elements/Page"; import { Page } from "../../elements/Page";
import { CoreApi, User } from "authentik-api"; import { CoreApi, User } from "authentik-api";
import { DEFAULT_CONFIG } from "../../api/Config"; import { DEFAULT_CONFIG } from "../../api/Config";
import { AdminURLManager } from "../../api/legacy";
import { EVENT_REFRESH } from "../../constants"; import { EVENT_REFRESH } from "../../constants";
import { showMessage } from "../../elements/messages/MessageContainer";
import { MessageLevel } from "../../elements/messages/Message";
@customElement("ak-user-view") @customElement("ak-user-view")
export class UserViewPage extends Page { export class UserViewPage extends Page {
@ -131,20 +134,35 @@ export class UserViewPage extends Page {
</dl> </dl>
</div> </div>
<div class="pf-c-card__footer"> <div class="pf-c-card__footer">
<ak-modal-button href="${AdminURLManager.users(`${this.user.pk}/update/`)}"> <ak-forms-modal>
<ak-spinner-button slot="trigger" class="pf-m-primary"> <span slot="submit">
${gettext("Update")}
</span>
<span slot="header">
${gettext("Update User")}
</span>
<ak-user-form slot="form" .user=${this.user}>
</ak-user-form>
<button slot="trigger" class="pf-m-primary pf-c-button">
${gettext("Edit")} ${gettext("Edit")}
</ak-spinner-button> </button>
<div slot="modal"></div> </ak-forms-modal>
</ak-modal-button>
</div> </div>
<div class="pf-c-card__footer"> <div class="pf-c-card__footer">
<ak-modal-button href="${AdminURLManager.users(`${this.user.pk}/reset/`)}"> <ak-action-button
<ak-spinner-button slot="trigger" class="pf-m-secondary"> .apiRequest=${() => {
return new CoreApi(DEFAULT_CONFIG).coreUsersRecovery({
id: this.user?.pk || 0,
}).then(rec => {
showMessage({
level: MessageLevel.success,
message: gettext("Successfully generated recovery link"),
description: rec.link
});
});
}}>
${gettext("Reset Password")} ${gettext("Reset Password")}
</ak-spinner-button> </ak-action-button>
<div slot="modal"></div>
</ak-modal-button>
</div> </div>
</div> </div>
<div class="pf-c-card pf-l-gallery__item pf-m-4-col" style="grid-column-end: span 4;grid-row-end: span 2;"> <div class="pf-c-card pf-l-gallery__item pf-m-4-col" style="grid-column-end: span 4;grid-row-end: span 2;">