sources: add custom icon support (#4022)

* add source icon

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add to oauth form

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add to other browser sources

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add migration, return icon in UI challenges

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* deduplicate file upload

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens L 2022-11-16 14:10:10 +01:00 committed by GitHub
parent 1e15d1f538
commit 9f5fb692ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 527 additions and 123 deletions

View File

@ -23,10 +23,15 @@ from authentik.admin.api.metrics import CoordinateSerializer
from authentik.api.decorators import permission_required
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import FilePathSerializer, FileUploadSerializer
from authentik.core.models import Application, User
from authentik.events.models import EventAction
from authentik.events.utils import sanitize_dict
from authentik.lib.utils.file import (
FilePathSerializer,
FileUploadSerializer,
set_file,
set_file_url,
)
from authentik.policies.api.exec import PolicyTestResultSerializer
from authentik.policies.engine import PolicyEngine
from authentik.policies.types import PolicyResult
@ -224,21 +229,7 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
def set_icon(self, request: Request, slug: str):
"""Set application icon"""
app: Application = self.get_object()
icon = request.FILES.get("file", None)
clear = request.data.get("clear", "false").lower() == "true"
if clear:
# .delete() saves the model by default
app.meta_icon.delete()
return Response({})
if icon:
app.meta_icon = icon
try:
app.save()
except PermissionError as exc:
LOGGER.warning("Failed to save icon", exc=exc)
return HttpResponseBadRequest()
return Response({})
return HttpResponseBadRequest()
return set_file(request, app, "meta_icon")
@permission_required("authentik_core.change_application")
@extend_schema(
@ -258,12 +249,7 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
def set_icon_url(self, request: Request, slug: str):
"""Set application icon (as URL)"""
app: Application = self.get_object()
url = request.data.get("url", None)
if url is None:
return HttpResponseBadRequest()
app.meta_icon.name = url
app.save()
return Response({})
return set_file_url(request, app, "meta_icon")
@permission_required("authentik_core.view_application", ["authentik_events.view_event"])
@extend_schema(responses={200: CoordinateSerializer(many=True)})

View File

@ -2,10 +2,11 @@
from typing import Iterable
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import extend_schema
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework import mixins
from rest_framework.decorators import action
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, ReadOnlyField, SerializerMethodField
@ -13,10 +14,17 @@ from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.core.models import Source, UserSourceConnection
from authentik.core.types import UserSettingSerializer
from authentik.lib.utils.file import (
FilePathSerializer,
FileUploadSerializer,
set_file,
set_file_url,
)
from authentik.lib.utils.reflection import all_subclasses
from authentik.policies.engine import PolicyEngine
@ -28,6 +36,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
managed = ReadOnlyField()
component = SerializerMethodField()
icon = ReadOnlyField(source="get_icon")
def get_component(self, obj: Source) -> str:
"""Get object component so that we know how to edit the object"""
@ -54,6 +63,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
"user_matching_mode",
"managed",
"user_path_template",
"icon",
]
@ -75,6 +85,49 @@ class SourceViewSet(
def get_queryset(self): # pragma: no cover
return Source.objects.select_subclasses()
@permission_required("authentik_core.change_source")
@extend_schema(
request={
"multipart/form-data": FileUploadSerializer,
},
responses={
200: OpenApiResponse(description="Success"),
400: OpenApiResponse(description="Bad request"),
},
)
@action(
detail=True,
pagination_class=None,
filter_backends=[],
methods=["POST"],
parser_classes=(MultiPartParser,),
)
# pylint: disable=unused-argument
def set_icon(self, request: Request, slug: str):
"""Set source icon"""
source: Source = self.get_object()
return set_file(request, source, "icon")
@permission_required("authentik_core.change_source")
@extend_schema(
request=FilePathSerializer,
responses={
200: OpenApiResponse(description="Success"),
400: OpenApiResponse(description="Bad request"),
},
)
@action(
detail=True,
pagination_class=None,
filter_backends=[],
methods=["POST"],
)
# pylint: disable=unused-argument
def set_icon_url(self, request: Request, slug: str):
"""Set source icon (as URL)"""
source: Source = self.get_object()
return set_file_url(request, source, "icon")
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def types(self, request: Request) -> Response:

View File

@ -2,7 +2,7 @@
from typing import Any
from django.db.models import Model
from rest_framework.fields import BooleanField, CharField, FileField, IntegerField
from rest_framework.fields import CharField, IntegerField
from rest_framework.serializers import Serializer, SerializerMethodField, ValidationError
@ -23,19 +23,6 @@ class PassiveSerializer(Serializer):
return Model()
class FileUploadSerializer(PassiveSerializer):
"""Serializer to upload file"""
file = FileField(required=False)
clear = BooleanField(default=False)
class FilePathSerializer(PassiveSerializer):
"""Serializer to upload file"""
url = CharField()
class MetaNameSerializer(PassiveSerializer):
"""Add verbose names to response"""

View File

@ -0,0 +1,20 @@
# Generated by Django 4.1.3 on 2022-11-15 20:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0023_source_authentik_c_slug_ccb2e5_idx_and_more"),
]
operations = [
migrations.AddField(
model_name="source",
name="icon",
field=models.FileField(
default=None, max_length=500, null=True, upload_to="source-icons/"
),
),
]

View File

@ -421,6 +421,12 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
enabled = models.BooleanField(default=True)
property_mappings = models.ManyToManyField("PropertyMapping", default=None, blank=True)
icon = models.FileField(
upload_to="source-icons/",
default=None,
null=True,
max_length=500,
)
authentication_flow = models.ForeignKey(
"authentik_flows.Flow",
@ -454,6 +460,16 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
objects = InheritanceManager()
@property
def get_icon(self) -> Optional[str]:
"""Get the URL to the Icon. If the name is /static or
starts with http it is returned as-is"""
if not self.icon:
return None
if "://" in self.icon.name or self.icon.name.startswith("/static"):
return self.icon.name
return self.icon.url
def get_user_path(self) -> str:
"""Get user path, fallback to default for formatting errors"""
try:

View File

@ -1,7 +1,6 @@
"""Flow API Views"""
from django.core.cache import cache
from django.http import HttpResponse
from django.http.response import HttpResponseBadRequest
from django.urls import reverse
from django.utils.translation import gettext as _
from drf_spectacular.types import OpenApiTypes
@ -19,19 +18,19 @@ from authentik.api.decorators import permission_required
from authentik.blueprints.v1.exporter import FlowExporter
from authentik.blueprints.v1.importer import Importer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import (
CacheSerializer,
FilePathSerializer,
FileUploadSerializer,
LinkSerializer,
PassiveSerializer,
)
from authentik.core.api.utils import CacheSerializer, LinkSerializer, PassiveSerializer
from authentik.events.utils import sanitize_dict
from authentik.flows.api.flows_diagram import FlowDiagram, FlowDiagramSerializer
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import Flow
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
from authentik.flows.views.executor import SESSION_KEY_HISTORY, SESSION_KEY_PLAN
from authentik.lib.utils.file import (
FilePathSerializer,
FileUploadSerializer,
set_file,
set_file_url,
)
from authentik.lib.views import bad_request_message
LOGGER = get_logger()
@ -249,25 +248,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
def set_background(self, request: Request, slug: str):
"""Set Flow background"""
flow: Flow = self.get_object()
background = request.FILES.get("file", None)
clear = request.data.get("clear", "false").lower() == "true"
if clear:
if flow.background_url.startswith("/media"):
# .delete() saves the model by default
flow.background.delete()
else:
flow.background = None
flow.save()
return Response({})
if background:
flow.background = background
try:
flow.save()
except PermissionError as exc:
LOGGER.warning("Failed to save icon", exc=exc)
return HttpResponseBadRequest()
return Response({})
return HttpResponseBadRequest()
return set_file(request, flow, "background")
@permission_required("authentik_core.change_application")
@extend_schema(
@ -287,12 +268,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
def set_background_url(self, request: Request, slug: str):
"""Set Flow background (as URL)"""
flow: Flow = self.get_object()
url = request.data.get("url", None)
if not url:
return HttpResponseBadRequest()
flow.background.name = url
flow.save()
return Response({})
return set_file_url(request, flow, "background")
@extend_schema(
responses={

View File

@ -0,0 +1,55 @@
"""file utils"""
from django.db.models import Model
from django.http import HttpResponseBadRequest
from rest_framework.fields import BooleanField, CharField, FileField
from rest_framework.request import Request
from rest_framework.response import Response
from structlog import get_logger
from authentik.core.api.utils import PassiveSerializer
LOGGER = get_logger()
class FileUploadSerializer(PassiveSerializer):
"""Serializer to upload file"""
file = FileField(required=False)
clear = BooleanField(default=False)
class FilePathSerializer(PassiveSerializer):
"""Serializer to upload file"""
url = CharField()
def set_file(request: Request, obj: Model, field: str):
"""Upload file"""
field = getattr(obj, field)
icon = request.FILES.get("file", None)
clear = request.data.get("clear", "false").lower() == "true"
if clear:
# .delete() saves the model by default
field.delete()
return Response({})
if icon:
field = icon
try:
obj.save()
except PermissionError as exc:
LOGGER.warning("Failed to save file", exc=exc)
return HttpResponseBadRequest()
return Response({})
return HttpResponseBadRequest()
def set_file_url(request: Request, obj: Model, field: str):
"""Set file field to URL"""
field = getattr(obj, field)
url = request.data.get("url", None)
if url is None:
return HttpResponseBadRequest()
field.name = url
obj.save()
return Response({})

View File

@ -35,6 +35,7 @@ class OAuthSourceSerializer(SourceSerializer):
provider_type = ChoiceField(choices=registry.get_name_tuple())
callback_url = SerializerMethodField()
type = SerializerMethodField()
def get_callback_url(self, instance: OAuthSource) -> str:
"""Get OAuth Callback URL"""
@ -46,8 +47,6 @@ class OAuthSourceSerializer(SourceSerializer):
return relative_url
return self.context["request"].build_absolute_uri(relative_url)
type = SerializerMethodField()
@extend_schema_field(SourceTypeSerializer)
def get_type(self, instance: OAuthSource) -> SourceTypeSerializer:
"""Get source's type configuration"""

View File

@ -75,15 +75,20 @@ class OAuthSource(Source):
def ui_login_button(self, request: HttpRequest) -> UILoginButton:
provider_type = self.type
provider = provider_type()
icon = self.get_icon
if not icon:
icon = provider.icon_url()
return UILoginButton(
name=self.name,
icon_url=provider.icon_url(),
challenge=provider.login_challenge(self, request),
icon_url=icon,
)
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
provider_type = self.type
provider = provider_type()
icon = self.get_icon
if not icon:
icon = provider_type().icon_url()
return UserSettingSerializer(
data={
"title": self.name,
@ -92,7 +97,7 @@ class OAuthSource(Source):
"authentik_sources_oauth:oauth-client-login",
kwargs={"source_slug": self.slug},
),
"icon_url": provider.icon_url(),
"icon_url": icon,
}
)

View File

@ -64,6 +64,9 @@ class PlexSource(Source):
return PlexSourceSerializer
def ui_login_button(self, request: HttpRequest) -> UILoginButton:
icon = self.get_icon
if not icon:
icon = static("authentik/sources/plex.svg")
return UILoginButton(
challenge=PlexAuthenticationChallenge(
{
@ -73,17 +76,20 @@ class PlexSource(Source):
"slug": self.slug,
}
),
icon_url=static("authentik/sources/plex.svg"),
icon_url=icon,
name=self.name,
)
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
icon = self.get_icon
if not icon:
icon = static("authentik/sources/plex.svg")
return UserSettingSerializer(
data={
"title": self.name,
"component": "ak-user-settings-source-plex",
"configure_url": self.client_id,
"icon_url": static("authentik/sources/plex.svg"),
"icon_url": icon,
}
)

View File

@ -40,7 +40,27 @@ class SAMLSourceViewSet(UsedByMixin, ModelViewSet):
queryset = SAMLSource.objects.all()
serializer_class = SAMLSourceSerializer
lookup_field = "slug"
filterset_fields = "__all__"
filterset_fields = [
"name",
"slug",
"enabled",
"authentication_flow",
"enrollment_flow",
"managed",
"policy_engine_mode",
"user_matching_mode",
"pre_authentication_flow",
"issuer",
"sso_url",
"slo_url",
"allow_idp_initiated",
"name_id_policy",
"binding_type",
"signing_kp",
"digest_algorithm",
"signature_algorithm",
"temporary_user_delete_after",
]
search_fields = ["name", "slug"]
ordering = ["name"]

View File

@ -191,9 +191,13 @@ class SAMLSource(Source):
}
),
name=self.name,
icon_url=self.get_icon,
)
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
icon = self.get_icon
if not icon:
icon = static(f"authentik/sources/{self.slug}.svg")
return UserSettingSerializer(
data={
"title": self.name,
@ -202,7 +206,7 @@ class SAMLSource(Source):
"authentik_sources_saml:login",
kwargs={"source_slug": self.slug},
),
"icon_url": static(f"authentik/sources/{self.slug}.svg"),
"icon_url": icon,
}
)

