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:
79
authentik/core/api/object_types.py
Normal file
79
authentik/core/api/object_types.py
Normal 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)
|
||||
@ -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(),
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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"""
|
||||
|
||||
|
||||
@ -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"""
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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=[])
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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]:
|
||||
|
||||
@ -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 = []
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
29
schema.yml
29
schema.yml
@ -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
|
||||
|
||||
BIN
web/authentik/sources/ldap.png
Normal file
BIN
web/authentik/sources/ldap.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.5 KiB |
13
web/authentik/sources/proxy.svg
Normal file
13
web/authentik/sources/proxy.svg
Normal 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="<Transparent Rectangle>" class="cls-1" width="32" height="32"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 787 B |
1
web/authentik/sources/rac.svg
Normal file
1
web/authentik/sources/rac.svg
Normal 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="<Transparent Rectangle>" class="cls-1" width="32" height="32"/></svg>
|
||||
|
After Width: | Height: | Size: 417 B |
16
web/authentik/sources/radius.svg
Normal file
16
web/authentik/sources/radius.svg
Normal 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="<Transparent Rectangle>" class="cls-1" width="32" height="32"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 895 B |
BIN
web/authentik/sources/saml.png
Normal file
BIN
web/authentik/sources/saml.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
BIN
web/authentik/sources/scim.png
Normal file
BIN
web/authentik/sources/scim.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
@ -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";
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>`;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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()}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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"];
|
||||
|
||||
146
web/src/elements/wizard/TypeCreateWizardPage.ts
Normal file
146
web/src/elements/wizard/TypeCreateWizardPage.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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/*"],
|
||||
|
||||
Reference in New Issue
Block a user