diff --git a/authentik/admin/urls.py b/authentik/admin/urls.py index fa2233539..03ae5b98f 100644 --- a/authentik/admin/urls.py +++ b/authentik/admin/urls.py @@ -2,7 +2,6 @@ from django.urls import path from authentik.admin.views import ( - applications, flows, outposts, outposts_service_connections, @@ -19,17 +18,6 @@ from authentik.admin.views import ( from authentik.providers.saml.views.metadata import MetadataImportView urlpatterns = [ - # Applications - path( - "applications/create/", - applications.ApplicationCreateView.as_view(), - name="application-create", - ), - path( - "applications//update/", - applications.ApplicationUpdateView.as_view(), - name="application-update", - ), # Sources path("sources/create/", sources.SourceCreateView.as_view(), name="source-create"), path( diff --git a/authentik/admin/views/applications.py b/authentik/admin/views/applications.py deleted file mode 100644 index 450e2e2d9..000000000 --- a/authentik/admin/views/applications.py +++ /dev/null @@ -1,66 +0,0 @@ -"""authentik Application administration""" -from typing import Any - -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.utils.translation import gettext as _ -from django.views.generic import UpdateView -from guardian.mixins import PermissionRequiredMixin -from guardian.shortcuts import get_objects_for_user - -from authentik.core.forms.applications import ApplicationForm -from authentik.core.models import Application -from authentik.lib.views import CreateAssignPermView - - -class ApplicationCreateView( - SuccessMessageMixin, - LoginRequiredMixin, - DjangoPermissionRequiredMixin, - CreateAssignPermView, -): - """Create new Application""" - - model = Application - form_class = ApplicationForm - permission_required = "authentik_core.add_application" - - success_url = "/" - template_name = "generic/create.html" - success_message = _("Successfully created Application") - - def get_initial(self) -> dict[str, Any]: - if "provider" in self.request.GET: - try: - initial_provider_pk = int(self.request.GET["provider"]) - except ValueError: - return super().get_initial() - providers = ( - get_objects_for_user(self.request.user, "authentik_core.view_provider") - .filter(pk=initial_provider_pk) - .select_subclasses() - ) - if not providers.exists(): - return {} - return {"provider": providers.first()} - return super().get_initial() - - -class ApplicationUpdateView( - SuccessMessageMixin, - LoginRequiredMixin, - PermissionRequiredMixin, - UpdateView, -): - """Update application""" - - model = Application - form_class = ApplicationForm - permission_required = "authentik_core.change_application" - - success_url = "/" - template_name = "generic/update.html" - success_message = _("Successfully updated Application") diff --git a/authentik/core/api/applications.py b/authentik/core/api/applications.py index fd2e1f0c5..784497bd0 100644 --- a/authentik/core/api/applications.py +++ b/authentik/core/api/applications.py @@ -121,6 +121,7 @@ class ApplicationViewSet(ModelViewSet): required=True, ) ], + responses={200: "Success"}, ) @action(detail=True, methods=["POST"], parser_classes=(MultiPartParser,)) # pylint: disable=unused-argument @@ -132,12 +133,7 @@ class ApplicationViewSet(ModelViewSet): return HttpResponseBadRequest() app.meta_icon = icon app.save() - return Response( - get_events_per_1h( - action=EventAction.AUTHORIZE_APPLICATION, - context__authorized_application__pk=app.pk.hex, - ) - ) + return Response({}) @permission_required( "authentik_core.view_application", ["authentik_events.view_event"] diff --git a/authentik/core/forms/applications.py b/authentik/core/forms/applications.py deleted file mode 100644 index 94bbfdfb9..000000000 --- a/authentik/core/forms/applications.py +++ /dev/null @@ -1,50 +0,0 @@ -"""authentik Core Application forms""" -from django import forms -from django.utils.translation import gettext_lazy as _ - -from authentik.core.models import Application, Provider -from authentik.lib.widgets import GroupedModelChoiceField - - -class ApplicationForm(forms.ModelForm): - """Application Form""" - - def __init__(self, *args, **kwargs): # pragma: no cover - super().__init__(*args, **kwargs) - self.fields["provider"].queryset = ( - Provider.objects.all().order_by("name").select_subclasses() - ) - - class Meta: - - model = Application - fields = [ - "name", - "slug", - "provider", - "meta_launch_url", - "meta_icon", - "meta_description", - "meta_publisher", - ] - widgets = { - "name": forms.TextInput(), - "meta_launch_url": forms.TextInput(), - "meta_publisher": forms.TextInput(), - "meta_icon": forms.FileInput(), - } - help_texts = { - "meta_launch_url": _( - ( - "If left empty, authentik will try to extract the launch URL " - "based on the selected provider." - ) - ), - } - field_classes = {"provider": GroupedModelChoiceField} - labels = { - "meta_launch_url": _("Launch URL"), - "meta_icon": _("Icon"), - "meta_description": _("Description"), - "meta_publisher": _("Publisher"), - } diff --git a/swagger.yaml b/swagger.yaml index 9d6640710..67f33b81a 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -1340,10 +1340,8 @@ paths: required: true type: file responses: - '201': - description: '' - schema: - $ref: '#/definitions/Application' + '200': + description: Success '403': description: Authentication credentials were invalid, absent or insufficient. schema: diff --git a/web/src/api/legacy.ts b/web/src/api/legacy.ts index f9f2db98e..ae8462b14 100644 --- a/web/src/api/legacy.ts +++ b/web/src/api/legacy.ts @@ -1,9 +1,5 @@ export class AdminURLManager { - static applications(rest: string): string { - return `/administration/applications/${rest}`; - } - static policies(rest: string): string { return `/administration/policies/${rest}`; } diff --git a/web/src/elements/buttons/ModalButton.ts b/web/src/elements/buttons/ModalButton.ts index a19029d88..c76e10a96 100644 --- a/web/src/elements/buttons/ModalButton.ts +++ b/web/src/elements/buttons/ModalButton.ts @@ -54,11 +54,18 @@ export class ModalButton extends LitElement { super(); window.addEventListener("keyup", (e) => { if (e.code === "Escape") { + this.resetForms(); this.open = false; } }); } + resetForms(): void { + this.querySelectorAll("[slot=form]").forEach(form => { + form.reset(); + }); + } + updateHandlers(): void { // Ensure links close the modal this.shadowRoot?.querySelectorAll("a").forEach((a) => { diff --git a/web/src/elements/forms/Form.ts b/web/src/elements/forms/Form.ts index d4e267aed..c0fae4c54 100644 --- a/web/src/elements/forms/Form.ts +++ b/web/src/elements/forms/Form.ts @@ -43,6 +43,40 @@ export class Form extends LitElement { return this.successMessage; } + /** + * Reset the inner iron-form + */ + reset(): void { + const ironForm = this.shadowRoot?.querySelector("iron-form"); + if (!ironForm) { + return; + } + ironForm.reset(); + } + + /** + * If this form contains a file input, and the input as been filled, this function returns + * said file. + * @returns File object or undefined + */ + getFormFile(): File | undefined { + const ironForm = this.shadowRoot?.querySelector("iron-form"); + if (!ironForm) { + return; + } + const elements = ironForm._getSubmittableElements(); + for (let i = 0; i < elements.length; i++) { + const element = elements[i] as HTMLInputElement; + if (element.tagName.toLowerCase() === "input" && element.type === "file") { + if ((element.files || []).length < 1) { + continue; + } + // We already checked the length + return (element.files || [])[0]; + } + } + } + serializeForm(form: IronFormElement): T { const elements = form._getSubmittableElements(); const json: { [key: string]: unknown } = {}; diff --git a/web/src/elements/forms/ModalForm.ts b/web/src/elements/forms/ModalForm.ts index fd26b0f6a..b3d21bb5e 100644 --- a/web/src/elements/forms/ModalForm.ts +++ b/web/src/elements/forms/ModalForm.ts @@ -15,6 +15,7 @@ export class ModalForm extends ModalButton { } formPromise.then(() => { this.open = false; + form.reset(); this.dispatchEvent( new CustomEvent(EVENT_REFRESH, { bubbles: true, diff --git a/web/src/pages/applications/ApplicationForm.ts b/web/src/pages/applications/ApplicationForm.ts new file mode 100644 index 000000000..7806daa61 --- /dev/null +++ b/web/src/pages/applications/ApplicationForm.ts @@ -0,0 +1,124 @@ +import { CoreApi, Application, ProvidersApi, Provider } 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 { until } from "lit-html/directives/until"; +import { ifDefined } from "lit-html/directives/if-defined"; +import "../../elements/forms/HorizontalFormElement"; +import "../../elements/CodeMirror"; + +@customElement("ak-application-form") +export class ApplicationForm extends Form { + + @property({ attribute: false }) + application?: Application; + + @property({ attribute: false }) + provider?: number; + + getSuccessMessage(): string { + if (this.application) { + return gettext("Successfully updated application."); + } else { + return gettext("Successfully created application."); + } + } + + send = (data: Application): Promise => { + let writeOp: Promise; + if (this.application) { + writeOp = new CoreApi(DEFAULT_CONFIG).coreApplicationsUpdate({ + slug: this.application.slug, + data: data + }); + } else { + writeOp = new CoreApi(DEFAULT_CONFIG).coreApplicationsCreate({ + data: data + }); + } + const icon = this.getFormFile(); + if (icon) { + return writeOp.then(app => { + return new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIcon({ + slug: app.slug, + file: icon + }); + }); + } + return writeOp; + }; + + groupProviders(providers: Provider[]): TemplateResult { + const m = new Map(); + providers.forEach(p => { + if (!m.has(p.verboseName || "")) { + m.set(p.verboseName || "", []); + } + const tProviders = m.get(p.verboseName || "") || []; + tProviders.push(p); + }); + return html` + ${Array.from(m).map(([group, providers]) => { + return html` + ${providers.map(p => { + const selected = (this.application?.provider?.pk === p.pk) || (this.provider === p.pk) + return html``; + })} + `; + })} + `; + } + + renderForm(): TemplateResult { + return html`
+ + +

${gettext("Application's display Name.")}

+
+ + +

${gettext("Internal application name, used in URLs.")}

+
+ + + + + +

${gettext("If left empty, authentik will try to extract the launch URL based on the selected provider.")}

+
+ + + + + + + + + +
`; + } + +} diff --git a/web/src/pages/applications/ApplicationListPage.ts b/web/src/pages/applications/ApplicationListPage.ts index 6caa469fb..5d457b5c2 100644 --- a/web/src/pages/applications/ApplicationListPage.ts +++ b/web/src/pages/applications/ApplicationListPage.ts @@ -1,17 +1,17 @@ import { gettext } from "django"; import { css, CSSResult, customElement, html, property, TemplateResult } from "lit-element"; +import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css"; import { AKResponse } from "../../api/Client"; import { TablePage } from "../../elements/table/TablePage"; -import "../../elements/buttons/ModalButton"; +import "../../elements/forms/ModalForm"; import "../../elements/forms/DeleteForm"; import "../../elements/buttons/SpinnerButton"; import { TableColumn } from "../../elements/table/Table"; import { PAGE_SIZE } from "../../constants"; import { Application, CoreApi } from "authentik-api"; import { DEFAULT_CONFIG } from "../../api/Config"; -import { AdminURLManager } from "../../api/legacy"; -import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css"; +import "./ApplicationForm"; @customElement("ak-application-list") export class ApplicationListPage extends TablePage { @@ -74,15 +74,22 @@ export class ApplicationListPage extends TablePage { ${item.metaPublisher ? html`${item.metaPublisher}` : html``} `, html`${item.slug}`, - html`${item.provider?.name}`, - html`${item.provider?.verboseName}`, + html`${item.provider?.name || "-"}`, + html`${item.provider?.verboseName || "-"}`, html` - - + + + ${gettext("Update")} + + + ${gettext("Update Application")} + + + + + { renderToolbar(): TemplateResult { return html` - - + + ${gettext("Create")} - -
-
+ + + ${gettext("Create Application")} + + + + + ${super.renderToolbar()} `; } diff --git a/web/src/pages/events/RuleListPage.ts b/web/src/pages/events/RuleListPage.ts index e3f00d248..15cb27f4b 100644 --- a/web/src/pages/events/RuleListPage.ts +++ b/web/src/pages/events/RuleListPage.ts @@ -10,7 +10,6 @@ import { TableColumn } from "../../elements/table/Table"; import { PAGE_SIZE } from "../../constants"; import { EventsApi, NotificationRule } from "authentik-api"; import { DEFAULT_CONFIG } from "../../api/Config"; -import { AdminURLManager } from "../../api/legacy"; import "../../elements/forms/DeleteForm"; import "./RuleForm"; diff --git a/web/src/pages/providers/RelatedApplicationButton.ts b/web/src/pages/providers/RelatedApplicationButton.ts index 77f1dd1d7..17733380d 100644 --- a/web/src/pages/providers/RelatedApplicationButton.ts +++ b/web/src/pages/providers/RelatedApplicationButton.ts @@ -1,14 +1,21 @@ import { gettext } from "django"; -import { customElement, html, LitElement, property, TemplateResult } from "lit-element"; +import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; import { Provider } from "authentik-api"; -import { AdminURLManager } from "../../api/legacy"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; +import PFButton from "@patternfly/patternfly/components/Button/button.css"; import "../../elements/buttons/ModalButton"; import "../../elements/Spinner"; +import "../../elements/forms/ModalForm"; +import "../../pages/applications/ApplicationForm"; @customElement("ak-provider-related-application") export class RelatedApplicationButton extends LitElement { + static get styles(): CSSResult[] { + return [PFBase, PFButton]; + } + @property({attribute: false}) provider?: Provider; @@ -18,12 +25,19 @@ export class RelatedApplicationButton extends LitElement { ${this.provider.assignedApplicationName} `; } - return html` - - ${gettext("Create")} - -
-
`; + return html` + + ${gettext("Create")} + + + ${gettext("Create Application")} + + + + + `; } }