View File

@ -15795,6 +15795,69 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/sources/all/{slug}/set_icon/:
post:
operationId: sources_all_set_icon_create
description: Set source icon
parameters:
- in: path
name: slug
schema:
type: string
description: Internal source name, used in URLs.
required: true
tags:
- sources
requestBody:
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/FileUploadRequest'
security:
- authentik: []
responses:
'200':
description: Success
'400':
description: Bad request
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/sources/all/{slug}/set_icon_url/:
post:
operationId: sources_all_set_icon_url_create
description: Set source icon (as URL)
parameters:
- in: path
name: slug
schema:
type: string
description: Internal source name, used in URLs.
required: true
tags:
- sources
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/FilePathRequest'
required: true
security:
- authentik: []
responses:
'200':
description: Success
'400':
description: Bad request
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/sources/all/{slug}/used_by/:
get:
operationId: sources_all_used_by_list
@ -17092,20 +17155,6 @@ paths:
description: Number of results to return per page.
schema:
type: integer
- in: query
name: pbm_uuid
schema:
type: string
format: uuid
- in: query
name: policies
schema:
type: array
items:
type: string
format: uuid
explode: true
style: form
- in: query
name: policy_engine_mode
schema:
@ -17118,15 +17167,6 @@ paths:
schema:
type: string
format: uuid
- in: query
name: property_mappings
schema:
type: array
items:
type: string
format: uuid
explode: true
style: form
- name: search
required: false
in: query
@ -17176,10 +17216,6 @@ paths:
- username_link
description: How the source determines if an existing user should be authenticated
or a new user enrolled.
- in: query
name: user_path_template
schema:
type: string
tags:
- sources
security:
@ -28871,6 +28907,10 @@ components:
readOnly: true
user_path_template:
type: string
icon:
type: string
nullable: true
readOnly: true
server_uri:
type: string
format: uri
@ -28933,6 +28973,7 @@ components:
required:
- base_dn
- component
- icon
- managed
- meta_model_name
- name
@ -29662,6 +29703,10 @@ components:
readOnly: true
user_path_template:
type: string
icon:
type: string
nullable: true
readOnly: true
provider_type:
$ref: '#/components/schemas/ProviderTypeEnum'
request_token_url:
@ -29707,6 +29752,7 @@ components:
- callback_url
- component
- consumer_key
- icon
- managed
- meta_model_name
- name
@ -35075,6 +35121,10 @@ components:
readOnly: true
user_path_template:
type: string
icon:
type: string
nullable: true
readOnly: true
client_id:
type: string
description: Client identifier used to talk to Plex.
@ -35092,6 +35142,7 @@ components:
description: Plex token used to check friends
required:
- component
- icon
- managed
- meta_model_name
- name
@ -36495,6 +36546,10 @@ components:
readOnly: true
user_path_template:
type: string
icon:
type: string
nullable: true
readOnly: true
pre_authentication_flow:
type: string
format: uuid
@ -36543,6 +36598,7 @@ components:
doesn''t log out manually. (Format: hours=1;minutes=2;seconds=3).'
required:
- component
- icon
- managed
- meta_model_name
- name
@ -36936,8 +36992,13 @@ components:
readOnly: true
user_path_template:
type: string
icon:
type: string
nullable: true
readOnly: true
required:
- component
- icon
- managed
- meta_model_name
- name

