sources/authenticator_webauthn: rewrite to webcomponent
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		| @ -94,7 +94,7 @@ class SourceViewSet( | ||||
|             if not policy_engine.passing: | ||||
|                 continue | ||||
|             source_settings = source.ui_user_settings | ||||
|             source_settings["object_uid"] = str(source.pk) | ||||
|             source_settings.initial_data["object_uid"] = str(source.pk) | ||||
|             if not source_settings.is_valid(): | ||||
|                 LOGGER.warning(source_settings.errors) | ||||
|             matching_sources.append(source_settings.validated_data) | ||||
|  | ||||
| @ -1,17 +1,16 @@ | ||||
| """WebAuthn stage""" | ||||
| from authentik.core.types import UserSettingSerializer | ||||
| from typing import Optional, Type | ||||
|  | ||||
| from django.contrib.auth import get_user_model | ||||
| from django.db import models | ||||
| from django.forms import ModelForm | ||||
| from django.urls import reverse | ||||
| from django.utils.timezone import now | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from django.views import View | ||||
| from django_otp.models import Device | ||||
| from rest_framework.serializers import BaseSerializer | ||||
|  | ||||
| from authentik.flows.challenge import Challenge, ChallengeTypes | ||||
| from authentik.flows.models import ConfigurableStage, Stage | ||||
|  | ||||
|  | ||||
| @ -43,15 +42,11 @@ class AuthenticateWebAuthnStage(ConfigurableStage, Stage): | ||||
|         return AuthenticateWebAuthnStageForm | ||||
|  | ||||
|     @property | ||||
|     def ui_user_settings(self) -> Optional[Challenge]: | ||||
|         return Challenge( | ||||
|     def ui_user_settings(self) -> Optional[UserSettingSerializer]: | ||||
|         return UserSettingSerializer( | ||||
|             data={ | ||||
|                 "type": ChallengeTypes.shell.value, | ||||
|                 "title": str(self._meta.verbose_name), | ||||
|                 "component": reverse( | ||||
|                     "authentik_stages_authenticator_webauthn:user-settings", | ||||
|                     kwargs={"stage_uuid": self.stage_uuid}, | ||||
|                 ), | ||||
|                 "component": "ak-user-settings-authenticator-webauthn", | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @ -1,47 +0,0 @@ | ||||
| {% load i18n %} | ||||
| {% load humanize %} | ||||
|  | ||||
| <div class="pf-c-card"> | ||||
|     <div class="pf-c-card__title"> | ||||
|         {% trans "WebAuthn Devices" %} | ||||
|     </div> | ||||
|     <div class="pf-c-card__body"> | ||||
|         <ul class="pf-c-data-list" role="list"> | ||||
|             {% for device in devices %} | ||||
|             <li class="pf-c-data-list__item" aria-labelledby="data-list-basic-item-1"> | ||||
|                 <div class="pf-c-data-list__item-row"> | ||||
|                     <div class="pf-c-data-list__item-content"> | ||||
|                         <div class="pf-c-data-list__cell">{{ device.name|default:"-" }}</div> | ||||
|                         <div class="pf-c-data-list__cell"> | ||||
|                             {% blocktrans with created_on=device.created_on|naturaltime %} | ||||
|                                 Created {{ created_on }} | ||||
|                             {% endblocktrans %} | ||||
|                         </div> | ||||
|                         <div class="pf-c-data-list__cell"> | ||||
|                             <ak-modal-button href="{% url 'authentik_stages_authenticator_webauthn:device-update' pk=device.pk %}"> | ||||
|                                 <ak-spinner-button slot="trigger" class="pf-m-primary"> | ||||
|                                     {% trans 'Update' %} | ||||
|                                 </ak-spinner-button> | ||||
|                                 <div slot="modal"></div> | ||||
|                             </ak-modal-button> | ||||
|                             <ak-modal-button href="{% url 'authentik_stages_authenticator_webauthn:device-delete' pk=device.pk %}"> | ||||
|                                 <ak-spinner-button slot="trigger" class="pf-m-danger"> | ||||
|                                     {% trans 'Delete' %} | ||||
|                                 </ak-spinner-button> | ||||
|                                 <div slot="modal"></div> | ||||
|                             </ak-modal-button> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </li> | ||||
|             {% endfor %} | ||||
|         </ul> | ||||
|     </div> | ||||
|     <div class="pf-c-card__footer"> | ||||
|         {% if stage.configure_flow %} | ||||
|         <a href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next=/%23user" | ||||
|             class="ak-root-link pf-c-button pf-m-primary">{% trans "Configure WebAuthn" %} | ||||
|         </a> | ||||
|         {% endif %} | ||||
|     </div> | ||||
| </div> | ||||
| @ -2,15 +2,9 @@ | ||||
| from django.urls import path | ||||
|  | ||||
| from authentik.stages.authenticator_webauthn.views import ( | ||||
|     DeviceDeleteView, | ||||
|     DeviceUpdateView, | ||||
|     UserSettingsView, | ||||
| ) | ||||
|  | ||||
| urlpatterns = [ | ||||
|     path( | ||||
|         "<uuid:stage_uuid>/settings/", UserSettingsView.as_view(), name="user-settings" | ||||
|     ), | ||||
|     path("devices/<int:pk>/delete/", DeviceDeleteView.as_view(), name="device-delete"), | ||||
|     path("devices/<int:pk>/update/", DeviceUpdateView.as_view(), name="device-update"), | ||||
| ] | ||||
|  | ||||
| @ -2,33 +2,14 @@ | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||||
| from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.http.response import Http404 | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views.generic import TemplateView, UpdateView | ||||
| from django.views.generic import UpdateView | ||||
|  | ||||
| from authentik.admin.views.utils import DeleteMessageView | ||||
| from authentik.stages.authenticator_webauthn.forms import DeviceEditForm | ||||
| from authentik.stages.authenticator_webauthn.models import ( | ||||
|     AuthenticateWebAuthnStage, | ||||
|     WebAuthnDevice, | ||||
| ) | ||||
|  | ||||
|  | ||||
| class UserSettingsView(LoginRequiredMixin, TemplateView): | ||||
|     """View for user settings to control WebAuthn devices""" | ||||
|  | ||||
|     template_name = "stages/authenticator_webauthn/user_settings.html" | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         kwargs = super().get_context_data(**kwargs) | ||||
|         kwargs["devices"] = WebAuthnDevice.objects.filter(user=self.request.user) | ||||
|         stage = get_object_or_404( | ||||
|             AuthenticateWebAuthnStage, pk=self.kwargs["stage_uuid"] | ||||
|         ) | ||||
|         kwargs["stage"] = stage | ||||
|         return kwargs | ||||
|  | ||||
|  | ||||
| class DeviceUpdateView(SuccessMessageMixin, LoginRequiredMixin, UpdateView): | ||||
|     """Update device""" | ||||
|  | ||||
| @ -43,18 +24,3 @@ class DeviceUpdateView(SuccessMessageMixin, LoginRequiredMixin, UpdateView): | ||||
|         if device.user != self.request.user: | ||||
|             raise Http404 | ||||
|         return device | ||||
|  | ||||
|  | ||||
| class DeviceDeleteView(LoginRequiredMixin, DeleteMessageView): | ||||
|     """Delete device""" | ||||
|  | ||||
|     model = WebAuthnDevice | ||||
|     template_name = "generic/delete.html" | ||||
|     success_url = "/" | ||||
|     success_message = _("Successfully deleted Device") | ||||
|  | ||||
|     def get_object(self) -> WebAuthnDevice: | ||||
|         device: WebAuthnDevice = super().get_object() | ||||
|         if device.user != self.request.user: | ||||
|             raise Http404 | ||||
|         return device | ||||
|  | ||||
| @ -83,6 +83,10 @@ export class UserURLManager { | ||||
|         return `/-/user/tokens/${rest}`; | ||||
|     } | ||||
|  | ||||
|     static authenticatorWebauthn(rest: string): string { | ||||
|         return `/-/user/authenticator/webauthn/${rest}`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| export class AppURLManager { | ||||
| @ -95,3 +99,11 @@ export class AppURLManager { | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| export class FlowURLManager { | ||||
|  | ||||
|     static configure(stageUuid: string, rest: string): string { | ||||
|         return `-/configure/${stageUuid}/${rest}`; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -13,13 +13,14 @@ import AKGlobal from "../../authentik.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||
| import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; | ||||
| import { SourcesApi, StagesApi } from "authentik-api"; | ||||
| import { SourcesApi, StagesApi, UserSetting } from "authentik-api"; | ||||
| import { DEFAULT_CONFIG } from "../../api/Config"; | ||||
| import { until } from "lit-html/directives/until"; | ||||
| import { ifDefined } from "lit-html/directives/if-defined"; | ||||
| import "../../elements/Tabs"; | ||||
| import "../tokens/UserTokenList"; | ||||
| import "../generic/SiteShell"; | ||||
| import { ifDefined } from "lit-html/directives/if-defined"; | ||||
| import "./settings/AuthenticatorWebAuthnDevices"; | ||||
|  | ||||
| @customElement("ak-user-settings") | ||||
| export class UserSettingsPage extends LitElement { | ||||
| @ -28,6 +29,22 @@ export class UserSettingsPage extends LitElement { | ||||
|         return [PFBase, PFPage, PFFlex, PFDisplay, PFGallery, PFContent, PFCard, PFDescriptionList, PFSizing, PFForm, PFFormControl, AKGlobal]; | ||||
|     } | ||||
|  | ||||
|     renderStageSettings(stage: UserSetting): TemplateResult { | ||||
|         switch (stage.component) { | ||||
|             case "ak-user-settings-authenticator-webauthn": | ||||
|                 return html`<ak-user-settings-authenticator-webauthn stageId=${stage.objectUid}> | ||||
|                 </ak-user-settings-authenticator-webauthn>`; | ||||
|             default: | ||||
|                 return html`<div class="pf-u-display-flex pf-u-justify-content-center"> | ||||
|                     <div class="pf-u-w-75"> | ||||
|                         <ak-site-shell url="${ifDefined(stage.component)}"> | ||||
|                             <div slot="body"></div> | ||||
|                         </ak-site-shell> | ||||
|                     </div> | ||||
|                 </div>`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<div class="pf-c-page"> | ||||
|             <main role="main" class="pf-c-page__main" tabindex="-1"> | ||||
| @ -55,22 +72,15 @@ export class UserSettingsPage extends LitElement { | ||||
|                     </section> | ||||
|                     ${until(new StagesApi(DEFAULT_CONFIG).stagesAllUserSettings({}).then((stages) => { | ||||
|                         return stages.map((stage) => { | ||||
|                             // TODO: Check for non-shell stages | ||||
|                             return html`<section slot="page-${stage.title}" data-tab-title="${ifDefined(stage.title)}" class="pf-c-page__main-section pf-m-no-padding-mobile"> | ||||
|                                     <div class="pf-u-display-flex pf-u-justify-content-center"> | ||||
|                                         <div class="pf-u-w-75"> | ||||
|                                             <ak-site-shell url="${ifDefined(stage.component)}"> | ||||
|                                                 <div slot="body"></div> | ||||
|                                             </ak-site-shell> | ||||
|                                         </div> | ||||
|                                     </div> | ||||
|                             return html`<section slot="page-${stage.objectUid}" data-tab-title="${ifDefined(stage.title)}" class="pf-c-page__main-section pf-m-no-padding-mobile"> | ||||
|                                 ${this.renderStageSettings(stage)} | ||||
|                             </section>`; | ||||
|                         }); | ||||
|                     }))} | ||||
|                     ${until(new SourcesApi(DEFAULT_CONFIG).sourcesAllUserSettings({}).then((sources) => { | ||||
|                         return sources.map((source) => { | ||||
|                             // TODO: Check for non-shell sources | ||||
|                             return html`<section slot="page-${source.title}" data-tab-title="${ifDefined(source.title)}" class="pf-c-page__main-section pf-m-no-padding-mobile"> | ||||
|                             return html`<section slot="page-${source.objectUid}" data-tab-title="${ifDefined(source.title)}" class="pf-c-page__main-section pf-m-no-padding-mobile"> | ||||
|                                     <div class="pf-u-display-flex pf-u-justify-content-center"> | ||||
|                                         <div class="pf-u-w-75"> | ||||
|                                             <ak-site-shell url="${ifDefined(source.component)}"> | ||||
|  | ||||
							
								
								
									
										79
									
								
								web/src/pages/users/settings/AuthenticatorWebAuthnDevices.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								web/src/pages/users/settings/AuthenticatorWebAuthnDevices.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,79 @@ | ||||
| import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
| import PFCard from "@patternfly/patternfly/components/Card/card.css"; | ||||
| import PFDataList from "@patternfly/patternfly/components/DataList/data-list.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import AKGlobal from "../../../authentik.css"; | ||||
| import { gettext } from "django"; | ||||
| import { AuthenticatorsApi, StagesApi } from "authentik-api"; | ||||
| import { until } from "lit-html/directives/until"; | ||||
| import { FlowURLManager, UserURLManager } from "../../../api/legacy"; | ||||
| import { DEFAULT_CONFIG } from "../../../api/Config"; | ||||
|  | ||||
| @customElement("ak-user-settings-authenticator-webauthn") | ||||
| export class UserSettingsAuthenticatorWebAuthnDevices extends LitElement { | ||||
|  | ||||
|     @property() | ||||
|     stageId!: string; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFCard, PFButton, PFDataList, AKGlobal]; | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<div class="pf-c-card"> | ||||
|             <div class="pf-c-card__title"> | ||||
|                 ${gettext("WebAuthn Devices")} | ||||
|             </div> | ||||
|             <div class="pf-c-card__body"> | ||||
|                 <ul class="pf-c-data-list" role="list"> | ||||
|                     ${until(new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsWebauthnList({}).then((devices) => { | ||||
|                         return devices.results.map((device) => { | ||||
|                             return html`<li class="pf-c-data-list__item"> | ||||
|                                 <div class="pf-c-data-list__item-row"> | ||||
|                                     <div class="pf-c-data-list__item-content"> | ||||
|                                         <div class="pf-c-data-list__cell">${device.name || "-"}</div> | ||||
|                                         <div class="pf-c-data-list__cell"> | ||||
|                                             ${gettext(`Created ${device.createdOn?.toLocaleString()}`)} | ||||
|                                         </div> | ||||
|                                         <div class="pf-c-data-list__cell"> | ||||
|                                             <ak-modal-button href="${UserURLManager.authenticatorWebauthn(`devices/${device.pk}/update/`)}"> | ||||
|                                                 <ak-spinner-button slot="trigger" class="pf-m-primary"> | ||||
|                                                     ${gettext('Update')} | ||||
|                                                 </ak-spinner-button> | ||||
|                                                 <div slot="modal"></div> | ||||
|                                             </ak-modal-button> | ||||
|                                             <ak-forms-delete | ||||
|                                                 .obj=${device} | ||||
|                                                 objectLabel=${gettext("Authenticator")} | ||||
|                                                 .delete=${() => { | ||||
|                                                     return new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsWebauthnDelete({ | ||||
|                                                         id: device.pk || 0 | ||||
|                                                     }); | ||||
|                                                 }}> | ||||
|                                                 <button slot="trigger" class="pf-c-dropdown__menu-item"> | ||||
|                                                     ${gettext("Delete")} | ||||
|                                                 </button> | ||||
|                                             </ak-forms-delete> | ||||
|                                         </div> | ||||
|                                     </div> | ||||
|                                 </div> | ||||
|                             </li>`; | ||||
|                         }); | ||||
|                     }))} | ||||
|                 </ul> | ||||
|             </div> | ||||
|             <div class="pf-c-card__footer"> | ||||
|                 ${until(new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorWebauthnRead({stageUuid: this.stageId}).then((stage) => { | ||||
|                     if (stage.configureFlow) { | ||||
|                         return html`<a href="${FlowURLManager.configure(stage.pk || "", '?next=/%23user')}" | ||||
|                                 class="pf-c-button pf-m-primary">${gettext("Configure WebAuthn")} | ||||
|                             </a>`; | ||||
|                     } | ||||
|                     return html``; | ||||
|                 }))} | ||||
|             </div> | ||||
|         </div>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer