web/admin: migrate user forms to web
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
fac8d53163
commit
526af26536
|
@ -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,
|
|
||||||
}
|
|
|
@ -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/",
|
||||||
|
|
|
@ -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("/")
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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()}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;">
|
||||||
|
|
Reference in New Issue