web/admin: rework initial wizard pages and add grid layout (#9668)

* remove @goauthentik/authentik as TS path

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* initial implementation

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* oh yeah

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* format earlier changes

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* support plain alert

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* initial attempt at dedupe

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make it a base class

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* migrate all wizards

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* create type create mixin to dedupe more, add icon to source create

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add ldap icon

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* Optimised images with calibre/image-actions

* match inverting

we should probably replace all icons with coloured ones so we don't need to invert them...I guess

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* format

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make everything more explicit

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add icons to provider

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add remaining provider icons

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* rework to not use inheritance

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix unrelated typo

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make app wizard use grid layout

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* keep wizard height consistent

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
This commit is contained in:
Jens L
2024-05-22 02:41:33 +02:00
committed by GitHub
parent 0ed4bba5a5
commit 6c4c535d57
58 changed files with 726 additions and 791 deletions

View File

@ -0,0 +1,79 @@
"""API Utilities"""
from drf_spectacular.utils import extend_schema
from rest_framework.decorators import action
from rest_framework.fields import (
BooleanField,
CharField,
)
from rest_framework.request import Request
from rest_framework.response import Response
from authentik.core.api.utils import PassiveSerializer
from authentik.enterprise.apps import EnterpriseConfig
from authentik.lib.utils.reflection import all_subclasses
class TypeCreateSerializer(PassiveSerializer):
"""Types of an object that can be created"""
name = CharField(required=True)
description = CharField(required=True)
component = CharField(required=True)
model_name = CharField(required=True)
icon_url = CharField(required=False)
requires_enterprise = BooleanField(default=False)
class CreatableType:
"""Class to inherit from to mark a model as creatable, even if the model itself is marked
as abstract"""
class NonCreatableType:
"""Class to inherit from to mark a model as non-creatable even if it is not abstract"""
class TypesMixin:
"""Mixin which adds an API endpoint to list all possible types that can be created"""
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def types(self, request: Request, additional: list[dict] | None = None) -> Response:
"""Get all creatable types"""
data = []
for subclass in all_subclasses(self.queryset.model):
instance = None
if subclass._meta.abstract:
if not issubclass(subclass, CreatableType):
continue
# Circumvent the django protection for not being able to instantiate
# abstract models. We need a model instance to access .component
# and further down .icon_url
instance = subclass.__new__(subclass)
# Django re-sets abstract = False so we need to override that
instance.Meta.abstract = True
else:
if issubclass(subclass, NonCreatableType):
continue
instance = subclass()
try:
data.append(
{
"name": subclass._meta.verbose_name,
"description": subclass.__doc__,
"component": instance.component,
"model_name": subclass._meta.model_name,
"icon_url": getattr(instance, "icon_url", None),
"requires_enterprise": isinstance(
subclass._meta.app_config, EnterpriseConfig
),
}
)
except NotImplementedError:
continue
if additional:
data.extend(additional)
data = sorted(data, key=lambda x: x["name"])
return Response(TypeCreateSerializer(data, many=True).data)

View File

@ -15,12 +15,15 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet
from authentik.blueprints.api import ManagedSerializer
from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, PassiveSerializer, TypeCreateSerializer
from authentik.core.api.utils import (
MetaNameSerializer,
PassiveSerializer,
)
from authentik.core.expression.evaluator import PropertyMappingEvaluator
from authentik.core.models import PropertyMapping
from authentik.events.utils import sanitize_item
from authentik.lib.utils.reflection import all_subclasses
from authentik.policies.api.exec import PolicyTestSerializer
from authentik.rbac.decorators import permission_required
@ -64,6 +67,7 @@ class PropertyMappingSerializer(ManagedSerializer, ModelSerializer, MetaNameSeri
class PropertyMappingViewSet(
TypesMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
@ -83,23 +87,6 @@ class PropertyMappingViewSet(
def get_queryset(self): # pragma: no cover
return PropertyMapping.objects.select_subclasses()
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def types(self, request: Request) -> Response:
"""Get all creatable property-mapping types"""
data = []
for subclass in all_subclasses(self.queryset.model):
subclass: PropertyMapping
data.append(
{
"name": subclass._meta.verbose_name,
"description": subclass.__doc__,
"component": subclass().component,
"model_name": subclass._meta.model_name,
}
)
return Response(TypeCreateSerializer(data, many=True).data)
@permission_required("authentik_core.view_propertymapping")
@extend_schema(
request=PolicyTestSerializer(),

View File

@ -5,20 +5,15 @@ from django.db.models.query import Q
from django.utils.translation import gettext_lazy as _
from django_filters.filters import BooleanFilter
from django_filters.filterset import FilterSet
from drf_spectacular.utils import extend_schema
from rest_framework import mixins
from rest_framework.decorators import action
from rest_framework.fields import ReadOnlyField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.core.api.utils import MetaNameSerializer
from authentik.core.models import Provider
from authentik.enterprise.apps import EnterpriseConfig
from authentik.lib.utils.reflection import all_subclasses
class ProviderSerializer(ModelSerializer, MetaNameSerializer):
@ -86,6 +81,7 @@ class ProviderFilter(FilterSet):
class ProviderViewSet(
TypesMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
@ -104,31 +100,3 @@ class ProviderViewSet(
def get_queryset(self): # pragma: no cover
return Provider.objects.select_subclasses()
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def types(self, request: Request) -> Response:
"""Get all creatable provider types"""
data = []
for subclass in all_subclasses(self.queryset.model):
subclass: Provider
if subclass._meta.abstract:
continue
data.append(
{
"name": subclass._meta.verbose_name,
"description": subclass.__doc__,
"component": subclass().component,
"model_name": subclass._meta.model_name,
"requires_enterprise": isinstance(subclass._meta.app_config, EnterpriseConfig),
}
)
data.append(
{
"name": _("SAML Provider from Metadata"),
"description": _("Create a SAML Provider by importing its Metadata."),
"component": "ak-provider-saml-import-form",
"model_name": "",
}
)
return Response(TypeCreateSerializer(data, many=True).data)

View File

@ -17,8 +17,9 @@ from structlog.stdlib import get_logger
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.core.api.utils import MetaNameSerializer
from authentik.core.models import Source, UserSourceConnection
from authentik.core.types import UserSettingSerializer
from authentik.lib.utils.file import (
@ -27,7 +28,6 @@ from authentik.lib.utils.file import (
set_file,
set_file_url,
)
from authentik.lib.utils.reflection import all_subclasses
from authentik.policies.engine import PolicyEngine
from authentik.rbac.decorators import permission_required
@ -74,6 +74,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
class SourceViewSet(
TypesMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
@ -132,30 +133,6 @@ class SourceViewSet(
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:
"""Get all creatable source types"""
data = []
for subclass in all_subclasses(self.queryset.model):
subclass: Source
component = ""
if len(subclass.__subclasses__()) > 0:
continue
if subclass._meta.abstract:
component = subclass.__bases__[0]().component
else:
component = subclass().component
data.append(
{
"name": subclass._meta.verbose_name,
"description": subclass.__doc__,
"component": component,
"model_name": subclass._meta.model_name,
}
)
return Response(TypeCreateSerializer(data, many=True).data)
@extend_schema(responses={200: UserSettingSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def user_settings(self, request: Request) -> Response:

View File

@ -6,8 +6,16 @@ from django.db.models import Model
from drf_spectacular.extensions import OpenApiSerializerFieldExtension
from drf_spectacular.plumbing import build_basic_type
from drf_spectacular.types import OpenApiTypes
from rest_framework.fields import BooleanField, CharField, IntegerField, JSONField
from rest_framework.serializers import Serializer, SerializerMethodField, ValidationError
from rest_framework.fields import (
CharField,
IntegerField,
JSONField,
SerializerMethodField,
)
from rest_framework.serializers import (
Serializer,
ValidationError,
)
def is_dict(value: Any):
@ -68,16 +76,6 @@ class MetaNameSerializer(PassiveSerializer):
return f"{obj._meta.app_label}.{obj._meta.model_name}"
class TypeCreateSerializer(PassiveSerializer):
"""Types of an object that can be created"""
name = CharField(required=True)
description = CharField(required=True)
component = CharField(required=True)
model_name = CharField(required=True)
requires_enterprise = BooleanField(default=False)
class CacheSerializer(PassiveSerializer):
"""Generic cache stats for an object"""

View File

@ -377,6 +377,10 @@ class Provider(SerializerModel):
Can return None for providers that are not URL-based"""
return None
@property
def icon_url(self) -> str | None:
return None
@property
def component(self) -> str:
"""Return component used to edit this object"""

View File

@ -9,7 +9,6 @@ from freezegun import freeze_time
from guardian.shortcuts import get_anonymous_user
from authentik.core.models import Provider, Source, Token
from authentik.flows.models import Stage
from authentik.lib.utils.reflection import all_subclasses
@ -31,7 +30,7 @@ class TestModels(TestCase):
self.assertFalse(token.is_expired)
def source_tester_factory(test_model: type[Stage]) -> Callable:
def source_tester_factory(test_model: type[Source]) -> Callable:
"""Test source"""
factory = RequestFactory()
@ -39,19 +38,19 @@ def source_tester_factory(test_model: type[Stage]) -> Callable:
def tester(self: TestModels):
model_class = None
if test_model._meta.abstract: # pragma: no cover
model_class = test_model.__bases__[0]()
if test_model._meta.abstract:
model_class = [x for x in test_model.__bases__ if issubclass(x, Source)][0]()
else:
model_class = test_model()
model_class.slug = "test"
self.assertIsNotNone(model_class.component)
_ = model_class.ui_login_button(request)
_ = model_class.ui_user_settings()
model_class.ui_login_button(request)
model_class.ui_user_settings()
return tester
def provider_tester_factory(test_model: type[Stage]) -> Callable:
def provider_tester_factory(test_model: type[Provider]) -> Callable:
"""Test provider"""
def tester(self: TestModels):

View File

@ -5,6 +5,7 @@ from uuid import uuid4
from django.db import models
from django.db.models import QuerySet
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _
from google.oauth2.service_account import Credentials
from rest_framework.serializers import Serializer
@ -98,6 +99,10 @@ class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider):
).with_subject(self.delegated_subject),
}
@property
def icon_url(self) -> str | None:
return static("authentik/sources/google.svg")
@property
def component(self) -> str:
return "ak-provider-google-workspace-form"

View File

@ -6,6 +6,7 @@ from uuid import uuid4
from azure.identity.aio import ClientSecretCredential
from django.db import models
from django.db.models import QuerySet
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
@ -87,6 +88,10 @@ class MicrosoftEntraProvider(OutgoingSyncProvider, BackchannelProvider):
)
}
@property
def icon_url(self) -> str | None:
return static("authentik/sources/azuread.svg")
@property
def component(self) -> str:
return "ak-provider-microsoft-entra-form"

View File

@ -7,6 +7,7 @@ from deepmerge import always_merger
from django.db import models
from django.db.models import QuerySet
from django.http import HttpRequest
from django.templatetags.static import static
from django.utils.translation import gettext as _
from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
@ -63,6 +64,10 @@ class RACProvider(Provider):
Can return None for providers that are not URL-based"""
return "goauthentik.io://providers/rac/launch"
@property
def icon_url(self) -> str | None:
return static("authentik/sources/rac.svg")
@property
def component(self) -> str:
return "ak-provider-rac-form"

View File

@ -19,7 +19,8 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.admin.api.metrics import CoordinateSerializer
from authentik.core.api.utils import PassiveSerializer, TypeCreateSerializer
from authentik.core.api.object_types import TypeCreateSerializer
from authentik.core.api.utils import PassiveSerializer
from authentik.events.models import Event, EventAction

View File

@ -10,10 +10,10 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger
from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.core.api.utils import MetaNameSerializer
from authentik.core.types import UserSettingSerializer
from authentik.enterprise.apps import EnterpriseConfig
from authentik.flows.api.flows import FlowSetSerializer
from authentik.flows.models import ConfigurableStage, Stage
from authentik.lib.utils.reflection import all_subclasses
@ -47,6 +47,7 @@ class StageSerializer(ModelSerializer, MetaNameSerializer):
class StageViewSet(
TypesMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
@ -63,25 +64,6 @@ class StageViewSet(
def get_queryset(self): # pragma: no cover
return Stage.objects.select_subclasses().prefetch_related("flow_set")
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def types(self, request: Request) -> Response:
"""Get all creatable stage types"""
data = []
for subclass in all_subclasses(self.queryset.model, False):
subclass: Stage
data.append(
{
"name": subclass._meta.verbose_name,
"description": subclass.__doc__,
"component": subclass().component,
"model_name": subclass._meta.model_name,
"requires_enterprise": isinstance(subclass._meta.app_config, EnterpriseConfig),
}
)
data = sorted(data, key=lambda x: x["name"])
return Response(TypeCreateSerializer(data, many=True).data)
@extend_schema(responses={200: UserSettingSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def user_settings(self, request: Request) -> Response:

View File

@ -12,7 +12,7 @@ from authentik.lib.config import CONFIG
SERVICE_HOST_ENV_NAME = "KUBERNETES_SERVICE_HOST"
def all_subclasses(cls, sort=True):
def all_subclasses[T](cls: T, sort=True) -> list[T] | set[T]:
"""Recursively return all subclassess of cls"""
classes = set(cls.__subclasses__()).union(
[s for c in cls.__subclasses__() for s in all_subclasses(c, sort=sort)]

View File

@ -15,9 +15,12 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, PassiveSerializer, TypeCreateSerializer
from authentik.lib.utils.reflection import all_subclasses
from authentik.core.api.utils import (
MetaNameSerializer,
PassiveSerializer,
)
from authentik.outposts.models import (
DockerServiceConnection,
KubernetesServiceConnection,
@ -57,6 +60,7 @@ class ServiceConnectionStateSerializer(PassiveSerializer):
class ServiceConnectionViewSet(
TypesMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
@ -70,23 +74,6 @@ class ServiceConnectionViewSet(
search_fields = ["name"]
filterset_fields = ["name"]
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def types(self, request: Request) -> Response:
"""Get all creatable service connection types"""
data = []
for subclass in all_subclasses(self.queryset.model):
subclass: OutpostServiceConnection
data.append(
{
"name": subclass._meta.verbose_name,
"description": subclass.__doc__,
"component": subclass().component,
"model_name": subclass._meta.model_name,
}
)
return Response(TypeCreateSerializer(data, many=True).data)
@extend_schema(responses={200: ServiceConnectionStateSerializer(many=False)})
@action(detail=True, pagination_class=None, filter_backends=[])
def state(self, request: Request, pk: str) -> Response:

View File

@ -13,10 +13,13 @@ from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger
from authentik.core.api.applications import user_app_cache_key
from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import CacheSerializer, MetaNameSerializer, TypeCreateSerializer
from authentik.core.api.utils import (
CacheSerializer,
MetaNameSerializer,
)
from authentik.events.logs import LogEventSerializer, capture_logs
from authentik.lib.utils.reflection import all_subclasses
from authentik.policies.api.exec import PolicyTestResultSerializer, PolicyTestSerializer
from authentik.policies.models import Policy, PolicyBinding
from authentik.policies.process import PolicyProcess
@ -69,6 +72,7 @@ class PolicySerializer(ModelSerializer, MetaNameSerializer):
class PolicyViewSet(
TypesMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
@ -89,23 +93,6 @@ class PolicyViewSet(
def get_queryset(self): # pragma: no cover
return Policy.objects.select_subclasses().prefetch_related("bindings", "promptstage_set")
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def types(self, request: Request) -> Response:
"""Get all creatable policy types"""
data = []
for subclass in all_subclasses(self.queryset.model):
subclass: Policy
data.append(
{
"name": subclass._meta.verbose_name,
"description": subclass.__doc__,
"component": subclass().component,
"model_name": subclass._meta.model_name,
}
)
return Response(TypeCreateSerializer(data, many=True).data)
@permission_required(None, ["authentik_policies.view_policy_cache"])
@extend_schema(responses={200: CacheSerializer(many=False)})
@action(detail=False, pagination_class=None, filter_backends=[])

View File

@ -3,6 +3,7 @@
from collections.abc import Iterable
from django.db import models
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
@ -90,6 +91,10 @@ class LDAPProvider(OutpostModel, BackchannelProvider):
def component(self) -> str:
return "ak-provider-ldap-form"
@property
def icon_url(self) -> str | None:
return static("authentik/sources/ldap.png")
@property
def serializer(self) -> type[Serializer]:
from authentik.providers.ldap.api import LDAPProviderSerializer

View File

@ -15,6 +15,7 @@ from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
from dacite.core import from_dict
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 jwt import encode
@ -262,6 +263,10 @@ class OAuth2Provider(Provider):
LOGGER.warning("Failed to format launch url", exc=exc)
return None
@property
def icon_url(self) -> str | None:
return static("authentik/sources/openidconnect.svg")
@property
def component(self) -> str:
return "ak-provider-oauth2-form"

View File

@ -6,6 +6,7 @@ from random import SystemRandom
from urllib.parse import urljoin
from django.db import models
from django.templatetags.static import static
from django.utils.translation import gettext as _
from rest_framework.serializers import Serializer
@ -115,6 +116,10 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
def component(self) -> str:
return "ak-provider-proxy-form"
@property
def icon_url(self) -> str | None:
return static("authentik/sources/proxy.svg")
@property
def serializer(self) -> type[Serializer]:
from authentik.providers.proxy.api import ProxyProviderSerializer

View File

@ -1,6 +1,7 @@
"""Radius Provider"""
from django.db import models
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
@ -46,6 +47,10 @@ class RadiusProvider(OutpostModel, Provider):
def component(self) -> str:
return "ak-provider-radius-form"
@property
def icon_url(self) -> str | None:
return static("authentik/sources/radius.svg")
@property
def serializer(self) -> type[Serializer]:
from authentik.providers.radius.api import RadiusProviderSerializer

View File

@ -1,11 +1,13 @@
"""authentik saml_idp Models"""
from django.db import models
from django.templatetags.static import static
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
from authentik.core.api.object_types import CreatableType
from authentik.core.models import PropertyMapping, Provider
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.utils.time import timedelta_string_validator
@ -159,6 +161,10 @@ class SAMLProvider(Provider):
except Provider.application.RelatedObjectDoesNotExist:
return None
@property
def icon_url(self) -> str | None:
return static("authentik/sources/saml.png")
@property
def serializer(self) -> type[Serializer]:
from authentik.providers.saml.api.providers import SAMLProviderSerializer
@ -200,3 +206,20 @@ class SAMLPropertyMapping(PropertyMapping):
class Meta:
verbose_name = _("SAML Property Mapping")
verbose_name_plural = _("SAML Property Mappings")
class SAMLProviderImportModel(CreatableType, Provider):
"""Create a SAML Provider by importing its Metadata."""
@property
def component(self):
return "ak-provider-saml-import-form"
@property
def icon_url(self) -> str | None:
return static("authentik/sources/saml.png")
class Meta:
abstract = True
verbose_name = _("SAML Provider from Metadata")
verbose_name_plural = _("SAML Providers from Metadata")

View File

@ -5,6 +5,7 @@ from uuid import uuid4
from django.db import models
from django.db.models import QuerySet
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
@ -32,6 +33,10 @@ class SCIMProvider(OutgoingSyncProvider, BackchannelProvider):
help_text=_("Property mappings used for group creation/updating."),
)
@property
def icon_url(self) -> str | None:
return static("authentik/sources/scim.png")
def client_for_model(
self, model: type[User | Group]
) -> BaseOutgoingSyncClient[User | Group, Any, Any, Self]:

View File

@ -8,6 +8,7 @@ from tempfile import NamedTemporaryFile, mkdtemp
from django.core.cache import cache
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
@ -126,6 +127,10 @@ class LDAPSource(Source):
return LDAPSourceSerializer
@property
def icon_url(self) -> str:
return static("authentik/sources/ldap.png")
def server(self, **kwargs) -> ServerPool:
"""Get LDAP Server/ServerPool"""
servers = []

View File

@ -8,6 +8,7 @@ from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
from authentik.core.api.object_types import CreatableType, NonCreatableType
from authentik.core.models import Source, UserSourceConnection
from authentik.core.types import UILoginButton, UserSettingSerializer
@ -15,7 +16,7 @@ if TYPE_CHECKING:
from authentik.sources.oauth.types.registry import SourceType
class OAuthSource(Source):
class OAuthSource(NonCreatableType, Source):
"""Login using a Generic OAuth provider."""
provider_type = models.CharField(max_length=255)
@ -72,23 +73,35 @@ class OAuthSource(Source):
return OAuthSourceSerializer
@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
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
def ui_login_button(self, request: HttpRequest) -> UILoginButton:
provider_type = self.source_type
provider = provider_type()
icon = self.icon_url
if not icon:
icon = provider.icon_url()
return UILoginButton(
name=self.name,
challenge=provider.login_challenge(self, request),
icon_url=icon,
icon_url=self.icon_url,
)
def ui_user_settings(self) -> UserSettingSerializer | None:
provider_type = self.source_type
icon = self.icon_url
if not icon:
icon = provider_type().icon_url()
return UserSettingSerializer(
data={
"title": self.name,
@ -97,7 +110,7 @@ class OAuthSource(Source):
"authentik_sources_oauth:oauth-client-login",
kwargs={"source_slug": self.slug},
),
"icon_url": icon,
"icon_url": self.icon_url,
}
)
@ -109,7 +122,7 @@ class OAuthSource(Source):
verbose_name_plural = _("OAuth Sources")
class GitHubOAuthSource(OAuthSource):
class GitHubOAuthSource(CreatableType, OAuthSource):
"""Social Login using GitHub.com or a GitHub-Enterprise Instance."""
class Meta:
@ -118,7 +131,7 @@ class GitHubOAuthSource(OAuthSource):
verbose_name_plural = _("GitHub OAuth Sources")
class GitLabOAuthSource(OAuthSource):
class GitLabOAuthSource(CreatableType, OAuthSource):
"""Social Login using GitLab.com or a GitLab Instance."""
class Meta:
@ -127,7 +140,7 @@ class GitLabOAuthSource(OAuthSource):
verbose_name_plural = _("GitLab OAuth Sources")
class TwitchOAuthSource(OAuthSource):
class TwitchOAuthSource(CreatableType, OAuthSource):
"""Social Login using Twitch."""
class Meta:
@ -136,7 +149,7 @@ class TwitchOAuthSource(OAuthSource):
verbose_name_plural = _("Twitch OAuth Sources")
class MailcowOAuthSource(OAuthSource):
class MailcowOAuthSource(CreatableType, OAuthSource):
"""Social Login using Mailcow."""
class Meta:
@ -145,7 +158,7 @@ class MailcowOAuthSource(OAuthSource):
verbose_name_plural = _("Mailcow OAuth Sources")
class TwitterOAuthSource(OAuthSource):
class TwitterOAuthSource(CreatableType, OAuthSource):
"""Social Login using Twitter.com"""
class Meta:
@ -154,7 +167,7 @@ class TwitterOAuthSource(OAuthSource):
verbose_name_plural = _("Twitter OAuth Sources")
class FacebookOAuthSource(OAuthSource):
class FacebookOAuthSource(CreatableType, OAuthSource):
"""Social Login using Facebook.com."""
class Meta:
@ -163,7 +176,7 @@ class FacebookOAuthSource(OAuthSource):
verbose_name_plural = _("Facebook OAuth Sources")
class DiscordOAuthSource(OAuthSource):
class DiscordOAuthSource(CreatableType, OAuthSource):
"""Social Login using Discord."""
class Meta:
@ -172,7 +185,7 @@ class DiscordOAuthSource(OAuthSource):
verbose_name_plural = _("Discord OAuth Sources")
class PatreonOAuthSource(OAuthSource):
class PatreonOAuthSource(CreatableType, OAuthSource):
"""Social Login using Patreon."""
class Meta:
@ -181,7 +194,7 @@ class PatreonOAuthSource(OAuthSource):
verbose_name_plural = _("Patreon OAuth Sources")
class GoogleOAuthSource(OAuthSource):
class GoogleOAuthSource(CreatableType, OAuthSource):
"""Social Login using Google or Google Workspace (GSuite)."""
class Meta:
@ -190,7 +203,7 @@ class GoogleOAuthSource(OAuthSource):
verbose_name_plural = _("Google OAuth Sources")
class AzureADOAuthSource(OAuthSource):
class AzureADOAuthSource(CreatableType, OAuthSource):
"""Social Login using Azure AD."""
class Meta:
@ -199,7 +212,7 @@ class AzureADOAuthSource(OAuthSource):
verbose_name_plural = _("Azure AD OAuth Sources")
class OpenIDConnectOAuthSource(OAuthSource):
class OpenIDConnectOAuthSource(CreatableType, OAuthSource):
"""Login using a Generic OpenID-Connect compliant provider."""
class Meta:
@ -208,7 +221,7 @@ class OpenIDConnectOAuthSource(OAuthSource):
verbose_name_plural = _("OpenID OAuth Sources")
class AppleOAuthSource(OAuthSource):
class AppleOAuthSource(CreatableType, OAuthSource):
"""Social Login using Apple."""
class Meta:
@ -217,7 +230,7 @@ class AppleOAuthSource(OAuthSource):
verbose_name_plural = _("Apple OAuth Sources")
class OktaOAuthSource(OAuthSource):
class OktaOAuthSource(CreatableType, OAuthSource):
"""Social Login using Okta."""
class Meta:
@ -226,7 +239,7 @@ class OktaOAuthSource(OAuthSource):
verbose_name_plural = _("Okta OAuth Sources")
class RedditOAuthSource(OAuthSource):
class RedditOAuthSource(CreatableType, OAuthSource):
"""Social Login using reddit.com."""
class Meta:

View File

@ -60,10 +60,14 @@ class PlexSource(Source):
return PlexSourceSerializer
def ui_login_button(self, request: HttpRequest) -> UILoginButton:
icon = self.icon_url
@property
def icon_url(self) -> str:
icon = super().icon_url
if not icon:
icon = static("authentik/sources/plex.svg")
return icon
def ui_login_button(self, request: HttpRequest) -> UILoginButton:
return UILoginButton(
challenge=PlexAuthenticationChallenge(
data={
@ -73,20 +77,17 @@ class PlexSource(Source):
"slug": self.slug,
}
),
icon_url=icon,
icon_url=self.icon_url,
name=self.name,
)
def ui_user_settings(self) -> UserSettingSerializer | None:
icon = self.icon_url
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": icon,
"icon_url": self.icon_url,
}
)

View File

@ -181,6 +181,13 @@ class SAMLSource(Source):
return SAMLSourceSerializer
@property
def icon_url(self) -> str:
icon = super().icon_url
if not icon:
return static("authentik/sources/saml.png")
return icon
def get_issuer(self, request: HttpRequest) -> str:
"""Get Source's Issuer, falling back to our Metadata URL if none is set"""
if self.issuer is None:
@ -209,9 +216,6 @@ class SAMLSource(Source):
)
def ui_user_settings(self) -> UserSettingSerializer | None:
icon = self.icon_url
if not icon:
icon = static(f"authentik/sources/{self.slug}.svg")
return UserSettingSerializer(
data={
"title": self.name,
@ -220,7 +224,7 @@ class SAMLSource(Source):
"authentik_sources_saml:login",
kwargs={"source_slug": self.slug},
),
"icon_url": icon,
"icon_url": self.icon_url,
}
)

View File

@ -3,6 +3,7 @@
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
@ -27,6 +28,10 @@ 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")
@property
def serializer(self) -> BaseSerializer:
from authentik.sources.scim.api.sources import SCIMSourceSerializer

View File

@ -7,8 +7,8 @@ from rest_framework.response import Response
from rest_framework.serializers import ValidationError
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.object_types import TypeCreateSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import TypeCreateSerializer
from authentik.flows.api.stages import StageSerializer
from authentik.stages.email.models import EmailStage, get_template_choices

View File

@ -10047,7 +10047,7 @@ paths:
/outposts/service_connections/all/types/:
get:
operationId: outposts_service_connections_all_types_list
description: Get all creatable service connection types
description: Get all creatable types
tags:
- outposts
security:
@ -10881,7 +10881,7 @@ paths:
/policies/all/types/:
get:
operationId: policies_all_types_list
description: Get all creatable policy types
description: Get all creatable types
tags:
- policies
security:
@ -13439,7 +13439,7 @@ paths:
/propertymappings/all/types/:
get:
operationId: propertymappings_all_types_list
description: Get all creatable property-mapping types
description: Get all creatable types
tags:
- propertymappings
security:
@ -15899,7 +15899,7 @@ paths:
/providers/all/types/:
get:
operationId: providers_all_types_list
description: Get all creatable provider types
description: Get all creatable types
tags:
- providers
security:
@ -21898,7 +21898,7 @@ paths:
/sources/all/types/:
get:
operationId: sources_all_types_list
description: Get all creatable source types
description: Get all creatable types
tags:
- sources
security:
@ -25532,7 +25532,7 @@ paths:
/stages/all/types/:
get:
operationId: stages_all_types_list
description: Get all creatable stage types
description: Get all creatable types
tags:
- stages
security:
@ -37793,10 +37793,6 @@ components:
type: string
icon:
type: string
nullable: true
description: |-
Get the URL to the Icon. If the name is /static or
starts with http it is returned as-is
readOnly: true
server_uri:
type: string
@ -39106,9 +39102,6 @@ components:
icon:
type: string
nullable: true
description: |-
Get the URL to the Icon. If the name is /static or
starts with http it is returned as-is
readOnly: true
provider_type:
$ref: '#/components/schemas/ProviderTypeEnum'
@ -43800,10 +43793,6 @@ components:
type: string
icon:
type: string
nullable: true
description: |-
Get the URL to the Icon. If the name is /static or
starts with http it is returned as-is
readOnly: true
client_id:
type: string
@ -45863,10 +45852,6 @@ components:
type: string
icon:
type: string
nullable: true
description: |-
Get the URL to the Icon. If the name is /static or
starts with http it is returned as-is
readOnly: true
pre_authentication_flow:
type: string
@ -47484,6 +47469,8 @@ components:
type: string
model_name:
type: string
icon_url:
type: string
requires_enterprise:
type: boolean
default: false

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@ -0,0 +1,13 @@
<svg id="icon" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<defs>
<style>
.cls-1 {
fill: none;
}
</style>
</defs>
<path d="M6,30H26a2,2,0,0,0,2-2V22a2,2,0,0,0-2-2H6a2,2,0,0,0-2,2v6A2,2,0,0,0,6,30Zm0-8H26v6H6Z" transform="translate(0 0)"/>
<circle cx="9" cy="25" r="1"/>
<path d="M26,2,24.59,3.41,27.17,6H22.315A6.9835,6.9835,0,0,0,9.08,10H4.83L7.41,7.41,6,6,1,11l5,5,1.41-1.41L4.83,12H9.685A6.9835,6.9835,0,0,0,22.92,8h4.25l-2.58,2.59L26,12l5-5ZM21,9a4.983,4.983,0,0,1-8.9745,3H16V10H11.1011a4.9852,4.9852,0,0,1,8.8734-4H16V8h4.8989A5.0019,5.0019,0,0,1,21,9Z" transform="translate(0 0)"/>
<rect id="_Transparent_Rectangle_" data-name="&lt;Transparent Rectangle&gt;" class="cls-1" width="32" height="32"/>
</svg>

After

Width:  |  Height:  |  Size: 787 B

View File

@ -0,0 +1 @@
<svg id="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs><style>.cls-1{fill:none;}</style></defs><title>desktop</title><path d="M28,4H4A2,2,0,0,0,2,6V22a2,2,0,0,0,2,2h8v4H8v2H24V28H20V24h8a2,2,0,0,0,2-2V6A2,2,0,0,0,28,4ZM18,28H14V24h4Zm10-6H4V6H28Z" transform="translate(0)"/><rect id="_Transparent_Rectangle_" data-name="&lt;Transparent Rectangle&gt;" class="cls-1" width="32" height="32"/></svg>

After

Width:  |  Height:  |  Size: 417 B

View File

@ -0,0 +1,16 @@
<svg id="icon" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<defs>
<style>
.cls-1 {
fill: none;
}
</style>
</defs>
<circle cx="16" cy="13.5" r="1.5"/>
<path d="M19.5356,10.4648a5,5,0,0,0-7.0717,0L11.05,9.0508a6.9992,6.9992,0,0,1,9.9,0Z" transform="translate(0 0)"/>
<path d="M23.0713,6.929a10,10,0,0,0-14.1426,0L7.5146,5.5144a12.0011,12.0011,0,0,1,16.9708,0Z" transform="translate(0 0)"/>
<polygon points="21 25 16 30 11 25 12.409 23.581 15 26.153 15 19 17 19 17 26.206 19.591 23.581 21 25"/>
<polygon points="24 11 19 16 24 21 25.419 19.591 22.847 17 30 17 30 15 22.794 15 25.419 12.409 24 11"/>
<polygon points="8 11 13 16 8 21 6.581 19.591 9.153 17 2 17 2 15 9.206 15 6.581 12.409 8 11"/>
<rect id="_Transparent_Rectangle_" data-name="&lt;Transparent Rectangle&gt;" class="cls-1" width="32" height="32"/>
</svg>

After

Width:  |  Height:  |  Size: 895 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -1,7 +1,7 @@
import { PaginatedResponse } from "@goauthentik/authentik/elements/table/Table";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKChart } from "@goauthentik/elements/charts/Chart";
import "@goauthentik/elements/forms/ConfirmationForm";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { ChartData, ChartOptions } from "chart.js";
import { msg } from "@lit/localize";

View File

@ -25,167 +25,152 @@ type ModelConverter = (provider: OneOfProvider) => ModelRequest;
type ProviderNoteProvider = () => TemplateResult | undefined;
type ProviderNote = ProviderNoteProvider | undefined;
/**
* There's an internal key and an API key because "Proxy" has three different subtypes.
*/
// prettier-ignore
type ProviderType = [
string, // internal key used by the wizard to distinguish between providers
string, // Name of the provider
string, // Description
ProviderRenderer, // Function that returns the provider's wizard panel as a TemplateResult
ProviderModelEnumType, // key used by the API to distinguish between providers
ModelConverter, // Handler that takes a generic provider and returns one specifically typed to its panel
ProviderNote?,
];
export type LocalTypeCreate = TypeCreate & {
formName: string;
modelName: ProviderModelEnumType;
converter: ModelConverter;
note?: ProviderNote;
renderer: ProviderRenderer;
};
// prettier-ignore
const _providerModelsTable: ProviderType[] = [
[
"oauth2provider",
msg("OAuth2/OIDC (Open Authorization/OpenID Connect)"),
msg("Modern applications, APIs and Single-page applications."),
() =>
export const providerModelsList: LocalTypeCreate[] = [
{
formName: "oauth2provider",
name: msg("OAuth2/OIDC (Open Authorization/OpenID Connect)"),
description: msg("Modern applications, APIs and Single-page applications."),
renderer: () =>
html`<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>`,
ProviderModelEnum.Oauth2Oauth2provider,
(provider: OneOfProvider) => ({
modelName: ProviderModelEnum.Oauth2Oauth2provider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.Oauth2Oauth2provider,
...(provider as OAuth2ProviderRequest),
}),
],
[
"ldapprovider",
msg("LDAP (Lightweight Directory Access Protocol)"),
msg("Provide an LDAP interface for applications and users to authenticate against."),
() =>
component: "",
iconUrl: "/static/authentik/sources/openidconnect.svg",
},
{
formName: "ldapprovider",
name: msg("LDAP (Lightweight Directory Access Protocol)"),
description: msg(
"Provide an LDAP interface for applications and users to authenticate against.",
),
renderer: () =>
html`<ak-application-wizard-authentication-by-ldap></ak-application-wizard-authentication-by-ldap>`,
ProviderModelEnum.LdapLdapprovider,
(provider: OneOfProvider) => ({
modelName: ProviderModelEnum.LdapLdapprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.LdapLdapprovider,
...(provider as LDAPProviderRequest),
}),
],
[
"proxyprovider-proxy",
msg("Transparent Reverse Proxy"),
msg("For transparent reverse proxies with required authentication"),
() =>
component: "",
iconUrl: "/static/authentik/sources/ldap.png",
},
{
formName: "proxyprovider-proxy",
name: msg("Transparent Reverse Proxy"),
description: msg("For transparent reverse proxies with required authentication"),
renderer: () =>
html`<ak-application-wizard-authentication-for-reverse-proxy></ak-application-wizard-authentication-for-reverse-proxy>`,
ProviderModelEnum.ProxyProxyprovider,
(provider: OneOfProvider) => ({
modelName: ProviderModelEnum.ProxyProxyprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.ProxyProxyprovider,
...(provider as ProxyProviderRequest),
mode: ProxyMode.Proxy,
}),
],
[
"proxyprovider-forwardsingle",
msg("Forward Auth (Single Application)"),
msg("For nginx's auth_request or traefik's forwardAuth"),
() =>
component: "",
iconUrl: "/static/authentik/sources/proxy.svg",
},
{
formName: "proxyprovider-forwardsingle",
name: msg("Forward Auth (Single Application)"),
description: msg("For nginx's auth_request or traefik's forwardAuth"),
renderer: () =>
html`<ak-application-wizard-authentication-for-single-forward-proxy></ak-application-wizard-authentication-for-single-forward-proxy>`,
ProviderModelEnum.ProxyProxyprovider,
(provider: OneOfProvider) => ({
modelName: ProviderModelEnum.ProxyProxyprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.ProxyProxyprovider,
...(provider as ProxyProviderRequest),
mode: ProxyMode.ForwardSingle,
}),
],
[
"proxyprovider-forwarddomain",
msg("Forward Auth (Domain Level)"),
msg("For nginx's auth_request or traefik's forwardAuth per root domain"),
() =>
component: "",
iconUrl: "/static/authentik/sources/proxy.svg",
},
{
formName: "proxyprovider-forwarddomain",
name: msg("Forward Auth (Domain Level)"),
description: msg("For nginx's auth_request or traefik's forwardAuth per root domain"),
renderer: () =>
html`<ak-application-wizard-authentication-for-forward-proxy-domain></ak-application-wizard-authentication-for-forward-proxy-domain>`,
ProviderModelEnum.ProxyProxyprovider,
(provider: OneOfProvider) => ({
modelName: ProviderModelEnum.ProxyProxyprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.ProxyProxyprovider,
...(provider as ProxyProviderRequest),
mode: ProxyMode.ForwardDomain,
}),
],
[
"racprovider",
msg("Remote Access Provider"),
msg("Remotely access computers/servers via RDP/SSH/VNC"),
() =>
component: "",
iconUrl: "/static/authentik/sources/proxy.svg",
},
{
formName: "racprovider",
name: msg("Remote Access Provider"),
description: msg("Remotely access computers/servers via RDP/SSH/VNC"),
renderer: () =>
html`<ak-application-wizard-authentication-for-rac></ak-application-wizard-authentication-for-rac>`,
ProviderModelEnum.RacRacprovider,
(provider: OneOfProvider) => ({
modelName: ProviderModelEnum.RacRacprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.RacRacprovider,
...(provider as RACProviderRequest),
}),
() => html`<ak-license-notice></ak-license-notice>`
],
[
"samlprovider",
msg("SAML (Security Assertion Markup Language)"),
msg("Configure SAML provider manually"),
() =>
note: () => html`<ak-license-notice></ak-license-notice>`,
requiresEnterprise: true,
component: "",
iconUrl: "/static/authentik/sources/rac.svg",
},
{
formName: "samlprovider",
name: msg("SAML (Security Assertion Markup Language)"),
description: msg("Configure SAML provider manually"),
renderer: () =>
html`<ak-application-wizard-authentication-by-saml-configuration></ak-application-wizard-authentication-by-saml-configuration>`,
ProviderModelEnum.SamlSamlprovider,
(provider: OneOfProvider) => ({
modelName: ProviderModelEnum.SamlSamlprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.SamlSamlprovider,
...(provider as SAMLProviderRequest),
}),
],
[
"radiusprovider",
msg("RADIUS (Remote Authentication Dial-In User Service)"),
msg("Configure RADIUS provider manually"),
() =>
component: "",
iconUrl: "/static/authentik/sources/saml.png",
},
{
formName: "radiusprovider",
name: msg("RADIUS (Remote Authentication Dial-In User Service)"),
description: msg("Configure RADIUS provider manually"),
renderer: () =>
html`<ak-application-wizard-authentication-by-radius></ak-application-wizard-authentication-by-radius>`,
ProviderModelEnum.RadiusRadiusprovider,
(provider: OneOfProvider) => ({
modelName: ProviderModelEnum.RadiusRadiusprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.RadiusRadiusprovider,
...(provider as RadiusProviderRequest),
}),
],
[
"scimprovider",
msg("SCIM (System for Cross-domain Identity Management)"),
msg("Configure SCIM provider manually"),
() =>
component: "",
iconUrl: "/static/authentik/sources/radius.svg",
},
{
formName: "scimprovider",
name: msg("SCIM (System for Cross-domain Identity Management)"),
description: msg("Configure SCIM provider manually"),
renderer: () =>
html`<ak-application-wizard-authentication-by-scim></ak-application-wizard-authentication-by-scim>`,
ProviderModelEnum.ScimScimprovider,
(provider: OneOfProvider) => ({
modelName: ProviderModelEnum.ScimScimprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.ScimScimprovider,
...(provider as SCIMProviderRequest),
}),
],
component: "",
iconUrl: "/static/authentik/sources/scim.png",
},
];
function mapProviders([
formName,
name,
description,
_,
modelName,
converter,
note,
]: ProviderType): LocalTypeCreate {
return {
formName,
name,
description,
component: "",
modelName,
converter,
note,
};
}
export const providerModelsList = _providerModelsTable.map(mapProviders);
export const providerRendererList = new Map<string, ProviderRenderer>(
_providerModelsTable.map(([modelName, _0, _1, renderer]) => [modelName, renderer]),
providerModelsList.map((tc) => [tc.formName, tc.renderer]),
);
export default providerModelsList;

View File

@ -3,63 +3,41 @@ import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/wizard/TypeCreateWizardPage";
import { TypeCreateWizardPageLayouts } from "@goauthentik/elements/wizard/TypeCreateWizardPage";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { html, nothing } from "lit";
import { map } from "lit/directives/map.js";
import { html } from "lit";
import BasePanel from "../BasePanel";
import providerModelsList from "./ak-application-wizard-authentication-method-choice.choices";
import type { LocalTypeCreate } from "./ak-application-wizard-authentication-method-choice.choices";
import providerModelsList from "./ak-application-wizard-authentication-method-choice.choices";
@customElement("ak-application-wizard-authentication-method-choice")
export class ApplicationWizardAuthenticationMethodChoice extends WithLicenseSummary(BasePanel) {
constructor() {
super();
this.handleChoice = this.handleChoice.bind(this);
this.renderProvider = this.renderProvider.bind(this);
}
handleChoice(ev: InputEvent) {
const target = ev.target as HTMLInputElement;
this.dispatchWizardUpdate({
update: {
...this.wizard,
providerModel: target.value,
errors: {},
},
status: this.valid ? "valid" : "invalid",
});
}
renderProvider(type: LocalTypeCreate) {
const method = this.wizard.providerModel;
return html`<div class="pf-c-radio">
<input
class="pf-c-radio__input"
type="radio"
name="type"
id="provider-${type.formName}"
?disabled=${type.formName === "racprovider" && !this.hasEnterpriseLicense}
value=${type.formName}
?checked=${type.formName === method}
@change=${this.handleChoice}
/>
<label class="pf-c-radio__label" for="provider-${type.formName}">${type.name}</label>
<span class="pf-c-radio__description"
>${type.description}${type.note ? type.note() : nothing}</span
>
</div>`;
}
render() {
const selectedTypes = providerModelsList.filter(
(t) => t.formName === this.wizard.providerModel,
);
return providerModelsList.length > 0
? html`<form class="pf-c-form pf-m-horizontal">
${map(providerModelsList, this.renderProvider)}
<ak-wizard-page-type-create
.types=${providerModelsList}
layout=${TypeCreateWizardPageLayouts.grid}
.selectedType=${selectedTypes.length > 0 ? selectedTypes[0] : undefined}
@select=${(ev: CustomEvent<LocalTypeCreate>) => {
this.dispatchWizardUpdate({
update: {
...this.wizard,
providerModel: ev.detail.formName,
errors: {},
},
status: this.valid ? "valid" : "invalid",
});
}}
></ak-wizard-page-type-create>
</form>`
: html`<ak-empty-state loading header=${msg("Loading")}></ak-empty-state>`;
}

View File

@ -9,7 +9,7 @@ import "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize";
import { customElement, state } from "@lit/reactive-element/decorators.js";
import { TemplateResult, css, html, nothing } from "lit";
import { PropertyValues, TemplateResult, css, html, nothing } from "lit";
import { classMap } from "lit/directives/class-map.js";
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
@ -94,8 +94,7 @@ export class ApplicationWizardCommitApplication extends BasePanel {
response?: TransactionApplicationResponse;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
willUpdate(_changedProperties: Map<string, any>) {
willUpdate(_changedProperties: PropertyValues<this>) {
if (this.commitState === idleState) {
this.response = undefined;
this.commitState = runningState;

View File

@ -12,8 +12,6 @@ import "./radius/ak-application-wizard-authentication-by-radius";
import "./saml/ak-application-wizard-authentication-by-saml-configuration";
import "./scim/ak-application-wizard-authentication-by-scim";
// prettier-ignore
@customElement("ak-application-wizard-authentication-method")
export class ApplicationWizardApplicationDetails extends BasePanel {
render() {

View File

@ -9,15 +9,14 @@ import { customElement, property } from "lit/decorators.js";
@customElement("ak-license-notice")
export class AkLicenceNotice extends WithLicenseSummary(AKElement) {
@property()
notice = msg("This feature requires an enterprise license.");
notice = msg("Enterprise only");
render() {
return this.hasEnterpriseLicense
? nothing
: html`
<ak-alert class="pf-c-radio__description" inline>
${this.notice}
<a href="#/enterprise/licenses">${msg("Learn more")}</a>
<ak-alert class="pf-c-radio__description" inline plain>
<a href="#/enterprise/licenses">${this.notice}</a>
</ak-alert>
`;
}

View File

@ -4,73 +4,24 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/forms/ProxyForm";
import "@goauthentik/elements/wizard/FormWizardPage";
import "@goauthentik/elements/wizard/TypeCreateWizardPage";
import "@goauthentik/elements/wizard/Wizard";
import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
import type { Wizard } from "@goauthentik/elements/wizard/Wizard";
import { msg, str } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { CSSResult, TemplateResult, html } from "lit";
import { property } from "lit/decorators.js";
import { property, query } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { OutpostsApi, TypeCreate } from "@goauthentik/api";
@customElement("ak-service-connection-wizard-initial")
export class InitialServiceConnectionWizardPage extends WizardPage {
@property({ attribute: false })
connectionTypes: TypeCreate[] = [];
static get styles(): CSSResult[] {
return [PFBase, PFForm, PFButton, PFRadio];
}
sidebarLabel = () => msg("Select type");
activeCallback: () => Promise<void> = async () => {
this.host.isValid = false;
this.shadowRoot
?.querySelectorAll<HTMLInputElement>("input[type=radio]")
.forEach((radio) => {
if (radio.checked) {
radio.dispatchEvent(new CustomEvent("change"));
}
});
};
render(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
${this.connectionTypes.map((type) => {
return html`<div class="pf-c-radio">
<input
class="pf-c-radio__input"
type="radio"
name="type"
id=${`${type.component}-${type.modelName}`}
@change=${() => {
this.host.steps = [
"initial",
`type-${type.component}-${type.modelName}`,
];
this.host.isValid = true;
}}
/>
<label class="pf-c-radio__label" for=${`${type.component}-${type.modelName}`}
>${type.name}</label
>
<span class="pf-c-radio__description">${type.description}</span>
</div>`;
})}
</form>`;
}
}
@customElement("ak-service-connection-wizard")
export class ServiceConnectionWizard extends AKElement {
static get styles(): CSSResult[] {
return [PFBase, PFButton, PFRadio];
return [PFBase, PFButton];
}
@property()
@ -79,6 +30,9 @@ export class ServiceConnectionWizard extends AKElement {
@property({ attribute: false })
connectionTypes: TypeCreate[] = [];
@query("ak-wizard")
wizard?: Wizard;
firstUpdated(): void {
new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsAllTypesList().then((types) => {
this.connectionTypes = types;
@ -92,11 +46,19 @@ export class ServiceConnectionWizard extends AKElement {
header=${msg("New outpost integration")}
description=${msg("Create a new outpost integration.")}
>
<ak-service-connection-wizard-initial
<ak-wizard-page-type-create
slot="initial"
.connectionTypes=${this.connectionTypes}
.types=${this.connectionTypes}
@select=${(ev: CustomEvent<TypeCreate>) => {
if (!this.wizard) return;
this.wizard.steps = [
"initial",
`type-${ev.detail.component}-${ev.detail.modelName}`,
];
this.wizard.isValid = true;
}}
>
</ak-service-connection-wizard-initial>
</ak-wizard-page-type-create>
${this.connectionTypes.map((type) => {
return html`
<ak-wizard-page-form

View File

@ -10,80 +10,24 @@ import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/forms/ProxyForm";
import "@goauthentik/elements/wizard/FormWizardPage";
import { FormWizardPage } from "@goauthentik/elements/wizard/FormWizardPage";
import "@goauthentik/elements/wizard/TypeCreateWizardPage";
import "@goauthentik/elements/wizard/Wizard";
import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
import type { Wizard } from "@goauthentik/elements/wizard/Wizard";
import { msg, str } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { CSSResult, TemplateResult, html } from "lit";
import { property } from "lit/decorators.js";
import { property, query } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { PoliciesApi, Policy, PolicyBinding, TypeCreate } from "@goauthentik/api";
@customElement("ak-policy-wizard-initial")
export class InitialPolicyWizardPage extends WizardPage {
@property({ attribute: false })
policyTypes: TypeCreate[] = [];
static get styles(): CSSResult[] {
return [PFBase, PFForm, PFButton, PFRadio];
}
sidebarLabel = () => msg("Select type");
activeCallback: () => Promise<void> = async () => {
this.host.isValid = false;
this.shadowRoot
?.querySelectorAll<HTMLInputElement>("input[type=radio]")
.forEach((radio) => {
if (radio.checked) {
radio.dispatchEvent(new CustomEvent("change"));
}
});
};
render(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
${this.policyTypes.map((type) => {
return html`<div class="pf-c-radio">
<input
class="pf-c-radio__input"
type="radio"
name="type"
id=${`${type.component}-${type.modelName}`}
@change=${() => {
const idx = this.host.steps.indexOf("initial") + 1;
// Exclude all current steps starting with type-,
// this happens when the user selects a type and then goes back
this.host.steps = this.host.steps.filter(
(step) => !step.startsWith("type-"),
);
this.host.steps.splice(
idx,
0,
`type-${type.component}-${type.modelName}`,
);
this.host.isValid = true;
}}
/>
<label class="pf-c-radio__label" for=${`${type.component}-${type.modelName}`}
>${type.name}</label
>
<span class="pf-c-radio__description">${type.description}</span>
</div>`;
})}
</form>`;
}
}
@customElement("ak-policy-wizard")
export class PolicyWizard extends AKElement {
static get styles(): CSSResult[] {
return [PFBase, PFButton, PFRadio];
return [PFBase, PFButton];
}
@property()
@ -98,6 +42,9 @@ export class PolicyWizard extends AKElement {
@property({ attribute: false })
policyTypes: TypeCreate[] = [];
@query("ak-wizard")
wizard?: Wizard;
firstUpdated(): void {
new PoliciesApi(DEFAULT_CONFIG).policiesAllTypesList().then((types) => {
this.policyTypes = types;
@ -111,8 +58,26 @@ export class PolicyWizard extends AKElement {
header=${msg("New policy")}
description=${msg("Create a new policy.")}
>
<ak-policy-wizard-initial slot="initial" .policyTypes=${this.policyTypes}>
</ak-policy-wizard-initial>
<ak-wizard-page-type-create
slot="initial"
.types=${this.policyTypes}
@select=${(ev: CustomEvent<TypeCreate>) => {
if (!this.wizard) return;
const idx = this.wizard.steps.indexOf("initial") + 1;
// Exclude all current steps starting with type-,
// this happens when the user selects a type and then goes back
this.wizard.steps = this.wizard.steps.filter(
(step) => !step.startsWith("type-"),
);
this.wizard.steps.splice(
idx,
0,
`type-${ev.detail.component}-${ev.detail.modelName}`,
);
this.wizard.isValid = true;
}}
>
</ak-wizard-page-type-create>
${this.policyTypes.map((type) => {
return html`
<ak-wizard-page-form

View File

@ -1,4 +1,3 @@
import "@goauthentik/admin/common/ak-license-notice";
import "@goauthentik/admin/property-mappings/PropertyMappingLDAPForm";
import "@goauthentik/admin/property-mappings/PropertyMappingNotification";
import "@goauthentik/admin/property-mappings/PropertyMappingRACForm";
@ -6,90 +5,35 @@ import "@goauthentik/admin/property-mappings/PropertyMappingSAMLForm";
import "@goauthentik/admin/property-mappings/PropertyMappingScopeForm";
import "@goauthentik/admin/property-mappings/PropertyMappingTestForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/Alert";
import { AKElement } from "@goauthentik/elements/Base";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
import "@goauthentik/elements/forms/ProxyForm";
import "@goauthentik/elements/wizard/FormWizardPage";
import "@goauthentik/elements/wizard/TypeCreateWizardPage";
import "@goauthentik/elements/wizard/Wizard";
import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
import type { Wizard } from "@goauthentik/elements/wizard/Wizard";
import { msg, str } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { TemplateResult, html, nothing } from "lit";
import { property } from "lit/decorators.js";
import { TemplateResult, html } from "lit";
import { property, query } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { PropertymappingsApi, TypeCreate } from "@goauthentik/api";
@customElement("ak-property-mapping-wizard-initial")
export class InitialPropertyMappingWizardPage extends WithLicenseSummary(WizardPage) {
@property({ attribute: false })
mappingTypes: TypeCreate[] = [];
static get styles() {
return [PFBase, PFForm, PFButton, PFRadio];
}
sidebarLabel = () => msg("Select type");
activeCallback: () => Promise<void> = async () => {
this.host.isValid = false;
this.shadowRoot
?.querySelectorAll<HTMLInputElement>("input[type=radio]")
.forEach((radio) => {
if (radio.checked) {
radio.dispatchEvent(new CustomEvent("change"));
}
});
};
render(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
${this.mappingTypes.map((type) => {
const requiresEnteprise = type.requiresEnterprise && !this.hasEnterpriseLicense;
return html`<div class="pf-c-radio">
<input
class="pf-c-radio__input"
type="radio"
name="type"
id=${`${type.component}-${type.modelName}`}
@change=${() => {
this.host.steps = [
"initial",
`type-${type.component}-${type.modelName}`,
];
this.host.isValid = true;
}}
?disabled=${type.requiresEnterprise ? this.hasEnterpriseLicense : false}
/>
<label class="pf-c-radio__label" for=${`${type.component}-${type.modelName}`}
>${type.name}</label
>
<span class="pf-c-radio__description"
>${type.description}
${requiresEnteprise
? html`<ak-license-notice></ak-license-notice>`
: nothing}</span
>
</div>`;
})}
</form>`;
}
}
@customElement("ak-property-mapping-wizard")
export class PropertyMappingWizard extends AKElement {
static get styles() {
return [PFBase, PFButton, PFRadio];
return [PFBase, PFButton];
}
@property({ attribute: false })
mappingTypes: TypeCreate[] = [];
@query("ak-wizard")
wizard?: Wizard;
async firstUpdated(): Promise<void> {
this.mappingTypes = await new PropertymappingsApi(
DEFAULT_CONFIG,
@ -103,11 +47,19 @@ export class PropertyMappingWizard extends AKElement {
header=${msg("New property mapping")}
description=${msg("Create a new property mapping.")}
>
<ak-property-mapping-wizard-initial
<ak-wizard-page-type-create
slot="initial"
.mappingTypes=${this.mappingTypes}
.types=${this.mappingTypes}
@select=${(ev: CustomEvent<TypeCreate>) => {
if (!this.wizard) return;
this.wizard.steps = [
"initial",
`type-${ev.detail.component}-${ev.detail.modelName}`,
];
this.wizard.isValid = true;
}}
>
</ak-property-mapping-wizard-initial>
</ak-wizard-page-type-create>
${this.mappingTypes.map((type) => {
return html`
<ak-wizard-page-form

View File

@ -5,103 +5,28 @@ import "@goauthentik/admin/providers/proxy/ProxyProviderForm";
import "@goauthentik/admin/providers/saml/SAMLProviderForm";
import "@goauthentik/admin/providers/saml/SAMLProviderImportForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/Alert";
import { AKElement } from "@goauthentik/elements/Base";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
import "@goauthentik/elements/forms/ProxyForm";
import { paramURL } from "@goauthentik/elements/router/RouterOutlet";
import "@goauthentik/elements/wizard/FormWizardPage";
import "@goauthentik/elements/wizard/TypeCreateWizardPage";
import { TypeCreateWizardPageLayouts } from "@goauthentik/elements/wizard/TypeCreateWizardPage";
import "@goauthentik/elements/wizard/Wizard";
import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
import type { Wizard } from "@goauthentik/elements/wizard/Wizard";
import { msg, str } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { CSSResult, TemplateResult, html, nothing } from "lit";
import { property } from "lit/decorators.js";
import { CSSResult, TemplateResult, html } from "lit";
import { property, query } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFHint from "@patternfly/patternfly/components/Hint/hint.css";
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { ProvidersApi, TypeCreate } from "@goauthentik/api";
@customElement("ak-provider-wizard-initial")
export class InitialProviderWizardPage extends WithLicenseSummary(WizardPage) {
@property({ attribute: false })
providerTypes: TypeCreate[] = [];
static get styles(): CSSResult[] {
return [PFBase, PFForm, PFHint, PFButton, PFRadio];
}
sidebarLabel = () => msg("Select type");
activeCallback: () => Promise<void> = async () => {
this.host.isValid = false;
this.shadowRoot
?.querySelectorAll<HTMLInputElement>("input[type=radio]")
.forEach((radio) => {
if (radio.checked) {
radio.dispatchEvent(new CustomEvent("change"));
}
});
};
renderHint(): TemplateResult {
return html`<div class="pf-c-hint">
<div class="pf-c-hint__title">${msg("Try the new application wizard")}</div>
<div class="pf-c-hint__body">
${msg(
"The new application wizard greatly simplifies the steps required to create applications and providers.",
)}
</div>
<div class="pf-c-hint__footer">
<a
class="pf-c-button pf-m-link pf-m-inline"
href=${paramURL("/core/applications", {
createForm: true,
})}
>${msg("Try it now")}</a
>
</div>
</div>
<br />`;
}
render(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
${this.providerTypes.map((type) => {
const requiresEnterprise = type.requiresEnterprise && !this.hasEnterpriseLicense;
return html`<div class="pf-c-radio">
<input
class="pf-c-radio__input"
type="radio"
name="type"
id=${type.component}
@change=${() => {
this.host.steps = ["initial", `type-${type.component}`];
this.host.isValid = true;
}}
?disabled=${requiresEnterprise}
/>
<label class="pf-c-radio__label" for=${type.component}>${type.name}</label>
<span class="pf-c-radio__description"
>${type.description}
${requiresEnterprise
? html`<ak-license-notice></ak-license-notice>`
: nothing}
</span>
</div>`;
})}
</form>`;
}
}
@customElement("ak-provider-wizard")
export class ProviderWizard extends AKElement {
static get styles(): CSSResult[] {
return [PFBase, PFButton, PFRadio];
return [PFBase, PFButton];
}
@property()
@ -115,6 +40,9 @@ export class ProviderWizard extends AKElement {
return Promise.resolve();
};
@query("ak-wizard")
wizard?: Wizard;
async firstUpdated(): Promise<void> {
this.providerTypes = await new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList();
}
@ -129,8 +57,17 @@ export class ProviderWizard extends AKElement {
return this.finalHandler();
}}
>
<ak-provider-wizard-initial slot="initial" .providerTypes=${this.providerTypes}>
</ak-provider-wizard-initial>
<ak-wizard-page-type-create
slot="initial"
layout=${TypeCreateWizardPageLayouts.grid}
.types=${this.providerTypes}
@select=${(ev: CustomEvent<TypeCreate>) => {
if (!this.wizard) return;
this.wizard.steps = ["initial", `type-${ev.detail.component}`];
this.wizard.isValid = true;
}}
>
</ak-wizard-page-type-create>
${this.providerTypes.map((type) => {
return html`
<ak-wizard-page-form

View File

@ -7,78 +7,32 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/forms/ProxyForm";
import "@goauthentik/elements/wizard/FormWizardPage";
import { TypeCreateWizardPageLayouts } from "@goauthentik/elements/wizard/TypeCreateWizardPage";
import "@goauthentik/elements/wizard/Wizard";
import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
import type { Wizard } from "@goauthentik/elements/wizard/Wizard";
import { msg, str } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { CSSResult, TemplateResult, html } from "lit";
import { property } from "lit/decorators.js";
import { property, query } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { SourcesApi, TypeCreate } from "@goauthentik/api";
@customElement("ak-source-wizard-initial")
export class InitialSourceWizardPage extends WizardPage {
@property({ attribute: false })
sourceTypes: TypeCreate[] = [];
static get styles(): CSSResult[] {
return [PFBase, PFForm, PFButton, PFRadio];
}
sidebarLabel = () => msg("Select type");
activeCallback: () => Promise<void> = async () => {
this.host.isValid = false;
this.shadowRoot
?.querySelectorAll<HTMLInputElement>("input[type=radio]")
.forEach((radio) => {
if (radio.checked) {
radio.dispatchEvent(new CustomEvent("change"));
}
});
};
render(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
${this.sourceTypes.map((type) => {
return html`<div class="pf-c-radio">
<input
class="pf-c-radio__input"
type="radio"
name="type"
id=${`${type.component}-${type.modelName}`}
@change=${() => {
this.host.steps = [
"initial",
`type-${type.component}-${type.modelName}`,
];
this.host.isValid = true;
}}
/>
<label class="pf-c-radio__label" for=${`${type.component}-${type.modelName}`}
>${type.name}</label
>
<span class="pf-c-radio__description">${type.description}</span>
</div>`;
})}
</form>`;
}
}
@customElement("ak-source-wizard")
export class SourceWizard extends AKElement {
static get styles(): CSSResult[] {
return [PFBase, PFButton, PFRadio];
return [PFBase, PFButton];
}
@property({ attribute: false })
sourceTypes: TypeCreate[] = [];
@query("ak-wizard")
wizard?: Wizard;
firstUpdated(): void {
new SourcesApi(DEFAULT_CONFIG).sourcesAllTypesList().then((types) => {
this.sourceTypes = types;
@ -92,8 +46,20 @@ export class SourceWizard extends AKElement {
header=${msg("New source")}
description=${msg("Create a new source.")}
>
<ak-source-wizard-initial slot="initial" .sourceTypes=${this.sourceTypes}>
</ak-source-wizard-initial>
<ak-wizard-page-type-create
slot="initial"
.types=${this.sourceTypes}
layout=${TypeCreateWizardPageLayouts.grid}
@select=${(ev: CustomEvent<TypeCreate>) => {
if (!this.wizard) return;
this.wizard.steps = [
"initial",
`type-${ev.detail.component}-${ev.detail.modelName}`,
];
this.wizard.isValid = true;
}}
>
</ak-wizard-page-type-create>
${this.sourceTypes.map((type) => {
return html`
<ak-wizard-page-form

View File

@ -1,5 +1,5 @@
import { placeholderHelperText } from "@goauthentik/admin/helperText";
import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm";
import { placeholderHelperText } from "@goauthentik/authentik/admin/helperText";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";

View File

@ -22,89 +22,27 @@ import "@goauthentik/admin/stages/user_logout/UserLogoutStageForm";
import "@goauthentik/admin/stages/user_write/UserWriteStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKElement } from "@goauthentik/elements/Base";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
import "@goauthentik/elements/forms/ProxyForm";
import "@goauthentik/elements/wizard/FormWizardPage";
import { FormWizardPage } from "@goauthentik/elements/wizard/FormWizardPage";
import "@goauthentik/elements/wizard/TypeCreateWizardPage";
import "@goauthentik/elements/wizard/Wizard";
import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
import { Wizard } from "@goauthentik/elements/wizard/Wizard";
import { msg, str } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { CSSResult, TemplateResult, html, nothing } from "lit";
import { property } from "lit/decorators.js";
import { CSSResult, TemplateResult, html } from "lit";
import { property, query } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { FlowStageBinding, Stage, StagesApi, TypeCreate } from "@goauthentik/api";
@customElement("ak-stage-wizard-initial")
export class InitialStageWizardPage extends WithLicenseSummary(WizardPage) {
@property({ attribute: false })
stageTypes: TypeCreate[] = [];
sidebarLabel = () => msg("Select type");
static get styles(): CSSResult[] {
return [PFBase, PFForm, PFButton, PFRadio];
}
activeCallback: () => Promise<void> = async () => {
this.host.isValid = false;
this.shadowRoot
?.querySelectorAll<HTMLInputElement>("input[type=radio]")
.forEach((radio) => {
if (radio.checked) {
radio.dispatchEvent(new CustomEvent("change"));
}
});
};
render(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
${this.stageTypes.map((type) => {
const requiresEnterprise = type.requiresEnterprise && !this.hasEnterpriseLicense;
return html`<div class="pf-c-radio">
<input
class="pf-c-radio__input"
type="radio"
name="type"
id=${`${type.component}-${type.modelName}`}
@change=${() => {
const idx = this.host.steps.indexOf("initial") + 1;
// Exclude all current steps starting with type-,
// this happens when the user selects a type and then goes back
this.host.steps = this.host.steps.filter(
(step) => !step.startsWith("type-"),
);
this.host.steps.splice(
idx,
0,
`type-${type.component}-${type.modelName}`,
);
this.host.isValid = true;
}}
?disabled=${requiresEnterprise}
/>
<label class="pf-c-radio__label" for=${`${type.component}-${type.modelName}`}
>${type.name}</label
>
<span class="pf-c-radio__description">${type.description}${
requiresEnterprise ? html`<ak-license-notice></ak-license-notice>` : nothing
}</span>
</span>
</div>`;
})}
</form>`;
}
}
@customElement("ak-stage-wizard")
export class StageWizard extends AKElement {
static get styles(): CSSResult[] {
return [PFBase, PFButton, PFRadio];
return [PFBase, PFButton];
}
@property()
@ -119,6 +57,9 @@ export class StageWizard extends AKElement {
@property({ attribute: false })
stageTypes: TypeCreate[] = [];
@query("ak-wizard")
wizard?: Wizard;
firstUpdated(): void {
new StagesApi(DEFAULT_CONFIG).stagesAllTypesList().then((types) => {
this.stageTypes = types;
@ -132,8 +73,26 @@ export class StageWizard extends AKElement {
header=${msg("New stage")}
description=${msg("Create a new stage.")}
>
<ak-stage-wizard-initial slot="initial" .stageTypes=${this.stageTypes}>
</ak-stage-wizard-initial>
<ak-wizard-page-type-create
slot="initial"
.types=${this.stageTypes}
@select=${(ev: CustomEvent<TypeCreate>) => {
if (!this.wizard) return;
const idx = this.wizard.steps.indexOf("initial") + 1;
// Exclude all current steps starting with type-,
// this happens when the user selects a type and then goes back
this.wizard.steps = this.wizard.steps.filter(
(step) => !step.startsWith("type-"),
);
this.wizard.steps.splice(
idx,
0,
`type-${ev.detail.component}-${ev.detail.modelName}`,
);
this.wizard.isValid = true;
}}
>
</ak-wizard-page-type-create>
${this.stageTypes.map((type) => {
return html`
<ak-wizard-page-form

View File

@ -1,10 +1,10 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { deviceTypeRestrictionPair } from "@goauthentik/admin/stages/authenticator_webauthn/utils";
import { DataProvision } from "@goauthentik/authentik/elements/ak-dual-select/types";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider";
import { DataProvision } from "@goauthentik/elements/ak-dual-select/types";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";

View File

@ -3,7 +3,7 @@ import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
import { msg } from "@lit/localize";
import { customElement, property, query } from "@lit/reactive-element/decorators.js";
import { TemplateResult, html, nothing } from "lit";
import { TemplateResult, css, html, nothing } from "lit";
import { classMap } from "lit/directives/class-map.js";
import { map } from "lit/directives/map.js";
@ -31,7 +31,15 @@ import { type WizardButton, WizardStepLabel } from "./types";
@customElement("ak-wizard-frame")
export class AkWizardFrame extends CustomEmitterElement(ModalButton) {
static get styles() {
return [...super.styles, PFWizard];
return [
...super.styles,
PFWizard,
css`
.pf-c-modal-box {
height: 75%;
}
`,
];
}
/**
@ -84,7 +92,7 @@ export class AkWizardFrame extends CustomEmitterElement(ModalButton) {
${this.renderHeader()}
<div class="pf-c-wizard__outer-wrap">
<div class="pf-c-wizard__inner-wrap">
${this.renderNavigation()}
${this.renderNavigation()}
${this.renderMainSection()}
</div>
${this.renderFooter()}

View File

@ -17,6 +17,8 @@ export enum Level {
export class Alert extends AKElement {
@property({ type: Boolean })
inline = false;
@property({ type: Boolean })
plain = false;
@property()
level: Level = Level.Warning;
@ -26,7 +28,11 @@ export class Alert extends AKElement {
}
render(): TemplateResult {
return html`<div class="pf-c-alert ${this.inline ? "pf-m-inline" : ""} ${this.level}">
return html`<div
class="pf-c-alert ${this.inline ? "pf-m-inline" : ""} ${this.plain
? "pf-m-plain"
: ""} ${this.level}"
>
<div class="pf-c-alert__icon">
<i class="fas fa-exclamation-circle"></i>
</div>

View File

@ -1,5 +1,5 @@
import { EVENT_REFRESH } from "@goauthentik/authentik/common/constants";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts";
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";

View File

@ -1,5 +1,5 @@
import { EVENT_REFRESH } from "@goauthentik/authentik/common/constants";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";

View File

@ -1,5 +1,5 @@
import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/authentik/common/constants";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/common/constants";
import { authentikEnterpriseContext } from "@goauthentik/elements/AuthentikContexts";
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";

View File

@ -1,7 +1,7 @@
import { EVENT_REFRESH } from "@goauthentik/authentik/common/constants";
import { getRelativeTime } from "@goauthentik/authentik/common/utils";
import { AKElement } from "@goauthentik/authentik/elements/Base";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { getRelativeTime } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-status-label";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/events/LogViewer";

View File

@ -1,7 +1,6 @@
import { sourceLocale, targetLocales } from "@goauthentik/authentik/locale-codes";
import { configureLocalization } from "@lit/localize";
import { sourceLocale, targetLocales } from "../../locale-codes";
import { getBestMatchLocale } from "./helpers";
type LocaleGetter = ReturnType<typeof configureLocalization>["getLocale"];

View File

@ -0,0 +1,146 @@
import "@goauthentik/admin/common/ak-license-notice";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
import { msg, str } from "@lit/localize";
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { TypeCreate } from "@goauthentik/api";
export enum TypeCreateWizardPageLayouts {
list = "list",
grid = "grid",
}
@customElement("ak-wizard-page-type-create")
export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) {
@property({ attribute: false })
types: TypeCreate[] = [];
@property({ attribute: false })
selectedType?: TypeCreate;
@property({ type: String })
layout: TypeCreateWizardPageLayouts = TypeCreateWizardPageLayouts.list;
static get styles(): CSSResult[] {
return [
PFBase,
PFForm,
PFGrid,
PFRadio,
PFCard,
css`
.pf-c-card__header-main img {
max-height: 2em;
min-height: 2em;
}
:host([theme="dark"]) .pf-c-card__header-main img {
filter: invert(1);
}
`,
];
}
sidebarLabel = () => msg("Select type");
activeCallback: () => Promise<void> = async () => {
this.host.isValid = false;
if (this.selectedType) {
this.selectDispatch(this.selectedType);
}
};
private selectDispatch(type: TypeCreate) {
this.dispatchEvent(
new CustomEvent("select", {
detail: type,
bubbles: true,
composed: true,
}),
);
}
renderGrid(): TemplateResult {
return html`<div class="pf-l-grid pf-m-gutter">
${this.types.map((type, idx) => {
const requiresEnterprise = type.requiresEnterprise && !this.hasEnterpriseLicense;
return html`<div
class="pf-l-grid__item pf-m-3-col pf-c-card ${requiresEnterprise
? "pf-m-non-selectable-raised"
: "pf-m-selectable-raised"} ${this.selectedType == type
? "pf-m-selected-raised"
: ""}"
tabindex=${idx}
@click=${() => {
if (requiresEnterprise) {
return;
}
this.selectDispatch(type);
this.selectedType = type;
}}
>
${type.iconUrl
? html`<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<img src=${type.iconUrl} alt=${msg(str`${type.name} Icon`)} />
</div>
</div>`
: nothing}
<div class="pf-c-card__title">${type.name}</div>
<div class="pf-c-card__body">${type.description}</div>
${requiresEnterprise
? html`<div class="pf-c-card__footer">
<ak-license-notice></ak-license-notice>
</div> `
: nothing}
</div>`;
})}
</div>`;
}
renderList(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
${this.types.map((type) => {
const requiresEnterprise = type.requiresEnterprise && !this.hasEnterpriseLicense;
return html`<div class="pf-c-radio">
<input
class="pf-c-radio__input"
type="radio"
name="type"
id=${`${type.component}-${type.modelName}`}
@change=${() => {
this.selectDispatch(type);
}}
?disabled=${requiresEnterprise}
/>
<label class="pf-c-radio__label" for=${`${type.component}-${type.modelName}`}
>${type.name}</label
>
<span class="pf-c-radio__description"
>${type.description}
${requiresEnterprise
? html`<ak-license-notice></ak-license-notice>`
: nothing}
</span>
</div>`;
})}
</form>`;
}
render(): TemplateResult {
switch (this.layout) {
case TypeCreateWizardPageLayouts.grid:
return this.renderGrid();
case TypeCreateWizardPageLayouts.list:
return this.renderList();
}
}
}

View File

@ -1,5 +1,5 @@
import { dateTimeLocal } from "@goauthentik/authentik/common/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { dateTimeLocal } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";

View File

@ -2,7 +2,6 @@
"extends": "./tsconfig.base.json",
"compilerOptions": {
"paths": {
"@goauthentik/authentik/*": ["src/*"],
"@goauthentik/admin/*": ["src/admin/*"],
"@goauthentik/common/*": ["src/common/*"],
"@goauthentik/components/*": ["src/components/*"],