View File

@ -1,5 +1,5 @@
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/FormGroup";
@ -9,11 +9,12 @@ import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import {
CapabilitiesEnum,
FlowsApi,
FlowsInstancesListDesignationEnum,
OAuthSource,
@ -57,6 +58,9 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
@property({ attribute: false })
providerType: SourceType | null = null;
@state()
clearIcon = false;
getSuccessMessage(): string {
if (this.instance) {
return t`Successfully updated source.`;
@ -65,18 +69,38 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
}
}
send = (data: OAuthSource): Promise<OAuthSource> => {
send = async (data: OAuthSource): Promise<OAuthSource> => {
data.providerType = (this.providerType?.slug || "") as ProviderTypeEnum;
if (this.instance?.slug) {
return new SourcesApi(DEFAULT_CONFIG).sourcesOauthPartialUpdate({
let source: OAuthSource;
if (this.instance) {
source = await new SourcesApi(DEFAULT_CONFIG).sourcesOauthPartialUpdate({
slug: this.instance.slug,
patchedOAuthSourceRequest: data,
});
} else {
return new SourcesApi(DEFAULT_CONFIG).sourcesOauthCreate({
source = await new SourcesApi(DEFAULT_CONFIG).sourcesOauthCreate({
oAuthSourceRequest: data as unknown as OAuthSourceRequest,
});
}
const c = await config();
if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
const icon = this.getFormFiles()["icon"];
if (icon || this.clearIcon) {
await new SourcesApi(DEFAULT_CONFIG).sourcesAllSetIconCreate({
slug: source.slug,
file: icon,
clear: this.clearIcon,
});
}
} else {
await new SourcesApi(DEFAULT_CONFIG).sourcesAllSetIconUrlCreate({
slug: source.slug,
filePathRequest: {
url: data.icon || "",
},
});
}
return source;
};
renderUrlOptions(): TemplateResult {
@ -282,6 +306,54 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`}
</p>
</ak-form-element-horizontal>
${until(
config().then((c) => {
if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
return html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
<input type="file" value="" class="pf-c-form-control" />
${this.instance?.icon
? html`
<p class="pf-c-form__helper-text">
${t`Currently set to:`} ${this.instance?.icon}
</p>
`
: html``}
</ak-form-element-horizontal>
${this.instance?.icon
? html`
<ak-form-element-horizontal>
<div class="pf-c-check">
<input
type="checkbox"
class="pf-c-check__input"
@change=${(ev: Event) => {
const target = ev.target as HTMLInputElement;
this.clearIcon = target.checked;
}}
/>
<label class="pf-c-check__label">
${t`Clear icon`}
</label>
</div>
<p class="pf-c-form__helper-text">
${t`Delete currently set icon.`}
</p>
</ak-form-element-horizontal>
`
: html``}`;
}
return html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
<input
type="text"
value="${first(this.instance?.icon, "")}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${t`Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".`}
</p>
</ak-form-element-horizontal>`;
}),
)}
<ak-form-group .expanded=${true}>
<span slot="header"> ${t`Protocol settings`} </span>

View File

@ -1,5 +1,5 @@
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { PlexAPIClient, PlexResource, popupCenterScreen } from "@goauthentik/common/helpers/plex";
import { first, randomString } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
@ -9,11 +9,12 @@ import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import {
CapabilitiesEnum,
FlowsApi,
FlowsInstancesListDesignationEnum,
PlexSource,
@ -35,6 +36,9 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> {
});
}
@state()
clearIcon = false;
@property()
plexToken?: string;
@ -55,18 +59,38 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> {
}
}
send = (data: PlexSource): Promise<PlexSource> => {
send = async (data: PlexSource): Promise<PlexSource> => {
data.plexToken = this.plexToken || "";
if (this.instance?.slug) {
return new SourcesApi(DEFAULT_CONFIG).sourcesPlexUpdate({
let source: PlexSource;
if (this.instance) {
source = await new SourcesApi(DEFAULT_CONFIG).sourcesPlexUpdate({
slug: this.instance.slug,
plexSourceRequest: data,
});
} else {
return new SourcesApi(DEFAULT_CONFIG).sourcesPlexCreate({
source = await new SourcesApi(DEFAULT_CONFIG).sourcesPlexCreate({
plexSourceRequest: data,
});
}
const c = await config();
if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
const icon = this.getFormFiles()["icon"];
if (icon || this.clearIcon) {
await new SourcesApi(DEFAULT_CONFIG).sourcesAllSetIconCreate({
slug: source.slug,
file: icon,
clear: this.clearIcon,
});
}
} else {
await new SourcesApi(DEFAULT_CONFIG).sourcesAllSetIconUrlCreate({
slug: source.slug,
filePathRequest: {
url: data.icon || "",
},
});
}
return source;
};
async doAuth(): Promise<void> {
@ -229,6 +253,54 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> {
${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`}
</p>
</ak-form-element-horizontal>
${until(
config().then((c) => {
if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
return html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
<input type="file" value="" class="pf-c-form-control" />
${this.instance?.icon
? html`
<p class="pf-c-form__helper-text">
${t`Currently set to:`} ${this.instance?.icon}
</p>
`
: html``}
</ak-form-element-horizontal>
${this.instance?.icon
? html`
<ak-form-element-horizontal>
<div class="pf-c-check">
<input
type="checkbox"
class="pf-c-check__input"
@change=${(ev: Event) => {
const target = ev.target as HTMLInputElement;
this.clearIcon = target.checked;
}}
/>
<label class="pf-c-check__label">
${t`Clear icon`}
</label>
</div>
<p class="pf-c-form__helper-text">
${t`Delete currently set icon.`}
</p>
</ak-form-element-horizontal>
`
: html``}`;
}
return html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
<input
type="text"
value="${first(this.instance?.icon, "")}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${t`Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".`}
</p>
</ak-form-element-horizontal>`;
}),
)}
<ak-form-group .expanded=${true}>
<span slot="header"> ${t`Protocol settings`} </span>

View File

@ -1,5 +1,5 @@
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -9,12 +9,13 @@ import "@goauthentik/elements/utils/TimeDeltaHelp";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import {
BindingTypeEnum,
CapabilitiesEnum,
CryptoApi,
DigestAlgorithmEnum,
FlowsApi,
@ -28,6 +29,9 @@ import {
@customElement("ak-source-saml-form")
export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
@state()
clearIcon = false;
loadInstance(pk: string): Promise<SAMLSource> {
return new SourcesApi(DEFAULT_CONFIG).sourcesSamlRetrieve({
slug: pk,
@ -42,17 +46,37 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
}
}
send = (data: SAMLSource): Promise<SAMLSource> => {
send = async (data: SAMLSource): Promise<SAMLSource> => {
let source: SAMLSource;
if (this.instance) {
return new SourcesApi(DEFAULT_CONFIG).sourcesSamlUpdate({
source = await new SourcesApi(DEFAULT_CONFIG).sourcesSamlUpdate({
slug: this.instance.slug,
sAMLSourceRequest: data,
});
} else {
return new SourcesApi(DEFAULT_CONFIG).sourcesSamlCreate({
source = await new SourcesApi(DEFAULT_CONFIG).sourcesSamlCreate({
sAMLSourceRequest: data,
});
}
const c = await config();
if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
const icon = this.getFormFiles()["icon"];
if (icon || this.clearIcon) {
await new SourcesApi(DEFAULT_CONFIG).sourcesAllSetIconCreate({
slug: source.slug,
file: icon,
clear: this.clearIcon,
});
}
} else {
await new SourcesApi(DEFAULT_CONFIG).sourcesAllSetIconUrlCreate({
slug: source.slug,
filePathRequest: {
url: data.icon || "",
},
});
}
return source;
};
renderForm(): TemplateResult {
@ -126,6 +150,54 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
</option>
</select>
</ak-form-element-horizontal>
${until(
config().then((c) => {
if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
return html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
<input type="file" value="" class="pf-c-form-control" />
${this.instance?.icon
? html`
<p class="pf-c-form__helper-text">
${t`Currently set to:`} ${this.instance?.icon}
</p>
`
: html``}
</ak-form-element-horizontal>
${this.instance?.icon
? html`
<ak-form-element-horizontal>
<div class="pf-c-check">
<input
type="checkbox"
class="pf-c-check__input"
@change=${(ev: Event) => {
const target = ev.target as HTMLInputElement;
this.clearIcon = target.checked;
}}
/>
<label class="pf-c-check__label">
${t`Clear icon`}
</label>
</div>
<p class="pf-c-form__helper-text">
${t`Delete currently set icon.`}
</p>
</ak-form-element-horizontal>
`
: html``}`;
}
return html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
<input
type="text"
value="${first(this.instance?.icon, "")}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${t`Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".`}
</p>
</ak-form-element-horizontal>`;
}),
)}
<ak-form-group .expanded=${true}>
<span slot="header"> ${t`Protocol settings`} </span>