From 9a85dc459f2255c8ca36abcfe023045587d667d2 Mon Sep 17 00:00:00 2001 From: Dominic R Date: Thu, 5 Feb 2026 19:43:37 -0500 Subject: [PATCH] start working on new implementation --- authentik/admin/files/backends/static.py | 16 +++- .../backends/tests/test_static_backend.py | 1 + authentik/admin/files/tests/test_api.py | 25 ++++++ authentik/core/api/object_types.py | 6 +- authentik/core/models.py | 79 +++++++++++++------ authentik/core/types.py | 8 +- authentik/sources/kerberos/models.py | 12 +-- authentik/sources/ldap/models.py | 5 +- authentik/sources/oauth/models.py | 70 ++++++++++++---- authentik/sources/oauth/types/registry.py | 5 -- authentik/sources/plex/models.py | 12 +-- authentik/sources/saml/models.py | 11 +-- authentik/sources/scim/models.py | 5 +- authentik/sources/telegram/models.py | 12 +-- authentik/stages/identification/stage.py | 3 +- authentik/stages/identification/tests.py | 24 ++++-- schema.yml | 33 +++++++- web/authentik/sources/apple/dark.svg | 3 + web/authentik/sources/apple/light.svg | 3 + web/authentik/sources/azuread/dark.svg | 3 + web/authentik/sources/azuread/light.svg | 3 + web/authentik/sources/discord/dark.svg | 3 + web/authentik/sources/discord/light.svg | 3 + web/authentik/sources/dropbox/dark.svg | 3 + web/authentik/sources/dropbox/light.svg | 3 + web/authentik/sources/entraid/dark.svg | 3 + web/authentik/sources/entraid/light.svg | 3 + web/authentik/sources/facebook/dark.svg | 3 + web/authentik/sources/facebook/light.svg | 3 + web/authentik/sources/github/dark.svg | 3 + web/authentik/sources/github/light.svg | 3 + web/authentik/sources/gitlab/dark.svg | 3 + web/authentik/sources/gitlab/light.svg | 3 + web/authentik/sources/google/dark.svg | 3 + web/authentik/sources/google/light.svg | 3 + web/authentik/sources/reddit/dark.svg | 4 + web/authentik/sources/reddit/light.svg | 4 + web/authentik/sources/saml/dark.svg | 5 ++ web/authentik/sources/saml/light.svg | 5 ++ web/authentik/sources/scim/dark.svg | 6 ++ web/authentik/sources/scim/light.svg | 6 ++ web/authentik/sources/slack/dark.svg | 3 + web/authentik/sources/slack/light.svg | 3 + web/authentik/sources/telegram/dark.svg | 3 + web/authentik/sources/telegram/light.svg | 3 + web/authentik/sources/twitch/dark.svg | 4 + web/authentik/sources/twitch/light.svg | 4 + web/authentik/sources/twitter/dark.svg | 3 + web/authentik/sources/twitter/light.svg | 3 + web/authentik/sources/wechat/dark.svg | 4 + web/authentik/sources/wechat/light.svg | 4 + web/authentik/sources/wsfed/dark.svg | 3 + web/authentik/sources/wsfed/light.svg | 3 + web/src/elements/sources/utils.ts | 30 +++++-- .../elements/user/sources/SourceSettings.css | 2 +- .../elements/user/sources/SourceSettings.ts | 9 ++- .../elements/wizard/TypeCreateWizardPage.ts | 35 ++++---- .../IdentificationStage.stories.ts | 5 +- .../identification/IdentificationStage.ts | 4 +- web/src/flow/stages/identification/styles.css | 7 +- 60 files changed, 408 insertions(+), 135 deletions(-) create mode 100644 web/authentik/sources/apple/dark.svg create mode 100644 web/authentik/sources/apple/light.svg create mode 100644 web/authentik/sources/azuread/dark.svg create mode 100644 web/authentik/sources/azuread/light.svg create mode 100644 web/authentik/sources/discord/dark.svg create mode 100644 web/authentik/sources/discord/light.svg create mode 100644 web/authentik/sources/dropbox/dark.svg create mode 100644 web/authentik/sources/dropbox/light.svg create mode 100644 web/authentik/sources/entraid/dark.svg create mode 100644 web/authentik/sources/entraid/light.svg create mode 100644 web/authentik/sources/facebook/dark.svg create mode 100644 web/authentik/sources/facebook/light.svg create mode 100644 web/authentik/sources/github/dark.svg create mode 100644 web/authentik/sources/github/light.svg create mode 100644 web/authentik/sources/gitlab/dark.svg create mode 100644 web/authentik/sources/gitlab/light.svg create mode 100644 web/authentik/sources/google/dark.svg create mode 100644 web/authentik/sources/google/light.svg create mode 100644 web/authentik/sources/reddit/dark.svg create mode 100644 web/authentik/sources/reddit/light.svg create mode 100644 web/authentik/sources/saml/dark.svg create mode 100644 web/authentik/sources/saml/light.svg create mode 100644 web/authentik/sources/scim/dark.svg create mode 100644 web/authentik/sources/scim/light.svg create mode 100644 web/authentik/sources/slack/dark.svg create mode 100644 web/authentik/sources/slack/light.svg create mode 100644 web/authentik/sources/telegram/dark.svg create mode 100644 web/authentik/sources/telegram/light.svg create mode 100644 web/authentik/sources/twitch/dark.svg create mode 100644 web/authentik/sources/twitch/light.svg create mode 100644 web/authentik/sources/twitter/dark.svg create mode 100644 web/authentik/sources/twitter/light.svg create mode 100644 web/authentik/sources/wechat/dark.svg create mode 100644 web/authentik/sources/wechat/light.svg create mode 100644 web/authentik/sources/wsfed/dark.svg create mode 100644 web/authentik/sources/wsfed/light.svg diff --git a/authentik/admin/files/backends/static.py b/authentik/admin/files/backends/static.py index edff6ffa5c..fbd9da1713 100644 --- a/authentik/admin/files/backends/static.py +++ b/authentik/admin/files/backends/static.py @@ -3,7 +3,7 @@ from pathlib import Path from django.http.request import HttpRequest -from authentik.admin.files.backends.base import Backend +from authentik.admin.files.backends.base import Backend, THEME_VARIABLE from authentik.admin.files.usage import FileUsage from authentik.lib.config import CONFIG @@ -35,6 +35,20 @@ class StaticBackend(Backend): for file_path in STATIC_ASSETS_SOURCES_DIR.iterdir(): if file_path.is_file() and (file_path.suffix in STATIC_FILE_EXTENSIONS): yield f"{STATIC_PATH_PREFIX}/authentik/sources/{file_path.name}" + continue + + if not file_path.is_dir(): + continue + + for extension in STATIC_FILE_EXTENSIONS: + light_variant = file_path / f"light{extension}" + dark_variant = file_path / f"dark{extension}" + if light_variant.exists() and dark_variant.exists(): + yield ( + f"{STATIC_PATH_PREFIX}/authentik/sources/" + f"{file_path.name}/{THEME_VARIABLE}{extension}" + ) + break # List other static assets for dir in STATIC_ASSETS_DIRS: diff --git a/authentik/admin/files/backends/tests/test_static_backend.py b/authentik/admin/files/backends/tests/test_static_backend.py index 4bff77d59e..768dd131f9 100644 --- a/authentik/admin/files/backends/tests/test_static_backend.py +++ b/authentik/admin/files/backends/tests/test_static_backend.py @@ -37,6 +37,7 @@ class TestStaticBackend(TestCase): """Test list_files includes expected files""" files = list(self.backend.list_files()) + self.assertIn("/static/authentik/sources/github/%(theme)s.svg", files) self.assertIn("/static/authentik/sources/ldap.png", files) self.assertIn("/static/authentik/sources/openidconnect.svg", files) self.assertIn("/static/authentik/sources/saml.png", files) diff --git a/authentik/admin/files/tests/test_api.py b/authentik/admin/files/tests/test_api.py index a7a9673a36..7e9c4f35bd 100644 --- a/authentik/admin/files/tests/test_api.py +++ b/authentik/admin/files/tests/test_api.py @@ -219,6 +219,31 @@ class TestFileAPI(FileTestFileBackendMixin, TestCase): manager.delete_file(file_name) + def test_list_files_includes_themed_urls_for_static_themed_icons(self): + """Test listing static themed icons exposes a %(theme)s placeholder entry.""" + response = self.client.get( + reverse( + "authentik_api:files", + query={ + "search": "github/%(theme)s.svg", + }, + ) + ) + + self.assertEqual(response.status_code, 200) + self.assertIn( + { + "name": "/static/authentik/sources/github/%(theme)s.svg", + "url": "http://testserver/static/authentik/sources/github/%(theme)s.svg", + "mime_type": "image/svg+xml", + "themed_urls": { + "light": "http://testserver/static/authentik/sources/github/light.svg", + "dark": "http://testserver/static/authentik/sources/github/dark.svg", + }, + }, + response.data, + ) + def test_list_files_includes_themed_urls_dict(self): """Test listing files includes themed_urls as dict for themed files""" manager = FileManager(FileUsage.MEDIA) diff --git a/authentik/core/api/object_types.py b/authentik/core/api/object_types.py index 278c643746..71d124de38 100644 --- a/authentik/core/api/object_types.py +++ b/authentik/core/api/object_types.py @@ -9,7 +9,7 @@ from rest_framework.fields import ( from rest_framework.request import Request from rest_framework.response import Response -from authentik.core.api.utils import PassiveSerializer +from authentik.core.api.utils import PassiveSerializer, ThemedUrlsSerializer from authentik.lib.models import DeprecatedMixin from authentik.lib.utils.reflection import all_subclasses @@ -22,7 +22,8 @@ class TypeCreateSerializer(PassiveSerializer): component = CharField(required=True) model_name = CharField(required=True) - icon_url = CharField(required=False) + icon_url = CharField(required=False, allow_null=True) + icon_themed_urls = ThemedUrlsSerializer(required=False, allow_null=True) requires_enterprise = BooleanField(default=False) deprecated = BooleanField(default=False) @@ -66,6 +67,7 @@ class TypesMixin: "component": instance.component, "model_name": subclass._meta.model_name, "icon_url": getattr(instance, "icon_url", None), + "icon_themed_urls": getattr(instance, "icon_themed_urls", None), "requires_enterprise": False, "deprecated": isinstance(instance, DeprecatedMixin), } diff --git a/authentik/core/models.py b/authentik/core/models.py index 4043baa9b8..13bd91b19e 100644 --- a/authentik/core/models.py +++ b/authentik/core/models.py @@ -14,10 +14,12 @@ from django.contrib.auth.hashers import check_password from django.contrib.auth.models import AbstractUser, Permission from django.contrib.auth.models import UserManager as DjangoUserManager from django.contrib.sessions.base_session import AbstractBaseSession +from django.contrib.staticfiles import finders from django.core.validators import validate_slug from django.db import models from django.db.models import Manager, Q, QuerySet, options from django.http import HttpRequest +from django.templatetags.static import static from django.utils.functional import cached_property from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ @@ -58,6 +60,27 @@ USER_PATH_SYSTEM_PREFIX = "goauthentik.io" _USER_ATTR_PREFIX = f"{USER_PATH_SYSTEM_PREFIX}/user" USER_ATTRIBUTE_DEBUG = f"{_USER_ATTR_PREFIX}/debug" USER_ATTRIBUTE_GENERATED = f"{_USER_ATTR_PREFIX}/generated" + + +def _get_default_source_icon_themed_urls(icon_name: str) -> dict[str, str] | None: + themed_paths = { + "light": f"authentik/sources/{icon_name}/light.svg", + "dark": f"authentik/sources/{icon_name}/dark.svg", + } + if all(finders.find(path) for path in themed_paths.values()): + return {theme: static(path) for theme, path in themed_paths.items()} + + for extension in ("svg", "png"): + legacy_path = f"authentik/sources/{icon_name}.{extension}" + if finders.find(legacy_path): + legacy_url = static(legacy_path) + return { + "light": legacy_url, + "dark": legacy_url, + } + return None + + USER_ATTRIBUTE_EXPIRES = f"{_USER_ATTR_PREFIX}/expires" USER_ATTRIBUTE_DELETE_ON_LOGOUT = f"{_USER_ATTR_PREFIX}/delete-on-logout" USER_ATTRIBUTE_SOURCES = f"{_USER_ATTR_PREFIX}/sources" @@ -951,34 +974,46 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel): objects = InheritanceManager() - def get_icon_url(self, request=None, use_cache: bool = True) -> str | None: - """Get the URL to the source icon.""" - if not self.icon: - return None - return get_file_manager(FileUsage.MEDIA).file_url(self.icon, request, use_cache=use_cache) + default_icon_name: str | None = None + """Source type name used to resolve built-in themed icons (e.g. "discord").""" @property def icon_url(self) -> str | None: - """Get the URL to the source icon""" - return self.get_icon_url() - - def get_icon_themed_urls( - self, - request=None, - use_cache: bool = True, - ) -> dict[str, str] | None: - """Get themed URLs for icon if it contains %(theme)s.""" - if not self.icon: - return None - return get_file_manager(FileUsage.MEDIA).themed_urls( - self.icon, - request, - use_cache=use_cache, - ) + """Get the URL to the source icon.""" + manager = get_file_manager(FileUsage.MEDIA) + custom_icon = None + try: + custom_icon = self.icon + except AttributeError: + # Abstract type instances (created via __new__) don't have field state. + custom_icon = None + if custom_icon: + return manager.file_url(custom_icon) + if self.default_icon_name: + return _get_default_source_icon_url(self.default_icon_name) + return None @property def icon_themed_urls(self) -> dict[str, str] | None: - return self.get_icon_themed_urls() + """Get themed URLs for source icon.""" + manager = get_file_manager(FileUsage.MEDIA) + # If the user set a custom icon, check if it supports themes. + custom_icon = None + try: + custom_icon = self.icon + except AttributeError: + # Abstract type instances (created via __new__) don't have field state. + custom_icon = None + if custom_icon: + return manager.themed_urls(custom_icon) + # Fall back to built-in default icons. + if self.default_icon_name: + return _get_default_source_icon_themed_urls(self.default_icon_name) + return None + + @property + def icon_dynamic_url(self) -> dict[str, str] | None: + return _build_dynamic_url_map(self.icon_url, self.icon_themed_urls) def get_user_path(self) -> str: """Get user path, fallback to default for formatting errors""" diff --git a/authentik/core/types.py b/authentik/core/types.py index 61c25fa3e1..2da9d9b223 100644 --- a/authentik/core/types.py +++ b/authentik/core/types.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from rest_framework.fields import CharField -from authentik.core.api.utils import PassiveSerializer +from authentik.core.api.utils import PassiveSerializer, ThemedUrlsSerializer from authentik.flows.challenge import Challenge @@ -21,6 +21,9 @@ class UILoginButton: # Icon URL, used as-is icon_url: str | None = None + # Pre-resolved themed icon URLs for light/dark variants + icon_themed_urls: dict[str, str] | None = None + # Whether this source should be displayed as a prominent button promoted: bool = False @@ -32,4 +35,5 @@ class UserSettingSerializer(PassiveSerializer): component = CharField() title = CharField(required=True) configure_url = CharField(required=False) - icon_url = CharField(required=False) + icon_url = CharField(required=False, allow_null=True) + icon_themed_urls = ThemedUrlsSerializer(required=False, allow_null=True) diff --git a/authentik/sources/kerberos/models.py b/authentik/sources/kerberos/models.py index 07ef0d5af6..9e83849c2a 100644 --- a/authentik/sources/kerberos/models.py +++ b/authentik/sources/kerberos/models.py @@ -11,7 +11,6 @@ import pglock from django.db import connection, models from django.http import HttpRequest from django.shortcuts import reverse -from django.templatetags.static import static from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ from kadmin import KAdm5Variant, KAdmin, KAdminApiVersion @@ -129,12 +128,7 @@ class KerberosSource(IncomingSyncSource): def property_mapping_type(self) -> type[PropertyMapping]: return KerberosSourcePropertyMapping - @property - def icon_url(self) -> str: - icon = super().icon_url - if not icon: - return static("authentik/sources/kerberos.png") - return icon + default_icon_name = "kerberos" @property def schedule_specs(self) -> list[ScheduleSpec]: @@ -168,7 +162,7 @@ class KerberosSource(IncomingSyncSource): } ), name=self.name, - icon_url=self.get_icon_url(request, use_cache=False) or self.icon_url, + icon_url=self.icon_dynamic_url, promoted=self.promoted, ) @@ -181,7 +175,7 @@ class KerberosSource(IncomingSyncSource): "authentik_sources_kerberos:spnego-login", kwargs={"source_slug": self.slug}, ), - "icon_url": self.icon_url, + "icon_url": self.icon_dynamic_url, } ) diff --git a/authentik/sources/ldap/models.py b/authentik/sources/ldap/models.py index f7fe889a97..9ab7dc9750 100644 --- a/authentik/sources/ldap/models.py +++ b/authentik/sources/ldap/models.py @@ -9,7 +9,6 @@ from typing import Any import pglock from django.db import connection, models -from django.templatetags.static import static from django.utils.translation import gettext_lazy as _ from ldap3 import ALL, NONE, RANDOM, Connection, Server, ServerPool, Tls from ldap3.core.exceptions import LDAPException, LDAPInsufficientAccessRightsResult, LDAPSchemaError @@ -211,9 +210,7 @@ class LDAPSource(IncomingSyncSource): **kwargs, ) - @property - def icon_url(self) -> str: - return static("authentik/sources/ldap.png") + default_icon_name = "ldap" def server(self, **kwargs) -> ServerPool: """Get LDAP Server/ServerPool""" diff --git a/authentik/sources/oauth/models.py b/authentik/sources/oauth/models.py index 3e57e0e4d8..56f7cc2195 100644 --- a/authentik/sources/oauth/models.py +++ b/authentik/sources/oauth/models.py @@ -15,6 +15,7 @@ from authentik.core.models import ( PropertyMapping, Source, UserSourceConnection, + _get_default_source_icon_themed_urls, ) from authentik.core.types import UILoginButton, UserSettingSerializer @@ -112,23 +113,23 @@ class OAuthSource(NonCreatableType, Source): return self.source_type().get_base_group_properties(source=self, **kwargs) @property - def icon_url(self) -> str | None: - # When listing source types, this property might be retrieved from an abstract - # model. In that case we can't check self.provider_type or self.icon_url - # and as such we attempt to find the correct provider type based on the mode name - if self.Meta.abstract: - from authentik.sources.oauth.types.registry import registry + def icon_themed_urls(self) -> dict[str, str] | None: + """Get themed URLs for source icon. - provider_type = registry.find_type( - self._meta.model_name.replace(OAuthSource._meta.model_name, "") - ) - return provider_type().icon_url() - icon = super().icon_url - if not icon: - provider_type = self.source_type - provider = provider_type() - icon = provider.icon_url() - return icon + OAuth source types are abstract models so the DB always stores OAuthSource + instances. We resolve the built-in icon from the provider_type field instead + of the class-level default_icon_name (which only exists on the abstract + subclasses used by the TypeCreate wizard). + """ + urls = super().icon_themed_urls + if urls: + return urls + try: + if self.provider_type: + return _get_default_source_icon_themed_urls(self.provider_type) + except AttributeError: + pass + return None def ui_login_button(self, request: HttpRequest) -> UILoginButton: provider_type = self.source_type @@ -136,7 +137,7 @@ class OAuthSource(NonCreatableType, Source): return UILoginButton( name=self.name, challenge=provider.login_challenge(self, request), - icon_url=self.get_icon_url(request, use_cache=False) or self.icon_url, + icon_url=self.icon_dynamic_url, promoted=self.promoted, ) @@ -150,6 +151,7 @@ class OAuthSource(NonCreatableType, Source): kwargs={"source_slug": self.slug}, ), "icon_url": self.icon_url, + "icon_themed_urls": self.icon_themed_urls, } ) @@ -164,6 +166,8 @@ class OAuthSource(NonCreatableType, Source): class GitHubOAuthSource(CreatableType, OAuthSource): """Social Login using GitHub.com or a GitHub-Enterprise Instance.""" + default_icon_name = "github" + class Meta: abstract = True verbose_name = _("GitHub OAuth Source") @@ -173,6 +177,8 @@ class GitHubOAuthSource(CreatableType, OAuthSource): class GitLabOAuthSource(CreatableType, OAuthSource): """Social Login using GitLab.com or a GitLab Instance.""" + default_icon_name = "gitlab" + class Meta: abstract = True verbose_name = _("GitLab OAuth Source") @@ -182,6 +188,8 @@ class GitLabOAuthSource(CreatableType, OAuthSource): class TwitchOAuthSource(CreatableType, OAuthSource): """Social Login using Twitch.""" + default_icon_name = "twitch" + class Meta: abstract = True verbose_name = _("Twitch OAuth Source") @@ -191,6 +199,8 @@ class TwitchOAuthSource(CreatableType, OAuthSource): class MailcowOAuthSource(CreatableType, OAuthSource): """Social Login using Mailcow.""" + default_icon_name = "mailcow" + class Meta: abstract = True verbose_name = _("Mailcow OAuth Source") @@ -200,6 +210,8 @@ class MailcowOAuthSource(CreatableType, OAuthSource): class TwitterOAuthSource(CreatableType, OAuthSource): """Social Login using Twitter.com""" + default_icon_name = "twitter" + class Meta: abstract = True verbose_name = _("Twitter OAuth Source") @@ -209,6 +221,8 @@ class TwitterOAuthSource(CreatableType, OAuthSource): class FacebookOAuthSource(CreatableType, OAuthSource): """Social Login using Facebook.com.""" + default_icon_name = "facebook" + class Meta: abstract = True verbose_name = _("Facebook OAuth Source") @@ -218,6 +232,8 @@ class FacebookOAuthSource(CreatableType, OAuthSource): class DiscordOAuthSource(CreatableType, OAuthSource): """Social Login using Discord.""" + default_icon_name = "discord" + class Meta: abstract = True verbose_name = _("Discord OAuth Source") @@ -227,6 +243,8 @@ class DiscordOAuthSource(CreatableType, OAuthSource): class SlackOAuthSource(CreatableType, OAuthSource): """Social Login using Slack.""" + default_icon_name = "slack" + class Meta: abstract = True verbose_name = _("Slack OAuth Source") @@ -236,6 +254,8 @@ class SlackOAuthSource(CreatableType, OAuthSource): class PatreonOAuthSource(CreatableType, OAuthSource): """Social Login using Patreon.""" + default_icon_name = "patreon" + class Meta: abstract = True verbose_name = _("Patreon OAuth Source") @@ -245,6 +265,8 @@ class PatreonOAuthSource(CreatableType, OAuthSource): class GoogleOAuthSource(CreatableType, OAuthSource): """Social Login using Google or Google Workspace (GSuite).""" + default_icon_name = "google" + class Meta: abstract = True verbose_name = _("Google OAuth Source") @@ -254,6 +276,8 @@ class GoogleOAuthSource(CreatableType, OAuthSource): class AzureADOAuthSource(CreatableType, OAuthSource): """(Deprecated) Social Login using Azure AD.""" + default_icon_name = "azuread" + class Meta: abstract = True verbose_name = _("Azure AD OAuth Source") @@ -265,6 +289,8 @@ class AzureADOAuthSource(CreatableType, OAuthSource): class EntraIDOAuthSource(CreatableType, OAuthSource): """Social Login using Entra ID.""" + default_icon_name = "entraid" + class Meta: abstract = True verbose_name = _("Entra ID OAuth Source") @@ -274,6 +300,8 @@ class EntraIDOAuthSource(CreatableType, OAuthSource): class OpenIDConnectOAuthSource(CreatableType, OAuthSource): """Login using a Generic OpenID-Connect compliant provider.""" + default_icon_name = "openidconnect" + class Meta: abstract = True verbose_name = _("OpenID OAuth Source") @@ -283,6 +311,8 @@ class OpenIDConnectOAuthSource(CreatableType, OAuthSource): class AppleOAuthSource(CreatableType, OAuthSource): """Social Login using Apple.""" + default_icon_name = "apple" + class Meta: abstract = True verbose_name = _("Apple OAuth Source") @@ -292,6 +322,8 @@ class AppleOAuthSource(CreatableType, OAuthSource): class OktaOAuthSource(CreatableType, OAuthSource): """Social Login using Okta.""" + default_icon_name = "okta" + class Meta: abstract = True verbose_name = _("Okta OAuth Source") @@ -301,6 +333,8 @@ class OktaOAuthSource(CreatableType, OAuthSource): class RedditOAuthSource(CreatableType, OAuthSource): """Social Login using reddit.com.""" + default_icon_name = "reddit" + class Meta: abstract = True verbose_name = _("Reddit OAuth Source") @@ -310,6 +344,8 @@ class RedditOAuthSource(CreatableType, OAuthSource): class WeChatOAuthSource(CreatableType, OAuthSource): """Social Login using WeChat.""" + default_icon_name = "wechat" + class Meta: abstract = True verbose_name = _("WeChat OAuth Source") diff --git a/authentik/sources/oauth/types/registry.py b/authentik/sources/oauth/types/registry.py index d501ffb2e1..c47143bb57 100644 --- a/authentik/sources/oauth/types/registry.py +++ b/authentik/sources/oauth/types/registry.py @@ -5,7 +5,6 @@ from enum import Enum from typing import Any from django.http.request import HttpRequest -from django.templatetags.static import static from django.urls.base import reverse from structlog.stdlib import get_logger @@ -46,10 +45,6 @@ class SourceType: AuthorizationCodeAuthMethod.BASIC_AUTH ) - def icon_url(self) -> str: - """Get Icon URL for login""" - return static(f"authentik/sources/{self.name}.svg") - def login_challenge(self, source: OAuthSource, request: HttpRequest) -> Challenge: """Allow types to return custom challenges""" return RedirectChallenge( diff --git a/authentik/sources/plex/models.py b/authentik/sources/plex/models.py index 660ffff3f6..d156d64a5f 100644 --- a/authentik/sources/plex/models.py +++ b/authentik/sources/plex/models.py @@ -5,7 +5,6 @@ from typing import Any from django.contrib.postgres.fields import ArrayField from django.db import models from django.http.request import HttpRequest -from django.templatetags.static import static from django.utils.translation import gettext_lazy as _ from rest_framework.fields import CharField from rest_framework.serializers import BaseSerializer, Serializer @@ -100,12 +99,7 @@ class PlexSource(ScheduledModel, Source): "name": group_id, } - @property - def icon_url(self) -> str: - icon = super().icon_url - if not icon: - icon = static("authentik/sources/plex.svg") - return icon + default_icon_name = "plex" def ui_login_button(self, request: HttpRequest) -> UILoginButton: return UILoginButton( @@ -116,7 +110,7 @@ class PlexSource(ScheduledModel, Source): "slug": self.slug, } ), - icon_url=self.get_icon_url(request, use_cache=False) or self.icon_url, + icon_url=self.icon_dynamic_url, name=self.name, promoted=self.promoted, ) @@ -127,7 +121,7 @@ class PlexSource(ScheduledModel, Source): "title": self.name, "component": "ak-user-settings-source-plex", "configure_url": self.client_id, - "icon_url": self.icon_url, + "icon_url": self.icon_dynamic_url, } ) diff --git a/authentik/sources/saml/models.py b/authentik/sources/saml/models.py index 614b7d1069..86f4c6cd6c 100644 --- a/authentik/sources/saml/models.py +++ b/authentik/sources/saml/models.py @@ -4,7 +4,6 @@ from typing import Any from django.db import models from django.http import HttpRequest -from django.templatetags.static import static from django.urls import reverse from django.utils.translation import gettext_lazy as _ from lxml.etree import _Element # nosec @@ -266,12 +265,7 @@ class SAMLSource(Source): reverse(f"authentik_sources_saml:{view}", kwargs={"source_slug": self.slug}) ) - @property - def icon_url(self) -> str: - icon = super().icon_url - if not icon: - return static("authentik/sources/saml.png") - return icon + default_icon_name = "saml" def ui_login_button(self, request: HttpRequest) -> UILoginButton: return UILoginButton( @@ -284,7 +278,7 @@ class SAMLSource(Source): } ), name=self.name, - icon_url=self.get_icon_url(request, use_cache=False) or self.icon_url, + icon_url=self.icon_dynamic_url, promoted=self.promoted, ) @@ -298,6 +292,7 @@ class SAMLSource(Source): kwargs={"source_slug": self.slug}, ), "icon_url": self.icon_url, + "icon_themed_urls": self.icon_themed_urls, } ) diff --git a/authentik/sources/scim/models.py b/authentik/sources/scim/models.py index db57185443..49269dc975 100644 --- a/authentik/sources/scim/models.py +++ b/authentik/sources/scim/models.py @@ -4,7 +4,6 @@ from typing import Any from uuid import uuid4 from django.db import models -from django.templatetags.static import static from django.utils.translation import gettext_lazy as _ from rest_framework.serializers import BaseSerializer, Serializer @@ -27,9 +26,7 @@ class SCIMSource(Source): """Return component used to edit this object""" return "ak-source-scim-form" - @property - def icon_url(self) -> str: - return static("authentik/sources/scim.png") + default_icon_name = "scim" @property def serializer(self) -> BaseSerializer: diff --git a/authentik/sources/telegram/models.py b/authentik/sources/telegram/models.py index f7ad5c7247..f36bd51212 100644 --- a/authentik/sources/telegram/models.py +++ b/authentik/sources/telegram/models.py @@ -5,7 +5,6 @@ from urllib.parse import urlencode from django.db import models from django.http import HttpRequest -from django.templatetags.static import static from django.urls import reverse from django.utils.translation import gettext_lazy as _ from rest_framework.serializers import BaseSerializer, Serializer @@ -42,12 +41,7 @@ class TelegramSource(Source): def component(self) -> str: return "ak-source-telegram-form" - @property - def icon_url(self) -> str | None: - icon = super().icon_url - if not icon: - icon = static("authentik/sources/telegram.svg") - return icon + default_icon_name = "telegram" @property def serializer(self) -> type[BaseSerializer]: @@ -66,7 +60,7 @@ class TelegramSource(Source): } ), name=self.name, - icon_url=self.get_icon_url(request, use_cache=False) or self.icon_url, + icon_url=self.icon_dynamic_url, promoted=self.promoted, ) @@ -75,7 +69,7 @@ class TelegramSource(Source): data={ "title": self.name, "component": "ak-user-settings-source-telegram", - "icon_url": self.icon_url, + "icon_url": self.icon_dynamic_url, "configure_url": urlencode( { "bot_username": self.bot_username, diff --git a/authentik/stages/identification/stage.py b/authentik/stages/identification/stage.py index 28780d278c..5df5039d0c 100644 --- a/authentik/stages/identification/stage.py +++ b/authentik/stages/identification/stage.py @@ -14,7 +14,7 @@ from rest_framework.fields import BooleanField, CharField, ChoiceField, DictFiel from rest_framework.serializers import ValidationError from sentry_sdk import start_span -from authentik.core.api.utils import JSONDictField, PassiveSerializer +from authentik.core.api.utils import JSONDictField, PassiveSerializer, ThemedUrlsSerializer from authentik.core.models import Application, Source, User from authentik.endpoints.connectors.agent.stage import PLAN_CONTEXT_DEVICE_AUTH_TOKEN from authentik.endpoints.models import Device @@ -85,6 +85,7 @@ class LoginSourceSerializer(PassiveSerializer): name = CharField() icon_url = CharField(required=False, allow_null=True) + icon_themed_urls = ThemedUrlsSerializer(required=False, allow_null=True) promoted = BooleanField(default=False) challenge = ChallengeDictWrapper() diff --git a/authentik/stages/identification/tests.py b/authentik/stages/identification/tests.py index 18096a1a50..e93e58df67 100644 --- a/authentik/stages/identification/tests.py +++ b/authentik/stages/identification/tests.py @@ -206,7 +206,8 @@ class TestIdentificationStage(FlowTestCase): "component": "xak-flow-redirect", "to": f"/source/oauth/login/{self.source.slug}/", }, - "icon_url": "/static/authentik/sources/default.svg", + "icon_url": None, + "icon_themed_urls": None, "name": self.source.name, "promoted": False, } @@ -242,7 +243,8 @@ class TestIdentificationStage(FlowTestCase): "component": "xak-flow-redirect", "to": f"/source/oauth/login/{self.source.slug}/", }, - "icon_url": "/static/authentik/sources/default.svg", + "icon_url": None, + "icon_themed_urls": None, "name": self.source.name, "promoted": False, } @@ -317,7 +319,8 @@ class TestIdentificationStage(FlowTestCase): "component": "xak-flow-redirect", "to": f"/source/oauth/login/{self.source.slug}/", }, - "icon_url": "/static/authentik/sources/default.svg", + "icon_url": None, + "icon_themed_urls": None, "name": self.source.name, "promoted": False, } @@ -373,7 +376,8 @@ class TestIdentificationStage(FlowTestCase): "component": "xak-flow-redirect", "to": f"/source/oauth/login/{self.source.slug}/", }, - "icon_url": "/static/authentik/sources/default.svg", + "icon_url": None, + "icon_themed_urls": None, "name": self.source.name, "promoted": False, } @@ -436,7 +440,8 @@ class TestIdentificationStage(FlowTestCase): "component": "xak-flow-redirect", "to": f"/source/oauth/login/{self.source.slug}/", }, - "icon_url": "/static/authentik/sources/default.svg", + "icon_url": None, + "icon_themed_urls": None, "name": self.source.name, "promoted": False, } @@ -481,7 +486,8 @@ class TestIdentificationStage(FlowTestCase): primary_action="Log in", sources=[ { - "icon_url": "/static/authentik/sources/default.svg", + "icon_url": None, + "icon_themed_urls": None, "name": self.source.name, "challenge": { "component": "xak-flow-redirect", @@ -523,7 +529,8 @@ class TestIdentificationStage(FlowTestCase): "component": "xak-flow-redirect", "to": f"/source/oauth/login/{self.source.slug}/", }, - "icon_url": "/static/authentik/sources/default.svg", + "icon_url": None, + "icon_themed_urls": None, "name": self.source.name, "promoted": False, } @@ -551,7 +558,8 @@ class TestIdentificationStage(FlowTestCase): "component": "xak-flow-redirect", "to": f"/source/oauth/login/{self.source.slug}/", }, - "icon_url": "/static/authentik/sources/default.svg", + "icon_url": None, + "icon_themed_urls": None, "name": self.source.name, "promoted": False, } diff --git a/schema.yml b/schema.yml index f5f27ba053..c6a451ba37 100644 --- a/schema.yml +++ b/schema.yml @@ -40860,6 +40860,9 @@ components: type: string icon_url: type: string + nullable: true + description: Get the URL to the source icon. Only returns user-configured + icons. readOnly: true icon_themed_urls: allOf: @@ -41550,6 +41553,9 @@ components: type: string icon_url: type: string + nullable: true + description: Get the URL to the source icon. Only returns user-configured + icons. readOnly: true icon_themed_urls: allOf: @@ -42353,6 +42359,10 @@ components: icon_url: type: string nullable: true + icon_themed_urls: + allOf: + - $ref: '#/components/schemas/ThemedUrls' + nullable: true promoted: type: boolean default: false @@ -43755,6 +43765,8 @@ components: icon_url: type: string nullable: true + description: Get the URL to the source icon. Only returns user-configured + icons. readOnly: true icon_themed_urls: allOf: @@ -51112,6 +51124,9 @@ components: type: string icon_url: type: string + nullable: true + description: Get the URL to the source icon. Only returns user-configured + icons. readOnly: true icon_themed_urls: allOf: @@ -53781,6 +53796,9 @@ components: type: string icon_url: type: string + nullable: true + description: Get the URL to the source icon. Only returns user-configured + icons. readOnly: true icon_themed_urls: allOf: @@ -55436,7 +55454,8 @@ components: icon_url: type: string nullable: true - description: Get the URL to the source icon + description: Get the URL to the source icon. Only returns user-configured + icons. readOnly: true icon_themed_urls: allOf: @@ -56118,6 +56137,8 @@ components: icon_url: type: string nullable: true + description: Get the URL to the source icon. Only returns user-configured + icons. readOnly: true icon_themed_urls: allOf: @@ -56580,6 +56601,11 @@ components: type: string icon_url: type: string + nullable: true + icon_themed_urls: + allOf: + - $ref: '#/components/schemas/ThemedUrls' + nullable: true requires_enterprise: type: boolean default: false @@ -57609,6 +57635,11 @@ components: type: string icon_url: type: string + nullable: true + icon_themed_urls: + allOf: + - $ref: '#/components/schemas/ThemedUrls' + nullable: true required: - component - object_uid diff --git a/web/authentik/sources/apple/dark.svg b/web/authentik/sources/apple/dark.svg new file mode 100644 index 0000000000..6d3e92f769 --- /dev/null +++ b/web/authentik/sources/apple/dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/apple/light.svg b/web/authentik/sources/apple/light.svg new file mode 100644 index 0000000000..f4bcba9d82 --- /dev/null +++ b/web/authentik/sources/apple/light.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/azuread/dark.svg b/web/authentik/sources/azuread/dark.svg new file mode 100644 index 0000000000..767552cd55 --- /dev/null +++ b/web/authentik/sources/azuread/dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/azuread/light.svg b/web/authentik/sources/azuread/light.svg new file mode 100644 index 0000000000..d320198e65 --- /dev/null +++ b/web/authentik/sources/azuread/light.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/discord/dark.svg b/web/authentik/sources/discord/dark.svg new file mode 100644 index 0000000000..3f2c5fb581 --- /dev/null +++ b/web/authentik/sources/discord/dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/discord/light.svg b/web/authentik/sources/discord/light.svg new file mode 100644 index 0000000000..11df26b303 --- /dev/null +++ b/web/authentik/sources/discord/light.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/dropbox/dark.svg b/web/authentik/sources/dropbox/dark.svg new file mode 100644 index 0000000000..73063cb9e8 --- /dev/null +++ b/web/authentik/sources/dropbox/dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/dropbox/light.svg b/web/authentik/sources/dropbox/light.svg new file mode 100644 index 0000000000..db5e739e61 --- /dev/null +++ b/web/authentik/sources/dropbox/light.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/entraid/dark.svg b/web/authentik/sources/entraid/dark.svg new file mode 100644 index 0000000000..767552cd55 --- /dev/null +++ b/web/authentik/sources/entraid/dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/entraid/light.svg b/web/authentik/sources/entraid/light.svg new file mode 100644 index 0000000000..d320198e65 --- /dev/null +++ b/web/authentik/sources/entraid/light.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/facebook/dark.svg b/web/authentik/sources/facebook/dark.svg new file mode 100644 index 0000000000..cee72684f1 --- /dev/null +++ b/web/authentik/sources/facebook/dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/facebook/light.svg b/web/authentik/sources/facebook/light.svg new file mode 100644 index 0000000000..4ebb4b770e --- /dev/null +++ b/web/authentik/sources/facebook/light.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/github/dark.svg b/web/authentik/sources/github/dark.svg new file mode 100644 index 0000000000..50f2fd261e --- /dev/null +++ b/web/authentik/sources/github/dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/github/light.svg b/web/authentik/sources/github/light.svg new file mode 100644 index 0000000000..8b406acd65 --- /dev/null +++ b/web/authentik/sources/github/light.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/gitlab/dark.svg b/web/authentik/sources/gitlab/dark.svg new file mode 100644 index 0000000000..385e4a3560 --- /dev/null +++ b/web/authentik/sources/gitlab/dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/gitlab/light.svg b/web/authentik/sources/gitlab/light.svg new file mode 100644 index 0000000000..15088b77a5 --- /dev/null +++ b/web/authentik/sources/gitlab/light.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/google/dark.svg b/web/authentik/sources/google/dark.svg new file mode 100644 index 0000000000..17d3daca09 --- /dev/null +++ b/web/authentik/sources/google/dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/google/light.svg b/web/authentik/sources/google/light.svg new file mode 100644 index 0000000000..057f1e6721 --- /dev/null +++ b/web/authentik/sources/google/light.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/reddit/dark.svg b/web/authentik/sources/reddit/dark.svg new file mode 100644 index 0000000000..659424f77e --- /dev/null +++ b/web/authentik/sources/reddit/dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/authentik/sources/reddit/light.svg b/web/authentik/sources/reddit/light.svg new file mode 100644 index 0000000000..581a83006d --- /dev/null +++ b/web/authentik/sources/reddit/light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/authentik/sources/saml/dark.svg b/web/authentik/sources/saml/dark.svg new file mode 100644 index 0000000000..730a5f66d2 --- /dev/null +++ b/web/authentik/sources/saml/dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/authentik/sources/saml/light.svg b/web/authentik/sources/saml/light.svg new file mode 100644 index 0000000000..00b2ebaa29 --- /dev/null +++ b/web/authentik/sources/saml/light.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/authentik/sources/scim/dark.svg b/web/authentik/sources/scim/dark.svg new file mode 100644 index 0000000000..128458ff3e --- /dev/null +++ b/web/authentik/sources/scim/dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/authentik/sources/scim/light.svg b/web/authentik/sources/scim/light.svg new file mode 100644 index 0000000000..4ee0dbe366 --- /dev/null +++ b/web/authentik/sources/scim/light.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/authentik/sources/slack/dark.svg b/web/authentik/sources/slack/dark.svg new file mode 100644 index 0000000000..07a9f5d8f9 --- /dev/null +++ b/web/authentik/sources/slack/dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/slack/light.svg b/web/authentik/sources/slack/light.svg new file mode 100644 index 0000000000..9e75176fef --- /dev/null +++ b/web/authentik/sources/slack/light.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/telegram/dark.svg b/web/authentik/sources/telegram/dark.svg new file mode 100644 index 0000000000..d04f454517 --- /dev/null +++ b/web/authentik/sources/telegram/dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/telegram/light.svg b/web/authentik/sources/telegram/light.svg new file mode 100644 index 0000000000..354aebb4b6 --- /dev/null +++ b/web/authentik/sources/telegram/light.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/twitch/dark.svg b/web/authentik/sources/twitch/dark.svg new file mode 100644 index 0000000000..0b31e0d6a2 --- /dev/null +++ b/web/authentik/sources/twitch/dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/authentik/sources/twitch/light.svg b/web/authentik/sources/twitch/light.svg new file mode 100644 index 0000000000..3be4a42624 --- /dev/null +++ b/web/authentik/sources/twitch/light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/authentik/sources/twitter/dark.svg b/web/authentik/sources/twitter/dark.svg new file mode 100644 index 0000000000..f632857642 --- /dev/null +++ b/web/authentik/sources/twitter/dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/twitter/light.svg b/web/authentik/sources/twitter/light.svg new file mode 100644 index 0000000000..947a4f77fe --- /dev/null +++ b/web/authentik/sources/twitter/light.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/wechat/dark.svg b/web/authentik/sources/wechat/dark.svg new file mode 100644 index 0000000000..fd12793c9d --- /dev/null +++ b/web/authentik/sources/wechat/dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/authentik/sources/wechat/light.svg b/web/authentik/sources/wechat/light.svg new file mode 100644 index 0000000000..f109f383ea --- /dev/null +++ b/web/authentik/sources/wechat/light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/authentik/sources/wsfed/dark.svg b/web/authentik/sources/wsfed/dark.svg new file mode 100644 index 0000000000..767552cd55 --- /dev/null +++ b/web/authentik/sources/wsfed/dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/authentik/sources/wsfed/light.svg b/web/authentik/sources/wsfed/light.svg new file mode 100644 index 0000000000..d320198e65 --- /dev/null +++ b/web/authentik/sources/wsfed/light.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/elements/sources/utils.ts b/web/src/elements/sources/utils.ts index a534caf78e..f9ba299b6b 100644 --- a/web/src/elements/sources/utils.ts +++ b/web/src/elements/sources/utils.ts @@ -1,21 +1,41 @@ import { PolicyBindingCheckTarget } from "#common/policies/utils"; +import { ResolvedUITheme } from "#common/theme"; + +import { ThemedUrls } from "@goauthentik/api"; import { msg } from "@lit/localize"; import { html, TemplateResult } from "lit"; -export function renderSourceIcon(name: string, iconUrl: string | undefined | null): TemplateResult { +function resolveSourceIconUrl( + iconUrl: string | undefined | null, + iconThemedUrls: ThemedUrls | undefined | null, + theme: ResolvedUITheme | undefined, +): string | undefined | null { + if (theme && iconThemedUrls?.[theme]) { + return iconThemedUrls[theme]; + } + return iconUrl; +} + +export function renderSourceIcon( + name: string, + iconUrl: string | undefined | null, + iconThemedUrls?: ThemedUrls | null, + theme?: ResolvedUITheme, +): TemplateResult { + const resolvedIconUrl = resolveSourceIconUrl(iconUrl, iconThemedUrls, theme); const icon = html``; - if (iconUrl) { - if (iconUrl.startsWith("fa://")) { - const url = iconUrl.replaceAll("fa://", ""); + if (resolvedIconUrl) { + if (resolvedIconUrl.startsWith("fa://")) { + const url = resolvedIconUrl.replaceAll("fa://", ""); return html``; } - return html`${name}`; + return html`${name}`; } return icon; } diff --git a/web/src/elements/user/sources/SourceSettings.css b/web/src/elements/user/sources/SourceSettings.css index b9f83dd161..79afb11c42 100644 --- a/web/src/elements/user/sources/SourceSettings.css +++ b/web/src/elements/user/sources/SourceSettings.css @@ -24,7 +24,7 @@ padding-inline-start: 0; border-top-color: var(--ak-dark-background-lighter); - .pf-c-data-list__cell img { + .pf-c-data-list__cell i[part="source-icon"] { filter: invert(1); } diff --git a/web/src/elements/user/sources/SourceSettings.ts b/web/src/elements/user/sources/SourceSettings.ts index d6ad712daa..6ababe5aa9 100644 --- a/web/src/elements/user/sources/SourceSettings.ts +++ b/web/src/elements/user/sources/SourceSettings.ts @@ -142,7 +142,6 @@ export class UserSourceSettingsPage extends AKElement { const connection = this.sourceToConnection.get(source); const connectionPk = connection?.pk ?? -1; const connectionUserPk = connection?.user ?? -1; - return html`
  • - ${renderSourceIcon(source.title, source.iconUrl)} ${source.title} + ${renderSourceIcon( + source.title, + source.iconUrl, + source.iconThemedUrls, + this.activeTheme, + )} + ${source.title}
    ${this.renderSourceSettings(source)}
    diff --git a/web/src/elements/wizard/TypeCreateWizardPage.ts b/web/src/elements/wizard/TypeCreateWizardPage.ts index a554ee6649..328a149fba 100644 --- a/web/src/elements/wizard/TypeCreateWizardPage.ts +++ b/web/src/elements/wizard/TypeCreateWizardPage.ts @@ -4,12 +4,13 @@ import "#elements/Alert"; import { WithLicenseSummary } from "#elements/mixins/license"; import { SlottedTemplateResult } from "#elements/types"; import { ifPresent } from "#elements/utils/attributes"; +import { renderDynamicIcon } from "#elements/utils/images"; import { WizardPage } from "#elements/wizard/WizardPage"; import { TypeCreate } from "@goauthentik/api"; import { msg, str } from "@lit/localize"; -import { css, CSSResult, html } from "lit"; +import { css, CSSResult, html, nothing } from "lit"; import { customElement, property } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { guard } from "lit/directives/guard.js"; @@ -57,8 +58,9 @@ export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) { max-height: 2em; min-height: 2em; } - :host([theme="dark"]) .pf-c-card__header-main img { - filter: invert(1); + .pf-c-card__header-main .font-awesome { + font-size: 2em; + line-height: 1; } :host([theme="dark"]) .pf-c-card { @@ -114,9 +116,14 @@ export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) { return this.types.map((type, idx) => { const disabled = !!(type.requiresEnterprise && !this.hasEnterpriseLicense); - const selected = this.selectedType === type; const inputID = `${type.component}-${type.modelName}`; + const icon = renderDynamicIcon({ + urls: type.iconUrl, + theme: this.activeTheme, + alt: msg(str`${type.name} Icon`), + ariaHidden: true, + }); return html`
    { if (disabled) return; - this.selectedType = type; - this.#selectDispatch(type); - }} + this.selectedType = type; + this.#selectDispatch(type); + }} > - ${type.iconUrl + ${icon !== nothing ? html`` - : null} + : nothing}
    ${type.name}
    ${type.description} @@ -162,7 +163,7 @@ export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) { ? html` ` - : null} + : nothing}
    `; }); } diff --git a/web/src/flow/stages/identification/IdentificationStage.stories.ts b/web/src/flow/stages/identification/IdentificationStage.stories.ts index 10d1103eea..68621f4190 100644 --- a/web/src/flow/stages/identification/IdentificationStage.stories.ts +++ b/web/src/flow/stages/identification/IdentificationStage.stories.ts @@ -79,7 +79,10 @@ export const ChallengeEverything = flowFactory("ak-stage-identification", { component: "xak-flow-redirect", to: "foo", }, - iconUrl: "/static/authentik/sources/google.svg", + iconThemedUrls: { + light: "/static/authentik/sources/google/light.svg", + dark: "/static/authentik/sources/google/dark.svg", + }, }, ], recoveryUrl: "foo", diff --git a/web/src/flow/stages/identification/IdentificationStage.ts b/web/src/flow/stages/identification/IdentificationStage.ts index 21d3789b9f..283ae4eee0 100644 --- a/web/src/flow/stages/identification/IdentificationStage.ts +++ b/web/src/flow/stages/identification/IdentificationStage.ts @@ -333,9 +333,9 @@ export class IdentificationStage extends BaseStage< //#region Render protected renderDefaultSource(source: LoginSource, showLabels: boolean) { - const { name, iconUrl, challenge } = source; + const { name, iconUrl, iconThemedUrls, challenge } = source; - const icon = renderSourceIcon(name, iconUrl); + const icon = renderSourceIcon(name, iconUrl, iconThemedUrls, this.activeTheme); return html`