From 6c4c535d5766c8df2c78e7c8aaf3d73adcd9925a Mon Sep 17 00:00:00 2001 From: Jens L Date: Wed, 22 May 2024 02:41:33 +0200 Subject: [PATCH] web/admin: rework initial wizard pages and add grid layout (#9668) * remove @goauthentik/authentik as TS path Signed-off-by: Jens Langhammer * initial implementation Signed-off-by: Jens Langhammer * oh yeah Signed-off-by: Jens Langhammer * format earlier changes Signed-off-by: Jens Langhammer * support plain alert Signed-off-by: Jens Langhammer * initial attempt at dedupe Signed-off-by: Jens Langhammer * make it a base class Signed-off-by: Jens Langhammer * migrate all wizards Signed-off-by: Jens Langhammer * create type create mixin to dedupe more, add icon to source create Signed-off-by: Jens Langhammer * add ldap icon Signed-off-by: Jens Langhammer * 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 * format Signed-off-by: Jens Langhammer * make everything more explicit Signed-off-by: Jens Langhammer * add icons to provider Signed-off-by: Jens Langhammer * add remaining provider icons Signed-off-by: Jens Langhammer * rework to not use inheritance Signed-off-by: Jens Langhammer * fix unrelated typo Signed-off-by: Jens Langhammer * make app wizard use grid layout Signed-off-by: Jens Langhammer * keep wizard height consistent Signed-off-by: Jens Langhammer * fix tests Signed-off-by: Jens Langhammer --------- Signed-off-by: Jens Langhammer Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com> --- authentik/core/api/object_types.py | 79 +++++++ authentik/core/api/propertymappings.py | 25 +-- authentik/core/api/providers.py | 38 +--- authentik/core/api/sources.py | 29 +-- authentik/core/api/utils.py | 22 +- authentik/core/models.py | 4 + authentik/core/tests/test_models.py | 13 +- .../providers/google_workspace/models.py | 5 + .../providers/microsoft_entra/models.py | 5 + authentik/enterprise/providers/rac/models.py | 5 + authentik/events/api/events.py | 3 +- authentik/flows/api/stages.py | 24 +- authentik/lib/utils/reflection.py | 2 +- authentik/outposts/api/service_connections.py | 25 +-- authentik/policies/api/policies.py | 25 +-- authentik/providers/ldap/models.py | 5 + authentik/providers/oauth2/models.py | 5 + authentik/providers/proxy/models.py | 5 + authentik/providers/radius/models.py | 5 + authentik/providers/saml/models.py | 23 ++ authentik/providers/scim/models.py | 5 + authentik/sources/ldap/models.py | 5 + authentik/sources/oauth/models.py | 61 +++-- authentik/sources/plex/models.py | 15 +- authentik/sources/saml/models.py | 12 +- authentik/sources/scim/models.py | 5 + authentik/stages/email/api.py | 2 +- schema.yml | 29 +-- web/authentik/sources/ldap.png | Bin 0 -> 8749 bytes web/authentik/sources/proxy.svg | 13 ++ web/authentik/sources/rac.svg | 1 + web/authentik/sources/radius.svg | 16 ++ web/authentik/sources/saml.png | Bin 0 -> 75966 bytes web/authentik/sources/scim.png | Bin 0 -> 6501 bytes .../admin-overview/charts/SyncStatusChart.ts | 2 +- ...rd-authentication-method-choice.choices.ts | 209 ++++++++---------- ...ion-wizard-authentication-method-choice.ts | 66 ++---- ...k-application-wizard-commit-application.ts | 5 +- ...pplication-wizard-authentication-method.ts | 2 - web/src/admin/common/ak-license-notice.ts | 7 +- .../admin/outposts/ServiceConnectionWizard.ts | 74 ++----- web/src/admin/policies/PolicyWizard.ts | 89 +++----- .../PropertyMappingWizard.ts | 86 ++----- web/src/admin/providers/ProviderWizard.ts | 103 ++------- web/src/admin/sources/SourceWizard.ts | 76 ++----- web/src/admin/sources/scim/SCIMSourceForm.ts | 2 +- web/src/admin/stages/StageWizard.ts | 97 +++----- .../AuthenticatorWebAuthnStageForm.ts | 2 +- .../ak-wizard-main/ak-wizard-frame.ts | 14 +- web/src/elements/Alert.ts | 8 +- .../Interface/BrandContextController.ts | 2 +- .../Interface/ConfigContextController.ts | 2 +- .../Interface/EnterpriseContextController.ts | 2 +- web/src/elements/SyncStatusCard.ts | 6 +- .../ak-locale-context/configureLocale.ts | 3 +- .../elements/wizard/TypeCreateWizardPage.ts | 146 ++++++++++++ .../user-settings/tokens/UserTokenForm.ts | 2 +- web/tsconfig.json | 1 - 58 files changed, 726 insertions(+), 791 deletions(-) create mode 100644 authentik/core/api/object_types.py create mode 100644 web/authentik/sources/ldap.png create mode 100644 web/authentik/sources/proxy.svg create mode 100644 web/authentik/sources/rac.svg create mode 100644 web/authentik/sources/radius.svg create mode 100644 web/authentik/sources/saml.png create mode 100644 web/authentik/sources/scim.png create mode 100644 web/src/elements/wizard/TypeCreateWizardPage.ts diff --git a/authentik/core/api/object_types.py b/authentik/core/api/object_types.py new file mode 100644 index 0000000000..12acd4e4a7 --- /dev/null +++ b/authentik/core/api/object_types.py @@ -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) diff --git a/authentik/core/api/propertymappings.py b/authentik/core/api/propertymappings.py index 8cb9590977..06eeb7a396 100644 --- a/authentik/core/api/propertymappings.py +++ b/authentik/core/api/propertymappings.py @@ -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(), diff --git a/authentik/core/api/providers.py b/authentik/core/api/providers.py index 76696c3a0d..2c33ccf1f6 100644 --- a/authentik/core/api/providers.py +++ b/authentik/core/api/providers.py @@ -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) diff --git a/authentik/core/api/sources.py b/authentik/core/api/sources.py index 04341d8804..977696c407 100644 --- a/authentik/core/api/sources.py +++ b/authentik/core/api/sources.py @@ -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: diff --git a/authentik/core/api/utils.py b/authentik/core/api/utils.py index 9cc3d3fc5d..08e4a66f22 100644 --- a/authentik/core/api/utils.py +++ b/authentik/core/api/utils.py @@ -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""" diff --git a/authentik/core/models.py b/authentik/core/models.py index d12de39905..67a6710c25 100644 --- a/authentik/core/models.py +++ b/authentik/core/models.py @@ -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""" diff --git a/authentik/core/tests/test_models.py b/authentik/core/tests/test_models.py index 989301a020..6f95fcce9e 100644 --- a/authentik/core/tests/test_models.py +++ b/authentik/core/tests/test_models.py @@ -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): diff --git a/authentik/enterprise/providers/google_workspace/models.py b/authentik/enterprise/providers/google_workspace/models.py index 4d3d831e60..12ace4cb39 100644 --- a/authentik/enterprise/providers/google_workspace/models.py +++ b/authentik/enterprise/providers/google_workspace/models.py @@ -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" diff --git a/authentik/enterprise/providers/microsoft_entra/models.py b/authentik/enterprise/providers/microsoft_entra/models.py index 3cee3bf15d..92d1725107 100644 --- a/authentik/enterprise/providers/microsoft_entra/models.py +++ b/authentik/enterprise/providers/microsoft_entra/models.py @@ -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" diff --git a/authentik/enterprise/providers/rac/models.py b/authentik/enterprise/providers/rac/models.py index 3469f23831..42e7a266e0 100644 --- a/authentik/enterprise/providers/rac/models.py +++ b/authentik/enterprise/providers/rac/models.py @@ -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" diff --git a/authentik/events/api/events.py b/authentik/events/api/events.py index 5ddb1a55b7..2e4ae6f307 100644 --- a/authentik/events/api/events.py +++ b/authentik/events/api/events.py @@ -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 diff --git a/authentik/flows/api/stages.py b/authentik/flows/api/stages.py index 81b3b1a8ab..a4973a62ec 100644 --- a/authentik/flows/api/stages.py +++ b/authentik/flows/api/stages.py @@ -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: diff --git a/authentik/lib/utils/reflection.py b/authentik/lib/utils/reflection.py index b4eba76333..b8f8e4c08e 100644 --- a/authentik/lib/utils/reflection.py +++ b/authentik/lib/utils/reflection.py @@ -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)] diff --git a/authentik/outposts/api/service_connections.py b/authentik/outposts/api/service_connections.py index 6fe8300d22..2c4484f83c 100644 --- a/authentik/outposts/api/service_connections.py +++ b/authentik/outposts/api/service_connections.py @@ -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: diff --git a/authentik/policies/api/policies.py b/authentik/policies/api/policies.py index 8b1d2eba3e..93137a0ce8 100644 --- a/authentik/policies/api/policies.py +++ b/authentik/policies/api/policies.py @@ -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=[]) diff --git a/authentik/providers/ldap/models.py b/authentik/providers/ldap/models.py index 5032ef3e47..3288b71498 100644 --- a/authentik/providers/ldap/models.py +++ b/authentik/providers/ldap/models.py @@ -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 diff --git a/authentik/providers/oauth2/models.py b/authentik/providers/oauth2/models.py index d9029e1158..45b2d9e0b8 100644 --- a/authentik/providers/oauth2/models.py +++ b/authentik/providers/oauth2/models.py @@ -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" diff --git a/authentik/providers/proxy/models.py b/authentik/providers/proxy/models.py index 4445be86c6..f824495309 100644 --- a/authentik/providers/proxy/models.py +++ b/authentik/providers/proxy/models.py @@ -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 diff --git a/authentik/providers/radius/models.py b/authentik/providers/radius/models.py index 83acd4f564..7e9784f191 100644 --- a/authentik/providers/radius/models.py +++ b/authentik/providers/radius/models.py @@ -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 diff --git a/authentik/providers/saml/models.py b/authentik/providers/saml/models.py index fafa4ad4ed..e0154bdf89 100644 --- a/authentik/providers/saml/models.py +++ b/authentik/providers/saml/models.py @@ -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") diff --git a/authentik/providers/scim/models.py b/authentik/providers/scim/models.py index 5225b0c01c..e7e8a0987c 100644 --- a/authentik/providers/scim/models.py +++ b/authentik/providers/scim/models.py @@ -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]: diff --git a/authentik/sources/ldap/models.py b/authentik/sources/ldap/models.py index 06ced2f9fa..587794ae04 100644 --- a/authentik/sources/ldap/models.py +++ b/authentik/sources/ldap/models.py @@ -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 = [] diff --git a/authentik/sources/oauth/models.py b/authentik/sources/oauth/models.py index b2dc3c9ea0..d05665bcb4 100644 --- a/authentik/sources/oauth/models.py +++ b/authentik/sources/oauth/models.py @@ -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: diff --git a/authentik/sources/plex/models.py b/authentik/sources/plex/models.py index 1e70d93dc1..13734cb3b3 100644 --- a/authentik/sources/plex/models.py +++ b/authentik/sources/plex/models.py @@ -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, } ) diff --git a/authentik/sources/saml/models.py b/authentik/sources/saml/models.py index 1bfab98ce6..c4b998bf26 100644 --- a/authentik/sources/saml/models.py +++ b/authentik/sources/saml/models.py @@ -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, } ) diff --git a/authentik/sources/scim/models.py b/authentik/sources/scim/models.py index 4684c4dfa8..de2149ee67 100644 --- a/authentik/sources/scim/models.py +++ b/authentik/sources/scim/models.py @@ -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 diff --git a/authentik/stages/email/api.py b/authentik/stages/email/api.py index 67f0f26f06..14f5a0108a 100644 --- a/authentik/stages/email/api.py +++ b/authentik/stages/email/api.py @@ -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 diff --git a/schema.yml b/schema.yml index 7f1e5917ca..7b017f79c3 100644 --- a/schema.yml +++ b/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 diff --git a/web/authentik/sources/ldap.png b/web/authentik/sources/ldap.png new file mode 100644 index 0000000000000000000000000000000000000000..8fda3be56ac96d9f69cb7d73dba4e9ae1597e37e GIT binary patch literal 8749 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 literal 0 HcmV?d00001 diff --git a/web/authentik/sources/proxy.svg b/web/authentik/sources/proxy.svg new file mode 100644 index 0000000000..13c6c0883a --- /dev/null +++ b/web/authentik/sources/proxy.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/web/authentik/sources/rac.svg b/web/authentik/sources/rac.svg new file mode 100644 index 0000000000..6b78fd3968 --- /dev/null +++ b/web/authentik/sources/rac.svg @@ -0,0 +1 @@ +desktop diff --git a/web/authentik/sources/radius.svg b/web/authentik/sources/radius.svg new file mode 100644 index 0000000000..9aca0e6b97 --- /dev/null +++ b/web/authentik/sources/radius.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/web/authentik/sources/saml.png b/web/authentik/sources/saml.png new file mode 100644 index 0000000000000000000000000000000000000000..81185a330467cde4b5abdcc9b75a36b0e4303c5e GIT binary patch literal 75966 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
icZH8g5%2vYy2lD~+ZkM>8* zH)gS@{!}ubU&L!WVjyJnLuo51c{}*@t)*)z=Lr^-@A39TsEi1wA_*9d%e{m)pW~ti z!Kx6OCGuqd{OE^2p$x2|qBesAt-PPontDDIsL-6E+ zBn(lqSoH40)5RJPIGtJAGaQ?=oG@hi-!o7-O8{M!3;hnxV1S2r^qpOZ#*h7untFx@ z-~MtroXIOCX%U$C9;>g>&=tz8-wSL=8Fv|XluV>^_<%fM^ML=oo{9Z5av7p-oF>Vh zwn&IdH**}KJ>abtsGr_|dSzgftidRk^F+3W#_*7#aSaofyT*WrPLuw0SDu4DHpCht zybp#^0#QB4yCGF95L@FqL4?6v*I_eEc11222VJP~+2Mf*E`WU~(nY#VO>k{(L*;k# znYV-b+m9ILp#Pen!?DO253jNxH}fmc(m3QAX|NRY+UAe-ajG{aBw;Ts^g<^n#K6up z#ZYsU2M0-4hH655=6_F+qlMvme6pwEofxlYHBtOplGQ%m_kSDdg;>-A?hViy4|mNDIjkr}W(X5W)AZqhW~_0gLX5mNlvGLCzxJ>) z$Ta5n?+F#Flck0Nwo z5MWj9r}v*hR2rXuSUllpV}g1F%R#I9#Hm^t9Jy}GNV+h`wF#$tFt`nHS+8z?=Zx@4 zNI8_7BkesrL$J2k(~dCcG~xRGwASGKJzYdQ{1gGdk*HM(LW)sVwT@|^$}hEmh*%N( zJd73w(W`oDPiGm{^u2KyGpzfN^&RarCumhmvr@e^hs8#V8*_I&F~#X{eC)1fCiMJ9 zWv0XClkx%khbc-P;@nD0M*j`fc3|rn1v@)_sC6aPUQU1p%jF)t`TLsTky?cd$2oSY z3=N&`%b@3Ag@{+gg&2oNQ{Q%&^XWN^ukAi~d(_^^>cZ>Y|dM z#lMgY`rhUi`ZOlO;qd95aTG?}DFZk=j1|+7uD*sE8dqT|0R0gXu-enND^@Yutyny< z7}~Xh)6y$x@Au{}*U_|}fI(^41)y@v&kDN3?8A&MDYbx3XHQ!a7aKEwSi7!KDKly4 zGxqc%KsHO?f!t2^mKD?(H_J(g4xqyubI=$Mh#^jDMOyFV#O$LlPU7>7bG%Bp$cMfA4_A+NA(MnibsBRR$8a-!%eRP#c#1)Jp zN+i5q*o5P>8i|TGH?CQ$Ku}F1I>z!sUSrBlw)uZYM5ILn+D2g{oJz5$|F~p?9iU$K z1*MXGKVR^E;wI6q5LE$&0SP&^GM|$~Xt)UR4d~D5{rY44(X8rq_2cnzWFDIW(_IC= zzW=D&_J;Gc37um?MvDaz2R(?aTreD75h-{OXm0N=Wt0KDz~)?jV3~XBOvhm_1hy_) zQBq|@Myb@o@iHq~WRA2ucld7IURJ(+w4?nnB0Sy(`06Xs6Z$osICjp^0+y+Tz=cr{ zI4Aj*5fM}ABbv&PgsKXv!21gMr_Bz*niv{mya%oG2e)BT$*A3?(3hUdp(l%XyPN%9 z|FcMD5Xt5~iREoZcad69PPp8n*$s!^a(C*jJaYHyvD0105%%11MRpz$6t>$usNJa{6G<7~!g5VKU!AAfEym-de|G;H0K0`< zT_k_5ROx`M0O9UNd$6OkL`=#uC5BNtjrhzW3tMw`D=SI`gJ+Pg~_! z@|J3ezb8G;;k7wR9TvA+N4^Hrs3yY z4|%ug0YQ1|Yh^%P@b}$A0j$MAdapPUZZqZS)eht1Cj0wKHrzOkgoytb|CfrVe3M!( zcOBb}bPvE=OCM-KjBV9~CK61o22Ma8 zGo&AOGi+b%i+bYq-;rXhNvg-pwHW7#VRQ4uV$kV7X0SVFH~JK;XmA#;uMDIk%+IA8 z-3?qk(n}x7P;1%zT1vh#eSRBZuJVsxY}v5qNu*AKwNHoh4Cnxns0wv-1u;fdBis5z z&`^yq_>aq-v*??`>hLtA2nDeA$1HI^5>?X)8z;buXbN#QmQj`0hSs8Uajkarphc9q zMs$e*CWZWTUNExO`OqeEUa*z^GKn~P-u*VoXE-Vet@}zh;)KXSzQDDDKRc#SFW7B3 zV`x{<*eCNJoHk?2Ek8@Q829FREG!7st@4y{qu`tii=U4^69)4>O{Jzaf*Fgc7*$Ov zGvq2)SRnp$bzi6Hw#M(d5{p`;u4mNR3hmG-;S9eDV$@y{4}KQ53HS(o=LyNZI5^FR zV->8n8M>uKvIEmGt%nqNuPF04IH-jr6@W8}5K^4Wxcm6ewQjcBe?7>Vs^^_5jg-A4 z8=Lg`j0{2hZfLD^ag0o3;DE70$pZ(LAZDf-iee=knYxUA6?L!uxS`?0I!ZZG*Yd*c zJ5z|)+l-;R;aV6X1|h-Uw|Q;c0B9)OWVEX-zmWWR5;E{Hp{@MoAViGp3u96ya6wrb zqv!y_ttZeMxK0-z1#aU@xa+IT4XPX4!$SWv^8_AXK$0*D`B#YEQqfP*1_NtNIPO}= zE6`61pGevdqh`(o!({-x?SfLy;6LY0Y!sF_$^pj%?%{mciFDviX`QN~3o!e;W10!Z?n*}u4WUlF7qFaZb?Wi=eJ}`7 z#bSWdO85wshdoYegXs;5+z+XDF_pQIt;(Ve8z zDgXk0)kB0tZc9sqi-5Cy^xtnem65-qEYMfgNhe7ku)P(nC0<>)J+zOC|G&EcL{sJ9 zd+SHJ&C3@01@|*}@cn=(?-hYf8bM5oROi$iQJ{6{oA204Yk(h~T1v6t@P)FFEVM%A5?V}@!p&v&_#y_&&O&3;~@g(F!N*t)) zqhBRuy>O-7>50jeVwCo-962~NX!b)!22ulpasUxp;yZT|aGf`Z>F0?*C?{a1d3xUy z;Wa__b^5K`JtDea2l#5o7{ZdF%NO*G;d|X~w-%&e{Y3c6*td@`xd2SALD6|+eP)$M zyJJ_Bh!5rCf@g$dDQ9Q9DBZ1@X-Y_)V-LK6i0%3`F`CD=0QuL(_|l< zuv!?65M4bVhg#R^yZBsdJV~PeHqF+Pr)K?WJi?rOIR|BCTz3-HF2#?@h3u~e+Sw6` zitb<%S%jCetaP2RsC+p=RS%a9YnIusxx$>o)qWmTR8YP`s?7k;YbySj{*-9z{5+p! zvFD;<a#6!^0_tfZGmv1zR*N_GVyXmXcz%{5qmxztAdilzn6Fa?q(a@1 zsifiFaXltSM}d=tK0G1CJ{|+&HRn1^$fwKFLYmcJ-KR+{Yj)A68oy6vIZrPR+%8q~ zqpP4xJLs~ci>&r5!K=@{AOyAg4voD$P}DgU3`%Rvr`%(kZ~Syvo|g9J-z+<@>EQ~7 zXBB3~J5?V;J5)Pl7*!H#I@ie0ZuHaLw~{KWqZsh5pt|sfVWH+!5D`J^(t-*#Bu!14 z-ytNL?RJX9k!5L{Z0LD2c~^J(PcN?_!$3v(m4k~PvW~PAYh0?|G5~pBd0yY^h{vIz zW;Ep##!(w%->CoI16+i!H6zx7<9+Eje{NoKp-yRWYEvjP^2KKQIn`Mvgjg18mnH5F z)T%))MaV$yomtgH(h1JHN?Af_=}|LnpOolZ!V_F>bCxYD9JH6iD8+aiaLje=y^+07$jBlpdZ$`Yw>Y-lfiY+AR8E zVexFbRA3TVTJ|%5G1dMP$7j0JN>(6J5BrB&dTW~AS_AW*&ORPy8)UE5QbUDv2LUG9Vj?y~ljFttUE;rJ8(P!bNT^91?u5Z#RqG z^Jkl%g8)``MmJ^&yYsY470%C(PZFei%J+52mQ+^O40u5tj-%o2bX-ixo#yLkaD@vI zxzD}(Wjtg;=RoMBLAYAISxBU+PAWsG;R|Gkj%iJA+tQ)wS_?5{+G9e%P>`!-R@048 z4bkLi(q3syP0AV-u&?q=+pa2<8#AB=LMCM)gX@rzM;ys&Au8eHLDXyLmvZ->yltcY zG#L8Obb!&A`9S)ra{u;36RF1;QY7ZMxEZPkFIGHY`kyy=Dj&poc}ONeOTWgXrH1IqVWng zz0>OEyixa(OT_*!Y-)Q9Wc`TLUZeOXH=66=givVq)QTuUvv6OC!kxgqeDM|Z3#5qR z@e_wfAYcbO?WGd`!FyUnY#yXYXr6rb!OP_-z)2->t?Z9Rvmcw69397s=h>!eBT-D$ zj5|+?bcB9)S%v;RK0m4>(Y$v`@cD;!W;Ms$+G$sH)w#J}AX5lWxvyC&!?;d>*;s-C z_~Ap;lTl$28e0PG*F`08u%aiWW06u25tAEHhBinIkK0af(XSFVBtF`v3?~g|T2;C) zTp#XWU8Yk*6#)UtJ)a7XDWp2(NHZwilT=NOF_}aiT!C%8CePLM%lGNbPsmgjACUR* zN+Ua)jE$FkIb97#=#MuIh0&X01^YHS;!9i|nVrE=E1$=&Vy@fRAwQlv?v~Q5EX|-y z|9B>K9;1nIP5@jng0b$$!S@V}gi;MzuN__oq5d$_(a&BiG?RV38jnBBmiZw0Q5nNc zF&X8|E`q?ATwKGDims2#Id+x8g1Ewi^A%D>OT4Zyf&W%ZaaPf+(bnRVNz>6{3!`^X z{&Tek9|Xxj+oOZ^clMSGD-Kj9JN|~@X17s_+{Lpg(8>$a$^~dSg$ZQK?~fGcef7kS zH+Ou0^B-=&Q=CNn9)m^vQqJA_x`CMJ{>MI-Pa+yM&SAa@{HK=&#VjRvPJ}@RhH)mw zq#Y|7)0d(pi%!lK;(lXfT6K2Og2Xfp6pL2(5pRY;h;}a4b&gis7TCi`rtKAUqJW9^ zyTA8)I{kld#`={T6krIx`LBlbws@iYpr z9KH1Dc}3LF23&w|tv_5)(Ty-*6!VXWkXt+^nikf>6}L5NjucsRb_zzBz8SiBwc!mhvk@+-mC?XorZ>UYYwS=vC-4Rm8TSVc8^UV)y8XB<$@A5kG1!zl>SSE#a_= zPxT6{g_Ad8PkuV-U-iqav|d2f6-;T~o7s8$_*r8FXhWSCdSqT0;zvr4z#2Jthp(%eypx{zRP^YO6VJ#gT z;IqAApJM%=8TK5v9IaedI;3=SD`GCTFs{CF%Q6+I&p2IfH|=a%;|O|K{EpIb7*GM6 zP9SRN!toOCmf^xJ_tNjY%m+QvhkaUTBZQ38g~7>1xGeJ6Ig3z0kWB5|hpO)i3y4q1 zG`NAV&xm;Ym3DYR6L;uPi|V)z7Q@OK3Loh)fd4r<_4BWkyYTZ#pNX?5j&sf(_VANU zu=L0%wX)QY#y>AK58YPAN)h?Wb6>xlt37Ky+Es<0_jiwsq)QYAZjy~JK74-=YPiTg z;d%Pz(%Pw`wNwjTUE0dW)Bib@@sihOifVj+jrYm@ZZ||o<5Omx0%Bon$xkl zUyBI=YVrRdz&B8u{>@EUjDAA&{VgUVv1H&pP+sy zRUr`4w~o*n6KO}Ro*001bGZEpPz1L$tG|oTGI5@$!+kn!i_uISb`LEkq+T=92uZN!OodFUu`Kn_$6`)Iq z{T#ne!Fu1wO(g}4LuUk0VntT|S-TE_v@9cMf?VCV4JuY}dz86)b!1$(@G`}*qfQ}1 zRWw!{9(49^#|^Us{w_O>R7F;8bZI=wS5yk@XxLt<3?|!F4c+*%N>>Z0^4067y(yTW(oLt^Qd3OW=+SD z2}+t|rFWU#fNjLRcEmguDZr7+wPl&`I--|@aw`71fJX!Fic;@d>lYbP$l;G$lPDYshs@!aYL_EiqoNIz=RfiNFAE!c>5QO_j0kOv798%u zS8AF|VrlI{_Bw*=&4X7CG9|4Qy6k=Evu^vyl*h?HSEX;E54`={o( zv7av%bc)=~pM>K+yL)@PJdJ)8T;}?@{Pk46`jbOP!ddUX2c!CR5IR0TZ)s9xa~rL( zfKG}QoCm$*wXT3+0677Wc~bBoqTbIOmr(cff_O)DfL88G_~eyR8UOQ{)?cFI&C6W#K2_HrWVI+;V+37-rahQb zb8?LJG020)J{j2N#qwuj>@v(e8HTBMH%8dyUVBOBUi$*yh>^`A@>$%yLiH)+jw?&V ze|>cWo?emNy^?_J*#jFA2EvxKwXb2k0A+sEuiJLj^<5gO)9(x9Bg6<{M*vtKZ!bUp z#gKXRXUPZ|rE+`F)|7{0cUMTQkcYy$6sLob=8{Pbm<%2|MK*;X0U^==mgO?;9$u9c z0tT%lnZB#{K-D?q-hUfiGqQSHRNo7$;lSZI%6Z}(2)8tA4;{U22EhR^GzgULQyUFu zn$;`_{g^THWVNQD-=%c;1*``uJhJ79Tzz5a$4Vp?P~3QynjBP?;DYTsyxN468(=)_ zgE}6xq;`*o+QDJ0;d>Vhz=p*Ngg0N7m9zbGJcV}Flk_enhhN={Sy9a36UKb`xfBpW znM>YL{wn3bEhS=XwRo*J-Qp-aFGtz+;e%~Kla@r0J^=#owl_vA?@2G*oI)^02ous) zWN7l4ciduQ&r{KzT3((GmMZej@Ta^ADce1B@lYD*W>3|KmCDN>!zbreUc%moPdcj!&Et#Pw-W7+tRN>V&gkR z6{f99Fle zN=>vgxMk%s&Z~rq;l(w72G<)R#RNAesHn96_4=*XtRx?Kc!{KcpPWqT^=1(0ug=Zei^=J!fZ<2oP2M8B_l+fs$U+MP9rQAMcI5FNdvWoSe4!2gM? zU=H7crklTz`KMqzfUzE@r*SJw__jQkG11$-B0RX7)iIOlpe2`WzG~w4v&oZRW6yQK z4us3G4EYN^L&u1)To+5SvoF7IU+=00jU}h%cGQgATe-gV2em?a)7!|9X7}abK_ly9 z&oYXWSS$DqR ziIwTxtDydQT+Ve`Rl90VX|G|5!WDmED}Sgu4=vErq~=La7ri0n!4UI36ke~I($Nzg z-3w2agu}D%^sF^rwY=;NdvkbI?$K2J&hE5?m!&+63 zJ!%+#Cst<2DATK4DGsYGf5*=9Sjv@bgz-$G?G-@j-f+xa{VW$f4Ks5Hs1*XH9p&}f! zo=_X!|8x7arRem<=@q7(R&@mCdYT2O()zKs{n!b4;+qq_H~2ssCR#}TRi|h@!LpdMPEei9v!LT0-fA9 zk8V#5l?8>;g`ZfAE9z@`KQH9x9quz|oOxP0j#u|MQpb|&=^OsK-75e(aO-;5jC7v0 z|AM6W>Okr5Yt%<%+#uooZNS6eX!~N#<`uz-KVQhEEUCvSs?Prv)8$@(-4Z>tX@5j**(5teIO0N6B^EUs#AX>g5fnfdG9$L1wW9k2; zpWznzJsQCp!gE@)28yAuICk!3nRB7AkouiSH4|X>MdaMoFy|v8Qhn^eJ_#7mY)@Lw z-%!a@7w_Q*jCdVRHkM1gAOK6X4+F8WQ(rg<^yw#lWa17%vMI6S=s5YhPjsVq{m$R{ z{&*%_xZIf7(VR7183g1g(%A zAOC%7<4`wOj~O%+U|7VG=|;h9*yuD}s!@kYr(^jxVB{gQ)P12;I=2UMmI&$&Wz2fI zFuQbQ%Ai|iK@elNfC;Vy?^iCU!74AT?H&d&kbo^m=+NjK0*u zoM6MtJ0qom=B8C;gH+~fKDk0De@WG+!U!XW3%+|~JJhke`f<*2oCk8GTn2GG3VL?z z@nf+}zzgoxXGvU4QYik(eaC~({JqV_U>h7G?y+$tu)$v#sgy8brOc+bw9D4ij4v#C zxKcOTMz0cVJz3~hFm5S9q?9)pO)iw`IrTSa4XxTa3I@f;kv|)nb9(x%NINpR+%=fa znM9oeb>*H_h^nhmk0*N6PmQS_B=u0!d4PKDg7VqbL+`5fTN*`r6RL^e&+(H;yEj?A z9+8d-taYa@X`eoMunO}t9NjD_Gy=KzO1I!s6;(p_o~o}6Km6&iesDve#bm3Z_VL0> z2`TZ1c!oFDeSqe8`fprKMR^(>+4o@gFv&c{eB3Gytkestm>Ylz_Tvk+Kb-*7F+4K) zBzp6xdvo%nSCwacO#5S=;+s&$(&d+hfo|^T1i@fl-es9r=~$D43JoiVFf-QX?L$I_ zFY#NoeE705H_bi7&fGGVF;zheaQ*7~Su-h)yd+1s_p_eX@h{VlGsw0u=l!Mc>WYf3 zPoY-t*izTMjcTZhUFE~GlDHnhIa$dsu8NZh`RA5rv!+Q@2~Ob-FxC9+$cLwuKH4aM z4h)wwH*;zpA1qmv4ZCmtuhNj%GSk_HI_;JPG7%Hr3O(xkJOA})>$Ta z`+|PES6Et|23MGKu$BC!_KKA@X@N)t5~IvI-d1K%9sE-!IF_ua9A%>o$ktI3T>6s2 zzf$yyOqWNTNVK>kVj+xvE9g z<8Fo0drYeqB9j2 zGi>4_fB*5`2xH+#x#89;LdZ03e5`MoCUS*$IvX7=de${LAol*zVB`1ui0ECl7kf+ELBl6O z7M98n3-8SdH~Hd)|FmAIn0F_xJKg<^`A&S>X=F6rK~HZ8!=?0y54SbXFR0MsjB0(s zdB?;oIgACnWoaKte(`me%mba$+Umi~13)-cSFoGhEG`o;{~>vP78A7+Yg*vc7#E{r#O#UPQw zAPhwCJg*=v7M)(WdU)VZ=keN#+_NIOtWDOo2eatBVYrvS<DBcPlHvgUiw08b1SWzd?evBcutE z>_iAg1oBCzzvMTAhAPn7PTs}>0%#v7^BtW_^V%pe1`X~_{1JVC9apaOM%KSgTVQL_ zu+<#L7Xf#HPEt(|i}5DL@1wOUEsUkbliyfE@qxxwr5~bBUU?lY_*!ZCzdL@YGE-FO zsGHSiozC*fX*tf`vQklJoa90qXjsq{YVY4LGy=*hAX#C=s;}JH>#n)LPRw3#+{0@aH{`JKSF+ zi82RzS}(VmEJQ_}W<^v8U%Xl0*C%x0S3}kbPoJ4h3kP9Ii=iBjLO<~Y&=9P2cPst42J z`xCQir;Xs*m(4kchC8J!8qIvaUr^*b^8_dg#WJw@(Fp6l;MR6n2~G;XxRkbZz4gk{ z^24lcxF{<9r`tMFHI)eE@&knb;4{%;tU#koTq#RTYK3_6)R|F75xFjWMFxYtD$k2n zQhcdkR@u17b-Vb^!<)OY5v4C;_f%w{+sQ>8e-%iTX4i#`<&`Dx-j!=HF8N?R>L@%R zNJuYbpo;GivP~MA1g^%`ndGzRywV-V6StP{vJ=bS-C$(xKQOj3(Yk8SDFgF57!Q!4 zdsC~y{GB~-M4v=NRR6?{U99%=NhYw=Dae2AR!1IVU+ML{D2!5qE3`ZLb+w5+e4uZ2 z!!eafKGP`e1-oX4d3iPEF%#6@+6os8N`fd3ZzugiiHyyx~aj5YD=s6E&7!us#Bp6b?@j z2pjEsR4-;Dct@hx(K0?Eq0}eibtZqru~QR+;yr%x%qOGhg)uqSi|z1tO3v~2tinNhjiQJ#{?kP$NfTEMkUjIc^2*etBUG9eGh-gf-Fv(68 z$}^Xeh~~9<14dG*_UHWdXf}y)P2Eu%!6lw9w9HK_uFW5d?!40&(NG98@EoHzF~QV ziBBa*?{BK`WA!E?#fJxre8x;@P;&javX}F^-20Y(<5CA3l~j7X3{188(IlX6rT>js z(R)CNG8wE1AmN~Qwg7Civv`sNJLc*Se zLshG(ov5cKZTlNL<@Y*Co&bO{?{MH^PsW+geT~oa=%$5pf0SmKOh0ywX}Ym|mt{*_ zs*{GdEAov!;d)JN+g!reJ_n20HNIUX_KojFuGq~lBH#Z9_CN{0E}PXJy+ zCo-y_IEYBc{5vVLK8%t(TH9bdXU#QDwhfS_I?{FQ$C8pmt6&V-5u%Fi#M6?gn^{e~ zATeN3y%Ow-ig3mE4*x$0O#}y)#^HO3Y7GNHf`RX?7)l zXOnSN!aD@ocggxx5jOPf=Y<1Dr;dYD-q;)DQfoZ z)ESlf!xg>!r;X*lAchxGOnMW#cGizu&3rR4wuYuTe zRy#rAMW)-*>(gOb*W{o|_lG(;soB;zc>PFTh27J=Ku#{RQD78V&vgwNfm3H?C#>C^n@#Yte|EY zv}_ynw{yM&AG-WnHLI2o(c}b;nvLPUyj7o=sX_ABQ%buV>ug`u*;^C}Dfsp^DKOWVUSI(@n} zNw$uxgk7X&Id+m?yOQoy#_`=6DBIM)Nx}K@K^doh17H(8r!} zg}u_FY;3Zq*#rKKTt1-B0p^6u)oQ5&@GO!uC ztavGb`k-8mA)d{ocy%UpV8>(4Z?~B%3 zrHM(hh2-0ArvL9AN|x;*-+dz`LT8fqo`jQe^5<7ZSE}=V?`F-)*K~C)B5Q2rc-f$} zpH-vbj!0KmC5F^2LzY3?k7{STSm-mNyaOwVYL?203~H8cdAq4)T-2n5=iO65dj!M8 zSV`B*oS-21&z>`@)&48FKmqk+oJp!?3#ptvbV6>X<8ygIMf7IvTrLv?qK(-Gq*6Bv zpcR*q+4>B~CaR5IG9lZMnx3nrZahPdYFqJ^wg^=L6hN4 zzM|q~rOWmL{%!a-(#4>V2wb)lXwpPgnvK?g7wV)57*W*NCZ4Mk=skN%-J+dPL8spq zWoh$Z5Z!@HA;rqR?RNSf@1g(qHd4KdX)*(TCI*AZh=tEbYOTg|JJ+gJrkt&Q?qLEI?hPSocbBTmI0kC zI5o2m7uwVEH7&TWfvJ+Nm#AiKK3%!J>M)<8892Lb9G!$K39Q@bMBGX$Xdz{@@wE@I z(5E+Xa2cf9AVzEh?rp#sabcmu?IL6e3dSJYk7meqP%l!#s=w%2(?kMaRgoyT&gTV8>BqhA~n=%3b^->;_nT6Hpmxw+cUW3WfzCvP@u+qq3}a4DXdD9gU& zcu~;W&TG(iQjdK3lxUsKpk`AjlKIw|Y=hWh6?^4C&32~~q%V1H{b@da6#I|r?PBIE zCX*#yFJpoNy+-FnO*(8D0E#`4PQV3pl5HeayOEM4tZ=Yd44$z4Ts+>TEO~d0Z6NkG z*dWZ+-!4K{>&cpcY-Jd3wOw=UFw$O!j2VGb< z5Qyi}WQJt8qr)zB!+d-n4C&mtYOC_){W)hK%f94zUdUoAMAwnUOWlrka4@LZ6uOm4 z(bxttR(6l5F4L%*6>s}g1xt`6pu~`8+euA;U=M`MsiLp6q>El>{%MtK{~GV+rmED* z0%$h~kX<6w%c6=OWMy}^TxOq!4qDrQdmE6F5}R9ZT7MOY=&}8g&~(ox+AtGjesJgV>Id zz1z+<_J8#zp}-x$meBusGeLGM*$Qh3#IvZ3FA45?2D-Q?BmK)=iEZ6FB+T2pxGejU z<3&Jgv$%QZxTyU3)g{AY>2`S(h1NDmD&+rOc9jMMr0(3wi5g9FU({YS<+i=rj!)=o z@Ty9>IMH|U#2LTT(gBYLJ2@FTK`kQCuBGiVLDgWV{}?lKrK_vw1e)2@*@$g0pGve0 zvzLUGXAOM|gc7ca)YdQyWH}*7OJKz}Li8 z3aHpk*aM^HY|tR6s*6ieS@tEz^FUUepfIjgi{aQjQ!(?C$OEd(_iLvS5Lw|U&oFG~^(zZZW-F1mC**9R9~ zr^}R(&~JKUl0#57i|BT7DV=zndB)jaeJpfOnmM_3qq3v+8ry(-8!QnE=qWa6J8+TB zkV=L41IVZDz@Drz$cla}-=^|~$ApSCH~VK*RIY$4Rw>{`gzW^b!KKfGsBR73Ezc|? zufG@d<4qDLIfy+I@PE!{%mhbf+cvDoy)5y7*n$}IRV?^^`#F# zgSH=a>%tSGt(XLRl_gz*2VF3=RKUXl_Vx--*JRKxq7!Zg`FMC?LG=^sT=1K6=$U3t zykOfPks6>(y zofL4AIzh)yN%Zi3*r!>u%1wNI{qa1=uG^QaM3BSaTOM1rsd#*0&-C6{J4kAFGy!L{ z#x{tyw0_*k$=wf?h7mPu@MdXEDTA67y8lO+N`k$rk}i6k)@yqwb?fixsSE+N1?S1;@!M{5LZv&Szw}?rQ2&>KPpazOIF(;OA?Hlj$~PQ z@nx2D(d%@aQ?KRD-fnKnKv3-?$$y(kw}-Q67Evmld_C|sF`wt5vh>>MZ|+H6U<&zw zv&a@$K}&wGrOE?_tXeBeUTzAV{Jlx>x8f7vZGQh&HB>h0pH zBv%QW*s5H)zzQ}tKF?8!Ajf|Mw6=?ybQ%@|DzC3zpGD0wXj25+ppUiPL;R2<6nQN$ z?rES=v;9fM##5WWK`MMRr`&ebB*9*3Nf*6N-LEH47LF_I^#tt^_U0xMV5g8@i)sa@ zZFWWLz|ChTUqoo`eK)cVlBtE^H1cP;glHvgaT@MIZg)txu0zBM1MX6$F27D)%4kJ{ zEK6T!a|`-!I9YS7ZpBhCOgv?0C5X$}Q?H2Zd|P$toKCh*zxSa3y?Cc1!geuP2-En2 z40lC+;j+RrRP_qo#iGn{nfI4*z zF64IvfhelkGL$GXqGsbM;?x>is-4v&!3=#3UiQL^+6&6u#+}A>cXU*hx%PrGo=&g? zN`S@at`+tFAtLQ|ksD*=@4ACXjP zGE(sG7ZPL_)BbG1$B(nHP-3uQxM$IOJ;TB(*DYMQq-cN1o(OWNC0!e$2RG>J?ymH+ zv^2nHB7Kl1YHWiDyn+`g*B#X?-RcJm9lBZ@QL_p3!H!k^+DybglCmVg0+}-_*RU*jb*nbzaj+4t*JsIQVq$Y=0%}y3T zn?foNZG!=}PIpB$ODo%kY1J(Gv6%E3EZPR*6jNhKuvgL7Kv3>3DaA|O>8SxN*9Z?6 zt19$XY5e-AZC^`u1&GY9V@YHMOcv@h7n6Okh^&MxGrvS%*4X3>N-RnRlmm5ZEKw#} z30mJ)ZBmrE!bel8x9!S}X|zc)@@?mJwza?6-2`kG3aWM*eSXvnGzgU_7Z+uC#hP1M zRj5$DoQ;i4j>(Z(()CvA)WA58!cWj=64mT90_`}BZ4mESYCH8mElrE>)uL5Sv#42A z47AwZV6qMdhz+c@Nw8Ph*MMHD<;KL+(7uH{GJ0BBsjnwlnDwTeT0F?T*f#Y0sDl*F z25YOX0+DHl3?(~YD&KOFRj`O`gqbELCA3#h_RRIYD$_~>C1k<-^Ql`gQN~sR-VxyE zh!To^)FYxIEe>i}>fY4k24Mx>xm~U8?q&zud4h#7iatMmtgV&4`P?!RtJX_sp^-VBSUvStk40OAlo0ox z#p;O>WC<8ZVpW0(-!@20 zl2Kwaf=czcA|XXclBvFJH!GV5Q5x7zBfv6fX9{SgteeCQE8TiQ>(2f#pgIIglo2+JO_EJq!v#o?On*L-JB+`dB zL9h+FYgL^#3HGx48i*Fj=rR?4@9OBNL<=kVCJU?i_Tthtw<3q&ZwOSIY-cX7@`liR zHj&D-CeSkAbI2-~hmd7omO4odrpXyysg$je=t*-a8z=w%iCfDN)m!_dhIR;~_0wLf zi4IyTfvXaDf&(p;?v3v=*W2R%RI?(pp(24kvm=D2G}D80x&_@VtsjZ79VftcwqPL` zO1^YDB^o|w$wf>4-m!A!N>!YkHvns!X=>y}>`px0+nvomQsQJ5WtlN1uX zN^hX`p2`x0>SB_JlMJk82{1_nr7X6AG2pPOdlh{RShd}_al<9v%O|a?jZGHNCJKv8 zVmVi&&N4mR$&J_Is;(yN{@+BVKds}H%Gqjy>m2fGv4P1%p_FTYSjr_fy? z(6UWk2l&^TlT;x*1??5Q0Kbl~6{`}&5-_f}!!DSoE>au3c@6j{Mzm~P1nEt{w5_#1eLY1@ug&)Opg7BAd9z)`6iVQ`6J%L zLm38F-|-MLA?8ucftUjECIl8`N9pt#OX>8zhzho1j_yEHyec3n_4a63*p@zz^3Td)FJN!nk=1l{_`osM%P8>ktMl!KpWO@ltDN z)p_agGcZx{s%L_+dY=#+wznmyj4tFi4wqIu8WtmrvS7G{GXoZFYMZ_7zCu}=b3 z0+bNTzf8bZt+l1T!RX0PX=?BMZWlObw6GO(8xu7(*^!`X6TORHZXFa9QpwS=WOm6h z>DT^yAgljs(+>X;zQrF8a&Ry#>%nC!+CpsnphR2fGupa)*J7oIiWHS8&Xh8uW)ZT< zWFMeE+mC$84pci^ml6`Mv>l?D^!sS}#=^ba3p{CWWu<~PnqF6LvJ5Z**2>IW>Frsv z55k|Uk(xB6xUIU}4*9cq(owQ&GF2B$p?cZTWEo_cn4s;mOREOt#4`9tZXHqChS|H{ z4YIL+l3J^E+kZ=vOsq(td_b1+FQL}98GWw12@@NVh5!4Mb`Gv5BIxsD47QVnY3tZK zAx5WDaFR@7$qI)W`}tLMcc3vm$86k*TjH zjvVt}Ew5=FLrH{Ayo?B`*JP?OP1e{3Sqk;3`A2SNf<5o+wVdB7s&iXsXQe&d#&xIH z)SI?lNX_=MbNP!aD)5$4UG6&58tFhibq8rW_fMvnNLE6Mup~`)s=IBhZJ5KWMrbXA zyryJo+qpg{X=$w_m9Ia8tc1LLqGy{V6Dtyom4D&?hHM?RA&q8YLk!JoR3|p@-AqS2 z*B?96=O|GF+sU;2k=R=v8HP%Ff?>Uv-QSF=fUn>CCSz+kcsqIt@#HX-yX+aYH0XG7}k zZwczX9lf3?iteOlo8oKrC_R=Nm~&YdCP~N|i4hYdq--V%kbRRbE`#XxcGAv#)4pEq ziIo>`7pvPiJt_hh<9_~C-=m6z*8#TKCdtI|FJrK!i4D(!E&k6Zb+U809AN~u+Ue`6 zg@B1MYip&yn|oUO3L&*?6e<+(tlk#VLU?^aYqOzOeEWD$PbCii>7trlKq30G{Zunw zm(eP}R~_~(sd-EbvHp~M3EUnWj+t?K2X~kUDMNPiDQG*=r`ZSs*E1;?!M+$d1f!Jittawu8&@%YP zM{00$yXrQMXn3ZQzjaI+r`~bX)%LQD0V7%~FPTIBl_pHkO)OKLs94st8fe_Iml z?13Z2MieWa76j_%x-9tv#9qj3ufk`l4TA7#VSV^FrEtGZjP`%4yA0ZqmIn+RyZre+v zW=9I}Nu+}Pp4#j?O?~ux)wQ#HaXI_ZM~)rs9iJdjn-O4J(>@;SZf$>`%FBv!+ee;u z%QeC@|2;M z#Z^rLDle$*<5Pi3DCJ~^*EtC`t8$%VgWTMdBnt}_rmSXH3ZR`y>mcC=BoeO7-97wc zxTaKqPb4?rpqkkf0&NOCe(F}5;ltBUb5J|`!XF^?q$kyB)hi5?1B3Pr3QU02!3&9d zvBP*<&t+YhykZ}kl3Ip!rUB2IU%h!Yp%2WLejJ1Bu<7zfr$a2K`)tFh*+(Pkz z_pk&mPFO7HD}-&kcI|XVo}Xu7Yit%-E@0bMQ;lGNZG^qcj4vPz(%OhAgVtzRm@H zqw=yM!n9j$!mk&%eKa<;*jPt0W0NtmDPMan)T{_tzU9W~QzUp7{Qs1q1ltyFo_A+< ziStNp*UpN+=NZ6#ZD4Dp&ekxgfo48tfo+gpKOXC|#i_as+Z0l@iCn7=cU_(A?3DOY z72gW?^$jU&WhMRE*D(n;rEKuUcwEZ}RV%=DDJa`@j*iMkTU%ufTv3-1XwgzgQf&iJ zw4Nnq2{WXc^K@HY`h!*(7)2TQkyJxF*qCjgopSrC`p#GalA&dL9 z@IvBKey}lrR-E-lb;4qQU4(3cTJB|JSsrRol4V(**ImtukY&I!lrVaG1sp}t7DL#= zd)>e}f7-aPj@1Va9B6ICQ_L&sHrKEf>ufdCb+hzF5Mdi?DRtiptX8?`1l4R{QWaVxVtM80Imz1os~5Z8(@DE0Cxk# zYS^C(&>c%ly!6&tJ_)w8t&&X!eyFC@X(S!f+6E2847{-^x32;|XqQY2@7lVay@OI4 zYdG$a{#s`ith7*x_xfgioFrI5skRQE|mjf_05Pk?A`XXGdEXwLW35zhQt^= zr?a)SG9bUly%qrhHLAF}O276s0Pyw;8W&Q!xAN`RIsutqxEZLeQh30f=AkyOClICBmlS;*1cuZq~qp#Xb;Ax)3Tt%ebMWVE@P7S%61zeQmsPR|0V(A#Nlj zgdoA)-QC@-NN{)80tMPqC=_Zy-Gx$G{?JldTC7mqB_uoFdxmrK-3%L}#9;G0=b72f z5|Wv@zd7?Bn<79f(z+Nzs%#)qo#a~o{4Tkb_1U4`WiFOCH&>Dvuw=Af1)f8HpQuK( zXeh^3)@Q}U2x(|ve+KPP_SOf9Cgx&f`crzdMY(h}OK(;SS@Nz3M8cXoxL*mtF%DqZ z##ve^qa%~or1b7>-6}lXJR9$}NWoTLo~^!eUAe_#HvMc>*mksbxYUxSu^V`d#im*=3r5+~YqD^ACfVT0Z zKU+KApar8(pA|trX>59bmI?;5Hr-FvsZC5e$lh!*&os!DY#=;5(Q;70l{_rEp$MyQ z<9vre8%VIljXMh0-tdqL`$l%^WK%CN&?1+xH7ZK5)%HdZUB5&>TYj3U#ITje;t1G| zQDJLteXk9}b|9}ed2XGM5Zb{(=>&2mduhGdobqOinKzs5 z;6vfb3dqvmNas_wRvzI#H7xBO1~P1e0ow?GV{qxR-=*~LZ!2J11ijl_!&daOrSmN& zHSo<=ok1oKe3Y_u##FShn%CaMEKTItYGEt9+Riv;Bq0p-3OHLMFt9?Dt!;!>uQl{T zC>oG8`}5mx%S;IhP90@ur$TfYY!m6Suw87lzX7%wM$$rGI@u1cXS69@44e^?H>bf{r}S!m4r@BsJkEm7it`CFa%n%SXfnKV_i?5$T(S_75TFwyFxtr z@y(?lml_XXZ@s%dZ_>mC+$od8*R;^@anO25^V+`AH zz_y=n*oo2IyW3Uu^UHYbEpD(CEf(cT4Fa}ex~`s4=AycAOaoiz?GzkquLrinH8bp} zxTddn=o=MXTr!G%+nkBw#!F)dMab__&ghLt(Lr50a|*!j`UgBSgOA! zkJdHdZ8@b&4gCCnA5PKqn7YhQlX|fZmwK^gDI4Tx$*ruc>H{3~x544qe5_o7F2HXuDl$K}!o= zK$eaxonzSs`X{^`Wn}yqw6G9{Z5LPH%VT=?c4!nAmvJ-7L2E4NYw_KNE#)To`KOP=aH=~5q+rCzM;#m?mE z0@L`t(Nfdv*7}MR3Q9Jho6OeHDPDE#9dt3cNMjhXQIu_9Y@+Mx6&WcOhKL$V9!OoD zsL)=3RvlsUxG(M(+C{@$hRw$D3K6mZXTXf|5}4rTQHz?T3fTx?osaU z%0!%`g)9%e&Vbz~fp#-Dmzm6uD}$ZAj!Gl8MM_tuSy>WjDG)Y+wcCFDSlwLeZBXFZ z2GoptcFnf!%UfG3QMgE>7_#Ke_ON$7&)#M!5iyn3R6DF4QK^q>88Vv)TCj$_3;S%ID|Y}0xiQ9*NM`~$@Sj2+ATsl zJtwf`VT=AWsUy3NUusi)z_yZw&75{7rs?`%JCI+ibL{}#_Ey%qc7b6X%R4y4X-6Z; zdaXf1C>Y4n@I}{IVUf4TIy$PY*_N~C`W8d>J=oi@H((pNV;9w4Q~qo(*V0?15>zq5 zWt8mA(&!EbZi+l!n?1qCS@ky1KH}uZg+PT0lgn1y7Hes#!d156zGbJ%`fP|a)mK#a zaI3=(+_1a{r*d`Ul(h_RR?VA~0)2UyLSdL7Hxo}omy;6Q8j&bVpKCk6*AQ(Rn>MRs<| zM%bIMcVP$EXnz-Mla-ZPs6c_RK1Rm6W+glZNI|e(+`xOBH2HP504?jYYFbxzvVrJr zkl#fL^hF*2dzbV*A1(nFzlT!E*i_fvJ(Pk~&O)CR^TD*q=InKJ;S7Ur+%P1=n^hkN zaU&sbSd8S6%Zl?;8Yv9WzNDvjtI%mhFpQ2X-AuU%#GSy(jaa;e(d)MMrLJaxmSGzS z*fzFvN*!0JN%_VVD`x6arRQc<7_ilQ^T|yU8f~erl+~g(p%Jj{U|;H3L(Vi1upP|u zK_46=@@m`Sbz^gLWuSlfI~ARs6Qj({GiK2lN^}bzv?bToY5djXfB{M?bXWifec#CQTAqu#)myG-`Y;D5sfVE?`MFP&JV-3kzM^-#QD-ar8%-qG_U94h!mFjASxc4>I+Tv+p5Wk6j*D#!CAtL+ zS+kpG&j!woiG47kLrqe%|ip)UK5@78zdcU;?d!>uIU4K@XAA#S+0x zhHM6CdCGuD>r#gjs`@TZQYgsY2J}T8{%2NeTfV%7r4m5_7ltgscDP^69P+$4SmPxZ zD#)og5@8wM>ADtBfU*3x6NaLLUH=a(W{l`Z3+2+9-W1BARx=pcnT8HICsDH z-XV38O)b*G`23JXXA0JX3Ne5+c*yo>l}33A`4q>^_VDVkZg6=O^l&Lx8Ol*_gcr^EpS`1(;s7VSkXeUNin-T@@C|p{^l}*gj>)3la6AXl>Aqo}b$glt{7bJ9K;I?DE zT6nXS*b9Ch3KX8HS1Zd00<@f8>c^U8fUxe~(S75MO?2WMmBsZMk0+~gutrWU=~I%M z_UqT6fm3mVt+skBH#xRkJ*GCHX<*~LzL}BnNuoc`q=4}bG0<^ohN^Btq!ulE+>6$rv4wr&p6cruK z{bp5f7W?xAXoWtjP9J!zW42T)$p9iTfU%$^DaaWIqXOd&0FW7vrDPrKN?suta6-MC zXe1*#c8RF4T4?e3k*+-l#M-m&+*Iq$Y6}#egCJO*G7v-{C@Zg)0K=1oeK=n^fIYi1 z428fxUXg2}UoujnaK7TLtyS1Y!efiYw!WjYE~RqQp~G6Wa*lF!waFn!R%BqCRY5~8 za%@GXxFPKwN`Bvvixs*^ukNcZQ||iRmVnO%9lYxc=nJrzjES4 z$c*ra)Jb>*o6UUoXA>)-^~HXco|tqs`9&?rK%)mzYzQ;w}R?TeqHdT{mF(2h3F{}OE5NU#;4C9jrB zcpI6SDSb=0Un2=2+S;138$z@xUj_-`*@M<`bJGqh#`^du6LA6-vot^w!2yOW*+;N< z8MNzpX8p<@7OOTa7EA&}8txHsc#mm!Q3raSipF=$kYS}nE z!tb1sLX>D?s_Roari^%$qep!?5GFb3?9~d;(lIAbmLH)*=tjqZoRDr#o~K2YNE8>C#1L$89oKbm)J>__VB&~&?Odvs zENPpEjD!M*ZFV`fbUZ|ktr+yEC+#bvpdrfGbYy!o%ZE)Q*mh^wc44o!1+Me@Bq8`m zyq@IjTp_~TJSsO`z6=t=(?ga@Ze5oqR6I4o%}tqYW20=avr~4$_QO7feZ@xmAHaLq zfs?d~TO$so=%%yRAx#i$EH{jq&469N+u4#n`$Sq7d5N4h<*P49lwaN+>9e!zbc{~0 zuvC-b1!z0imAXt9nzm3u4uq-N>*Gjpa}I_HYc9N5QEsa*rRzBhf;|;%wGx8t)pAb> zf8o(GXu{}*%$0dz?QEQH3ed)~QA40|1Y3It-K6+h%X>F!lmS}-j^Y4YaZI$c$23n= z>Np&!Yutdkwoc2N85!#cwjJ=4)(va6yX4gpfSZtnU~QA$%C}7Wa`yHKB6jc;UA_!o zxd5%j&%54BoEsbWXojb!vedyrc@;o?8+H)(1?(FMwC@4LJ78-7+!QrA+DPf_;mzWENVjkPl?g(#=j-V`raQ z^~@|EMY2a5FAT6vP+@DYn;c(z(Fnk{qKk{2$Q8}vk+`6}HNCLSP1+YfRXvgRC922T zI=Fs95&~r+wC8-F$~jL?8Po3(6UZD zQ_^QM(z?{%wNf>h{8(K8vVjWPK|Ya(0mqD*`m#pG%7DPcSaFf-3xugz68{g{W-Ony z;0%O@!kbmgbonOo0tCVGtV_zU)k+9Ls})`?AOA4ER_KNoP20{Mq5Ufv8|wsUMIT|> zR(K3sWm0^d#e-V4Dya{)g_vubtF$kx%vXO5@>~2+n6PU zKBdE7NgxRU$5Osd4HCk$lMTF>0fJ!vS+CVf2&7N4oRs0!GGwCJV#s=W zglvq(6ILt(wz4!Gqat8i2Tx=Z%hg=fw^_4N<(-}FwZWdE2wQzk!{xLu?bDSu?Tf0% z+M8S7C)l=P*mh)V&j4%OCn{i8ZwUUlQGtuuOX=&KqtNC-~{TC3xG-%Fex8lIM7 zYpbSfZRVyi2PMdUD}i=5w@jSRtq})XJ6_XNf=yzHKzOr@8MJeFbWMgnt3GDuBCRW* zPpldUt7qr@Fa$S`VD5i9&^_#3`QnxXVbZwhYTV$w5eL4Sa)np}@-Qe3N2GMUU_r2F zs@IC-eeQBb&3r=m#udfQS7L?ra`8J7!xJ$oOY`2UQU^9Xtl$fb+n!%HH8N7_k%Zvt_gBS|B`a$s1Z|^m zgM^TG(3X6)WyjYh`ui)>a1l}ineANt^(h1PdkM7rIfr(glyT74(c>2p1moFXQyHp@ z@OlXugLbx7pXF{{wVpDq>zRB}AZ%8RHt}&5mTGh9AXTF^QicX5cAyJOZuTK6!h|-a z>aPr10xUneHIls9O5~}tmiVFt!Sd|A$w44luazYPy4eY@mXr8nIEWg@PetXr*p<8z z%bu*5U@XSPS5z_X33zN&BS+L>Tm1k%56>dzOK|;s^vec=|dujQKS0=f-s^FxQtzEbf zKen+^zQ)D)BkYismGT`=Ov+vtwZ&e=GG6(GiV92 zb=YXX2E4Z>cJ{i7Nbx>$WTjz_lc*IRBhHM+&r}ye&4Rp14 zdQj2Lty;8|Rf0i6ct*0pcWbxQnN_x|Zl;@?vVt4PeBj`qd zf{RSdsOnL&^ih#oBL~7HO-v0yNT6-NxwH-FH3#?90)?!C3x-C^IS0o?eyyz6iYx?v zLI~xaz-1Y<1cyYjwyuH0Y8V^qDzhg`kgd;F7gigvZE9z)OpLENuV;-Ku5nII>JwBE zDQw7fPXqy5xhH}c^2no=M7J+>SZEtF%hWo0U`t+Y1FUPh3H9?1{ya&_LXaEJ7$k(e zgVy!64n5zP5gxA00a(`qqHhsQ395h&Nd`Z{4&&f|54e8Gd9{?=V)K%E9GKqcl1;XU+_iElR zc@B>Q5cSEp4Pq_wX6Z4GJdCV@V8y&0V!p#RtKgU@WDpX9?A3B^b0SMoNj+SHUKR3z zcx=uv0xhol4(=Yx%<9c%jc(i4qh>LKZ610ev=wPz^=wNlZ3qXcz64ugbX@9S9W=JI z(zW#s9gtvWS3xTw7_t!Z23ZqW;Hu;rSEqS;Dsx#jc#8vG-!f#6uzd&nj2nM%19+DM zye|`It?h1#AQ<;An8i7>OR-(Spk2gX?iB8CAoN*v2&0-N#rmwa$x4391_HFTY@HsK z!QVxoEoWk;Yv&Q?B45bdA2mY@W3MR#w5ldc4bLe9;c??~@@5nC1;JjR+}URy9Fz50 z4w?pVflCB=wF0y}CWvm}m3lgR{g%X%K?YplNVJ91&<3Dw>Extza&y;Bt=w?V(B{qE zi;!oVr=AE|jlz+7B8XXby*oI%eOE(19+G@e-^fU*j^op-g!|P>E-qDL%*^y=As7l7 zatB(Q4`(fCGd(;)Hw72mQf?sg4g>WE2J9~qXg}f9E-KBX&R(Ob$AN8$vpf@QEPJf; z*qdFh0qtyVFq4r#%lfQ3gt3syZq@$O{2#$%zWx6SgECepejbg7hfu{>sq#GWcr_R<6 zw}n^R91v-bYrYeVfJ+f~3IcU;DXE)QwaJ*?fNhMUW5(lg7J;8agKb`dW7?hwA`^jP zDjlpHu2+{}+nkNO+S+hBYEo{kZ`js2JG)BS{>cUjArG>_))pOiO!o6rX5fHQ18jMaKZWh%@ij9! zwX2Ooscn?5B?ZAKXLbedn5(c|$pNwXtlf^+mp_}6KAWF+NeYAw3rc(|2#c;XHIOzo z(G4zLzKk3Q6Eng@#sS?J1ZdS%8fsP!ql=jqHF>kN*hwqPN!v1b0C|J0HaI5qT9Jjo zPY|kCtB!wpTrowDF>6@F001BWNklt}wE){5B}=7G zuivVZ3|sAZm%@f^Zpw7C2#$$yOVly1M+-;SS4HZZfNc}u)#7+mGc!~AxcmQ}=;&BA z!py9ksNBp>LNHv@{|{RGpSJI8x*)Ma`aCZ$HL$fEpn0EjWe>xS!j7^1Oqwmc*^dEO z>VNPGK0gDmhZq^@>e@Iil!IWb%@SZ&Yd|}V<%NOS<A)w!8UD~7CGl1dW@THreGTgokMdYz&V+ouVRaY0QR&P3?h(Z1xV%2HN_ z2o}=oYB)~yEG(1`exXB2LJ+Bn`g$Q4+9^H>T90jQySz7pdK=&%uI4EUAF>vEgaP{- z>^Ot=8xE3Df7-Qx>{JSb8Jnh;H@BQbxv|`pi?rAk1X%%E)@$eRIGa)2f3~BV*k8!` zvmy}Iz|rk)0RAR_>TFah@QC)J6+s4(+NA+)H7;7Hrgrjr9E4F*y0jxgwEaB|vcZ4P zM394HBIS#2a9olbFP|szXACNPV3ekD_2%wD?GPs zI)CLiZ2wbmO!Vg9$}tffQ!^1-n_E9f*1)ztd%9HM(ao{c?W7VVsz+H_l{ZKTxt9$} zudY)4;&e|>Rcocb1^aL?zl5iHn049TVSm7WBk2Ga9eK1=74|yK34={0(3+a3vX?rQ z8^$bU$zTnGb_Gib1mQ_)SFgwYXVv}&{Fq&?u<|rrWmwZ~xTiZrN*a-dfpn(`(mh~= zAPu9t8A6Ed(XMfmwn#UOl zgaqj&D}{`}gcx4C=ue55@vdIxAp;mx$z|0GN*#_&xWgabKI3g$Er@LkU1iE>`Y)90 zzC1JLX(+{ry}k-0sqX;gd@UE779Pl~c;?-Gy*j=GqB%ajb_bP+!cF@J+orB{D6Db~ zQ6CMN6Ka*#cTW@~J49xDHD4Gq&Z-?219pJu6GrUp)Z7Zs7q%D}YKB6jh@LCX7r~!) zu76$c4h)n{_d5GyJ?GtZ9Tb=&$D0 zOrmxA-Y`90D%#L!Z#SD0Q0p?|6yz_K)Cg7E^jv$^r!!cqaP$N`Q)okdH}`Lpm4_%s zeQ8@L#+OjihD_Lh$KI>etMw%v?RzNU5aYTmtmYzMF1fm3iyK5iuaDYdMQ$f`pMey1A5pOCASML29^Zw__;Y zo4+x3SxNck6pY>tq@yRlhhN?o|1QZ?QpaYOp*Q>Y+``2V=UCqAJ3G#%w~ zIDiwoVlEu$?Vua$;ydHr%jCau8tn5XQ}WmTMTM~_uehVV<4PSq?5Vxv zHhz#lgfW6~61ZLaDgKXSL02Ic9{b)ntxeRQVWnq10wxWVT<;YXbrRk&2EpO@F#KI$ zon)?Q89@UgnL935&hUtx(i`%otC8nmXAfI*Hu^Xa#f#tgG(4Wp*Tg;)Tps+j(eiHz_QE7{U`1vv zDl;Ax<+>XU8;UI2*FjoaELH3LNXONJ=qHVx=J4Y-y~bY7X#_YwY*XzuL1Z0kH;U*I zeiBIX5ny`)#OmEv7Mz1w_Y*?@)#63Ny@;jv)a%HS5;I;Z3}2W8V5U>P#5GYWC_m(q z$s`-`3Uy&seR(D3ap_-0-L7vG(FPINGii+(A_t}_iOx#%E z0pPkJ0DFzPaszqQL1Nlb(vRnCPRiNYwi@2!T8jP~lELV(g0@GJU&2D%YT0oKcCYv< zJ67a(pW$|5+1O70RqP*_|=0vQhKp zdrR!uFi}j{HNE-S!z9Bpsv6aLIL_GYlYJuJwB8O3v}penJoC|tv^sGGG|o)79mXEn zx&F12gUr>%MAIMcJ#)6P?Gf#%BDEDOHH6AlB*@Pj2jkY_?}M595K{E%=@4WW9wO-# zi&+8-__yBAZkdcZMEpu0a7<|*1*Ukn&rUQ;y78>PVB5UAx*`q|S)CFv(eTL}^}3en z>3F>$J&Mzln~RW^lM~d-CqyLeg@r^A?!T_^dLowU^NlaVbmIQahW5xO+eKMe7>6md zGW5=9V&ty~8P+SeOlF>_O#L>yPw}r&LrB_?y+)mlY>Of(Js=VYL8x;~`uY(5)xG%) z;}X={Au|lhuqGANmCxi~;w0(CdZFe@NQ&58!S4Wu(vppZD=mmjK{|3{#-2otai82n z(t_oQPaFe>uvS0YAa4YP^aI7Ec=TB|!}8}F-n&1YyEn67ElX1i4G9g3i?a)owF-+h zY0Qag8>wJsf92HtFIBY$3V&*ZR&(yncb+qJRg&)C;TzTOViFGKN`{ab|qI- zpIHM1{6s($xeatMtx^&+2XjYUF;mNDuwU{6FTR(N!d42uNgFX|kgwL#W0iz8 zoXpyOA-)vbtgm~@nkamCzsZqe1OGO)_1Rf7N3vDZHOjg5l{$7G4J2)m9pWby-AUTe z+|}GHC{^|FP2x>lER`X07yeZNz!oMQSw*QDEY7Z8V_BD#rHoN^iN2ap(*0Fs-HISF zCO4byg{SjBCfMm&Ql4;-ol>+QF7*gbw~v^-8nj3I^-H>Emj5e$nj~sWfQj z7=Vm55+PC5WT8gm9tDUG5mFP&6a(OlAPjhZYTRONM1~h#Rm8wx;#^Jox4_+6{}<%E zI$|jiGi8a5-$2Xx)7Idme|=5zj$*9N##ns97&9G3sJlkqo9X9o`K?~Q9EuWYlMD)7 zNQE!GeC|+zhiS%c4M^+>qya)p1@TBmYfEXFQtVBon2XZSHwo|&Eqr!g+~irr%ov47 zn$A$^`SWMSt{<1iMXZiUTM2)-2rsWsT}KqokEKP!aVK^QKJJ%E1sp@E;AdA(2?!2q zB^90ok8j|Zl-&o)2ny;+)IyW-KECrC;bDBVDKgXvw~-e9!pe=2TGAn(fxROq*DZn+ zyll%>X#4EtwD;RMcdD4Bwi2tCbIa0FC#Jc*vV3`d2k48x=J~vRW$e9t=9<2K>wZ&h z)WM0e+ZSR24sqo@r8L5PW(;c-ke4^jn2GRPZZeWeMQzAY+OCV=5-e3)Qj?@9Vt#vtz;GMn?-en>_hcagL^tH7=VlE{~o@=%|Q7v!z zqZ;M}NSGCMvh|!gm-*UPm7%s9NE9!|&Xd0q#{Z=4q()s?6 z<6ys-+vd7t2Lm%p`x3q}lNUsGNWvug`ZZ}oMN|4~`odQC+gB}SIuY*f_;HN&_p!W zE=9`TYWoOl{pm|zB&1?x=l06aGg!1F9mWwZ$3Y!3k|vYr?_BteANOd?lzkP%3Ny+V zQg#0(6L((7vKfLu$+zWdkv1zD&(7YlPf`^~noQU5V}#NQn*pyRF-n=O-R+q}T#TV} z>aUo}lkJHo2kRCT6fkMGD5b*>k#;bMZNMP-{ApKc^FWCaGMuz**nr4Qd$gLX!G6a9 zrEdFTNjSu}c`(t0TxLU(bl=7X1s?2*I7;~&Cs37*QYj#J-!^0<(1?rA*GHu-4Bq)2 zzPR-lUE+8CPz*zOb-rKi`9@`!IQ;uU$&xoty#IF-721-ZX->1O#QRiY&ZzKM{=bO{ z+oWdl_QrzWf3U0L#-SUbFD2bci=@-oIQd7b4L7U9rR%PmYh><+m4&Eb<Ux>TQQYA!w99hOqZ zJKBiAvqN&@zh1w7|t9Ku{>A-yv-@s9463grSH+KAe^_Q9LOHF3o@|d)8 znBcWu10uXaaL>gV})mFHQ1MG|Gt5kPBD=GruUXj<98Jz6H|r1F$R} zRDW=ungRld#`=IRZK@^)xmWde$64Y{x;+ADwBuj>`jwHsejb$YNK`oocQu#$rc3o4 z{ChT-ou>mxA*AEZyEqc+E@Dm2&?BlW>JKNbbD3G6oBxb}R+r|d-6{&_e^Vt`t6dcK z*X@s&OeF?xZ6Y=G(D**TVx@y9oi|`D)jY^2g#8vxEXAaR*-_m_ixHOWt}Wxi^3>J) z2@@&@(TT0M8DTVK&3U7DO3ptp2%@WKaLKu0;CE03?Lz3;NV4HWPCDKsbdR=2WR)yf zz}COXd*+J|7wUUqk=Dgu73Q6HlzCV@3VWu@E0TV9nH$%zP56-X`7q4)@UWIxJ=L#KzAh-H9yH(UR8zqW5;Kw*BwSk`S^?0cej|d|C z2C_o)-Zm;&`)eU)o_3SzoShXD)W=;#TALiep`u(Ir$&tkhRLxX!sh;PA@npuBIKB` zBbKuj!>TFDpMqc*&(3aSQm-tXCZ%zYI+4K?;S${Lr;D0aU4uL{X6>H7qr>4Io$+Tp zDMTP2!UjZjOh$b{n_HZ*`sher%I^;biRjRB2(&@i(brj3BXNmnm)MLI*K%$8D=qk6 zthwc+h5aZN$V@}7$Kl7=GTI5P**k6rO~Xk7w;K@}7(HvrqLu#JWXyU|#~)FILV=T1 zzyXDzhI8ww^2~#!e$I)~=S4ddFhR3x%8?{tMUxo`x1@t4+;d1d)cR{+?LzHz)W(g*Wf{`jzoYtDEocm!_?cY!9mgF_{-^C zgFSalzGui?_Aa2ow1+?xEq#sMk@)x4PbRJ5bx!UeJjAbGf3$U4jY_K=u?2+ATA23n zh9nt&HCBujt7UlqmXS^u5e_G_e8ReUMg1x-!swA7VMGHJvs|#u!h@k5;fG&yfDI=% zy?RSd?5ovqO=+MFGnw+ElplEfYm;)8%1%6r#>6LYipFlIO~Vo2nkIH6Z(PN;6SUrs z%Fw{_&KXB^=bqJx@kCnUEEetK3_=g*a?DS{_`PZ#pU_fN5eO&Q6VKQqssegV>mmy_ z6Jlo@8nEd6f)a{htsx%{Wo|kBOY9cCV!%K&25TA*w2Cc8D z{n?2vis-(NORhh}Qo&}yu|ae3v-K4R z9+v6vi|VZQ0^2Iy_ZA$p%nCXzdJ%7qoAqEok27jAELrgEz7WSVg1we&45fy?u$-Rc z$%v*HNu|REOJDgd2$}=*CoN)u9Z-GdMxNbQ;tr5MX*T}EkErV zBgW%tPYhMKn00Pab9;V*iE{NeJ4rYycz8X*ae(F=F+anHWDL~%5hR5iEu#r0tg@j# zvX4Y{N^H_*&F$t{TwAR^gbB@!9z1F&jQSs_&Vt91&e?VKTY1%6+8&@WYx|PtUE6X{ zjK(fYY@#zAme1>v&wo5f`F7?V|W8^^dP45R^)`74`cNVdUmg(K(B^;584Lj?`O$a{O24&SE zLm9K2*WfeHw%R1(F$eY3nT~B5>xs7kl1a4zZa+)Q2zh`8R9p3}_#AmQnA0Gxx+79# z00#GJmoBu_6Bl)O4Co*gl^rH~`yC2|N(%=@gfIDx{WAy6pN!SvB7%YXOb%~J0PQOZ zAKLx3#%zA{-{{F!_!1}7OV#G9{79+2lA?ia`=oU$QPaQYM8|-F+<@;zzVze*a(Wu` zmT;X|qX#Hl&TLILz1;}>O-y+RIZ=C2mMu{ppp~Whfuh#ty2*PBs>M>(^t>dHh@1Z?FZH_gaQm-qHSf4u#q%Pzr_b{0 z@;fT(VW&asgXh;HBe^0&IQ=ROb6Kmb109fP`I#3vh7ic<%ra6*jA%hlW^O9_IW6|J zDCW%@q{7l*kelw8M7UYxiLf{?m+c5A;d5>akX(>&#_IJUwu3%8Av!|h5Dp?Xopu(A zwB7-@NE!;k-=$d*zTinZ$vYcdMotPR#7ndOzE``&umMD;@6 zw>&Q^$*qGHv!thzb4PT}Favu_G#H(V!q6H>H$wy?0W#>F3ipURSqUkjjX5n60zE1= z!>6^&Pvkr>O^vJnf>gCH(^dZB&{A*S86V+d77Wyv=S@#)2cA^#?x6pC-`HFoeu+*nW*J>@Dq0#1h>^Yb%Y zkmLR6^iJSZMwbszYQOsCt8m{GPT-fNSEA4R30UGX<$;qDFq>B+^S3UPjIIUb4*11H z`fi;ykDh8dV(4E(!)}6wBI!_-#s2{QFrB-7v2xs#60MV)7o((pR1l)6F{6UcO4#T+ zZ|$PPA?*~eUG$Vl_j(HSYS4~pr+%vge#a=mnTFchcwwBDkm}qe7IuuhQ{LE+HLL$)cnsnvKQYV>tBr-t6Drf0(wHs5=S? zzor8x_iEuUqMiN1@IJHRkq^?wIOz&MbnLzsM;NT1%kq6afxHC*N8Ddoo40>Dcub+4 z^t`XDB%lLO^H9XxeB|GOTES3ERGup;W$OInG1S;_^I-09hP{6a%KP$}!L061az`_j z41?N}#*j0OPf)nCh8h%R?>pK(x|92IaoP)(3bMU58{w|AnIY93K1xJ&o%n83>UNVTS9wx zk3S>&p0`2{S*T^?B^H9yY`G@Ej4oDWa;|UnW%wk)P)bNHO0%3)H3Or zS=W!pweyda4d<(auepQ5@@3XzjOAuJIx=X;QXF}f{OBY)z4j`_osTpewOhhJ>nbpi zhtMJzI}jvw7rtyyqAKk+rR#T9%0+c9)`e;oF0d*`wv9Qe$5V^&+O6z2UBc$ekf{>N zsi~1b{~B+yU;-(k$9yQUr6)oGQ4}NgDAm5i)L1tAI*{gD>uiKDdDx?2o~tn zOTH+d#VDq##V@eBm!Or@#Z^I_qwo~5{cEx~Eh);TY+ne+?dE>U`Pv8e{5b~ccI{7j z$N8C_>q$%Q(K4uaEeDAD5p5yaYR&SWN_A&+N|Ykx=_AzlyrXXA-kBXucyZ1!n}*6G zYz~q#hJ$*Km%IJZMK2B6;o4Cjm&m*ZiTu43cR)hm9Y+D^OV3`tVvBAEO{^zh1;CeB zVn3vvVo$~fOS`Nd+KM=Fs>{e^jG-M!%__ylQ>@SZ#r|&59mKLMAH!=Q=kBG?xS6sm zd(3xiSEV;%+kH)+b*T6IO#3{Lx~L7Oh(UX6V61K&4H^V1noD21DUNoVI&Xv2^m<~{ z?fb|VaGM*`Gx%(^wMhAej(j14UfK~C0SW@^IzP7YzWk`mpI$46W^v*l9t|ZB^|R6z~-2Nbxl|c}-FFEx9<>~P?E?N*)_!IqdbEP$zycF3# zLk_}Gb=pZi{-q2ISokSk08~+zvuljl;UJpuK0H)qzQXLDg{PkefBdTLh%k}{6}oBXcOg$dCZ;L^YI!K0a zM}%ZZOe}W-kD*j+@WU0iOKK{{6?};|oTE%S3oz=!{#N@v`tnWLpXTV-4pAOL1VH%G z`UFb<1I*}H`morPnG;-5UOrG?Ww^&z=2M6M4%JIoBVT3`bJiUZXw2Eia1g4Og{aDY zC$6;Uf9sBpid*lgV1*VKG7NjNOuE;ieAllD!vz>ZvX4eMNi|>-bIiximxH3omL;fl zXC{We4+N;=K*gND5Eqc*D@fs+U`2?N8ZeA$CJ$eO>ElR*1?M$!lqxS%b4J?fSp7r|Bq& zXP`5ry|Ic9HJ5703|}4wtUp0BGY!QXfDJ}9fH8pQfd!h&PpQ6v&bDe_7uy)rNSsn9 zB>BoD++e?ny3z33q}`9HLA6km4Vl`g5>&WdEAj#V|G5B|X7sZT%9E>+`@UrUPyhsUGw$@0=xIMEJlA` zN`(~d95nZ#Ei%5Pfh|>4U9}Ba0L-_c!(Zk^Jw*MRCbFWLgWNH=SI`PtP1q_6azCl| zecJ{4af!DlPBQ@jbKC+1vNO?+YDvC?f0MUCGi&4xF(rKFM$%BhL-Jv=9@jV#$Gy^kSv!cEzmqv*LCz zk@SyVNJg7e;bpQJk?wwbDJ38D0_E&;@Ls_}a6McyT#5&^FP>cq; z@Mn$u(A!j&ke7IKAY&r<<*XH3d}H=JBYEL1@Oh*I1E;6+K_I&fG&$-qV#Ap5H=4oJ z?|UJVa9zWl!o0T79Gq_->hg=bg%NQV4}{!U#fO4OP$Xb}A+M6t)}+iS%r!@<+4pP- z{s#77%Qs?}R4{ch)aaz4b^jbdIjmncZHvC#N>VSgnW*VghL0$n11*>JFH0Bnc3X~5 zI_=P@r(8Kz4c}J$&P*(EHCJ!%n1k_gx1nx3cN@0BzaIR~*b@LUgbPm@qG^2)e*7{B z-dtq_IL5GiJ?iGj zhkVRA)K|c)2+`GXiKwU$6cW@kNi}<1pZ-LimaD9Uwa;TTW{E>%tV$;=Dqwuz zGZgt+Sn#;QQGu}A0@sYc_iwT9r|&wQI5umTHeAez?s*`M4fyh7Jk=%`ygH=*x|Z`D zE+`*(G)J&bKz`=0 zg0Hc93g{;70?o#`n=>n%qdnv+)QnV;cVJXhAX6UGmUkbOm37bd$cdK1EUBwZ$j|h* zo=~)?!|GcunHZF_w<%=2a(SV>Ti(`J$>y-S=ar=9@f?rE(Fvu_l2HMwMF<4gQ|ss# zRBM|3iH)J5sbO?sc712wuIM*tY+^`Z0SM?T2Gza`gooE2HHI+3-s?8mJ>(#bcPRb* zZ28epqLIV-Yf^-f6)W9Jl*~4>F>OR-Lt3^ zt>x8~!`ezB9`*oOpaQDVQBDWIA?W}~Gy_Qzb^BjnhjAMli_yD+9~`P1 z8=(sNH(FKZVNEKJ{61rSfu$#MSeCH>O@aH*k8H=A3dzEIZWbuiQI7BLzyLNO?PsSo zS>@%AWoUwAp5`L#dY#*UKxFjf5SRDWSAKVX@%*(2mruA96bYh2>da%g8)n`eAnvwb zbAJ0HF{-K~1Yu!2hIvDN?kuQQg!7m`50Vgm?1OX(K9jXb__HY7P6dr1GBuI?j??`Y z^AR)riROk4tu4zNs8&-KIZWA$v>LE)ks*NA-5x@rq^6B74S**2z|j?#&0 zDEcv<0#)HKUZi6wD%H5-`sVLM>AxPScp{&5V~TFJXwyyGzDOxtww$hv>{E<+4R_=Q? zdBhDjO{a?TWQRPtHs?g5RTD7k;HNm8xA4j6df@W#_cPx}&Z~(u|&}T!WNY+Z0(mhCFzB+7VwTVEyZ@yiHcw_&mtN_uGUQn{$C6L*j8e%;HzDX8)c9ZbcT z@sRy>>h1#2I6v>)a&b%kf|CrNai>G)2n*vm<{ zJjx}^lbHK6Sn(caj57Bu$0_CNPh}qc^}D{wzQze*oDhat-PIfV1N8X9`N)NDrfj4! zpr>afZ)ji2nO)N{W4c5o()R|4Zo{%&<&6MI91?J=cx!mlscZnwu5g}Ek z)xnl_RDiikxC13)pUPVMvh7}K{7;d-IOJ}qb!p*toI!a7ffoiY5 z5c#-2hEKNx(Tk-xzrPJ3eb=~#qAGIf@t(`H=PGcjr}HjcSU~Gu;(Vc zV^M_yYvvW54I7hL)^^kyO3VF5{GxFEUjX5<&mBz~C;sBfVIiJSlh2f$ojTg$0T#$O ztbk_T_$Lv0A5F_IkOGPQPfGB$f*4^pE%PG=?m)EBD5f+Do31GtEJ|^gqymAs8?4Of zyCokvQ}fGE}by4;IlVKzSL*utBOvt zcqYyLp$(z3uue+3o^uT zVRy^4s_9c*NKvWb=Zqx}rVbxtWtx18h784DPIwv%ZZ0b_ZqHssfMjKB$~?9(w>`G5 z{0%>D4K%e{LZe+s?80k%YR0zbGKGZHUsyYNxKWKgPrGys`*x@?Qph%`_N093-lm6f zs#)9Y3`u|pLDsBPAgi6-4@T&D|+ zWAT77@dI2-x9<6^;-UfC69720W`7e(uzJMX?JEs_Ex(Ly;o{}E*s9HG&N&`Ol=0=D$|WPVMwwkgAS;kTkr4ut5jpqEkVqepqz?y zev|*;2pq6fYs`rcM*|{yBHP&cNc<(!kb*#nMIB=RYuNlNpQFI0!EOe1ZlFQt#^>*p z=Rr)$561#|iCuTPF^sDF^#O%?twXGCz-xyKH^Z5d8QETZ6rYysab5avGWo0x&J9;* zR~Bkt?zs>SU9K#KnbABU0SF~QV!4S!gZ@Ha&OB@{%d$}0#H2BP_eA1}x!j(!F}gly z=MOJAQ47h&n2aCLa>99r;IQ0#H?n2WFN%=zy9eosU-x9)nRLv5~Av=L1zBp=tcG0Rk5G0{lU}q-)4Q7t~u;h)v`0bQ2JA3KuJS)}Q3W94q@MaAE#Ua0M67yg&Ttv$x7~LLw z?tl&U5cCOzfiUV0=b*vvJ^gRaE;)~ z3Kif5HS-WrxJdAmeO)H~$Cp|KJe4u~o=y=TuT|>Q!ch5|a#=`P?Xdg>TmOlgoB)p5^T1fqgB1j?@kMv1WXN8)~NhHqXD18?L?&$I>02)v;Rc75U!QkdiVzFS4(LQsWbiCcONUzK#k^ATznw#u$i` zlqq&BA+e>oRT0}(Mr(l4ztQ{l4?f2R>oM<E1W2e>O_~qrkH%by z?Y;c9Q;QFa<{(?cjmk=kTOZzE=){14H5=`15a+FP_aK-He^>ISD^bw^r`q^<*q(#Akf5dLQ&sm~xcnSGhjaXeg|p^oG{Kqg7}`!1VHwBI`A|4C(K@gqJb zPdel0dB52dp1=$3O>*4Odr(TRis9?&`DRwy62NNoD)g&c-!xedJG+4>EN!dXWElj8Y#leo=!wPvJF0L%GFZLx9r~9(&V{94RRk)9ad5L}=xKHuF&HhE2N1I34PTS+do`Z!{acgG| zu^cXh3>$!{q_ad=e}1Bd?7lB8Z$JC)W_^pQpi>J~f^KwVYQ=1)iRi zDA&pS;(|%xgJ@c*DRN%0%nfxAO8>>elAiUeBl#zjK(R$`bo4$3$>$x;m(O0n1M7yA z)<;_1N19HKDdF|yF2l>ZySro?ySrAr0(Le`c=tu!dL5i~&Msp(74Hho@!f9lL#TZ5 zeKCErPBR5<7QId0;g-&nqkLG<#VD!A-e3Si3(lSXeUkzN!Pk0b1;oOi0)y=uySfC2 zW;ZPYf*w@?Tbt}?Fmo$_0}R_Rc9KEky?pH#A5dS`29LHyoxf6M5c4x73xvmCpK>@H zitl{HP}|54F;pOI3xBG?0;Q6a;%HYrcV|)y1eArC4@gQUh^VA#efA7JQ2^ZI;OLwGCMQjl6Ce!FQ6ep-FX9w{WU*BVxhCGi%Wh?e>0opKhQ$B-_4#3{Cv+>R; zLX`YF=}ji-wj=pH6-CLP#WugQ0RU&#xx2Y1G-s%tJiH{egUO^&2Dc5P4ZDp)UfmPj z5e9DMEycWa@OjwA{KH#k6hz|t+!yPHEnx0$wdT%z9%adqc5&7Ul43m4Z$AA=+u4b^ z?fa>?MT2bCk@$UVgLDww)4l;AAP~wg^*uxs(>Vgf-kBs)UT&s0s`r%u#K`4SC}TWD z2jT~#&pY3lP+QqU`1ck%agnZZfW8T13dZ_GA8$1V!!o{Q zq5DM2nDOIiM|-*is`GYG$rAF4BXtw64EU{tdL}2YV3#h??g1O-dNcPv$G~ns(Q25| zz*aDkGCcR|*+FnMC#OHozi{gEsL4xsyH!`Px;v7io4RP9ofT zB{REF_a0`Z7oG8MbrP8)k5=r1@z;ojAdF&YwLvI^zt7k!tb067MSW2|Rkig&IS6K|<@nj5@t690y4 zX;5{|!|X@9OcX+O6RwcU9wC}$z&wg;5|Jrw8i^XnO=}1|biGsT4pLOS!|3Xhf-GwZ76##)m}KP(0I5in4+lJ>to!5GVEobzrxloEiSg z5hOAU-ry1v?&y!y_4}d#4rsl%qwQO8tzd8Gk2#6HxGw2xm*vxRZFTdxrZiHbMO^+> z>UuZpuUtRDwvUZkzn%P`T&@a@5>Yq-NqztsFP3?E1%hvj6DeMO33f=uPsbl*!doD@$WND6JrZa>RC)1D7 z-eV^>x`_DmFl2|ZtIWijWZem>QIz4c6nru1fgTu!@O~2~(df}C;^E=+2s0P|R+y8^ zqXH>Q?Fdt1CVIR@>%2km9!1Btp_4QDuw-> zF)AeP38euhM35;%%(K5%wjUeqZjKzx3?`pUmqp3mGgB|(;vi+;TZ%+xKQpevvj=s! z&f&(ebxn0?^w+&nbiQI@nnJ?AWA#TjBZS%do%2lBSuyHfq(5*?oINt=pz@1IVC&iG zU-6Cv=G?g-R%*XGNm&;|wN7`pbE(wV!kn zP}SV;;@m#OlT<J_==7zyl7Lh>?txW;y}8q)ty z0n#yO0l*&M>lxqNOAGa;SZ+W^VxpjfBP(b3&8)#e9--n<^I(^qk&C7{VrM!RyG(&4 z<=hZUym*-fRMnlkOcPl)A24!|hS!80r?hHWh#gA$TO94`-*kT*dN7PH36@`9YP3t5 zz-lf^A2!B4LYqbrH!UahUxl`Z=^KEd%DGkeauL+dpjs(IXo;DxtErlHP1Xgf3ma;A zi1{E4A^ko);}q%g?xSGuxW&Ru{dQuyY{=n2PHr4kkwtDvH1pL`t>x$zI2hZ8>xh|W zQucR?BPzl5LgRfp3+Nc@c~KluZtOw_B5{%jj}*Ct5J@t>Xt9J_M$}`v!3P6|fw0NU zOztoWQ4rXebCSbQn9}YIt1@DR+%s415(gx9GUUo3pTGbH>2+YiRU9}2wR$iuW(o>M51^6wC zu6;Uv*;GDR>Tv1Mcq$vK`Lq5lVpDNu*^@e_e2$jx{(S!v_h0o-`aDL7iqyOEzD zu2LyWuX8oVg>CXf-9%s7_A~w8k5APG&eGq4Zgc1@F9DVvz=mk%BRDJO3ExIv^qs87YG)l&<9Znm_PqHAlGzdEM+w1dzW?^t; zt@GsF-=4Z_k@7^_Es_Aexi{)V^=9l)0A-1 zwYz6k6PL>Uk*r7tUy8Ji=+%A|6g>cjr79@Tr+H&%i*kUfJ)VP#@r38HhmXqX-#7_f zfzS+@z_)$AjdpFc)ZLkZC~s>9sj`#ztq)s$??XgBTzvP~X^C!(zS*X0U;2O!^dgwA zKNe(h{SXs7+M51Z(zH2!ukVFvP|Uho{?!{43?;*y&acv3mKJ#H`pm_d?r-`%d_qI<%QeWwT_YCITKo z)07(tEsBDskqDJ$mS2p-tUGLi{Ie=Nb*e@wrDMqc2x3@aDTf>IJ6xFLR~`IwhG78% zlVZLJiG~uH;D2|-GV9jk2VZp1DJzfIg5eN0nhi{Ha$DTcQyiq)9}82bIX&O2{cp#N z*O%`(k{Q(Rch`G%)8jY`|0vQ4fbT}Q!pkCkwu;gr`1scgw}kPJbuDfG^09DL%%9cs z$mKcn2d^+KyaCWHyuwrpf8R~a-M;0n&u&d+ooM+#MpIDNk z(p^XJk@o&-hbT2B1y@Jrd|Noc5pfmdq1sgQ8r>xFLf;?O^7w>$&42Iz1n$@e{x;8>G{s?O0x+X+mbZx1bVKw;d7q#DBmg1_A2DktF zIh@wF_6q?huP%J=dih_tS#nvbj-rdObBKZjg@rE$+3RI*dBnvJ6g|wbHi93vBrkg8 zzI_YY8LOR^Y{g-qqp@h45(+ob1shd0`){13ag2`N5Z`-H(zE0QUXoZoupNiZCKf6L znHMVaVD}Xa*;iD&1ND{8LG2&quNZp*yVzN=azYi$?p{|V}l+*Lw&#@*noUQ#!C*ul?FUkf~KoV;6?s$%1J!@Ft0-Bv{z* zA|^~b-z4epsE;*ZMKHF z-WkhKcA+Po@gS+upgh_G;gILa=p~#0nwF`y{NCd_QhA4GAQHPERe6htJs2645h1Q3 zlP{B*cs5m{ke8%s1AKaRS@mRz2&|(*Y}*o^Wrp)4S`aSEuh4siNJ`6dlB$BGp2hhr7bpZ?ONIO-Ph`~D}IJ+KSHCkAWF3PcX|I_ zn+f%Z$3=+$D|6Hk9>##^ys|F)?DJ_kq@#r7b3#2+`h*U^X?+Z9iGc@*WRwd=`cr3?CxYn6h((vU%s{xk~a0PB1PBHEmKXh=kqA3 zSZf>)iwudltyJ?8nn!-9{VgFYJMv5@;W6n6lP>vi@|!3n5lY#=(uF8u#R`E?<^XyO z{F|ZpZa@m&kF`<%h?X92&mP;wtR43bdU{%I%hY+C(T9FPCM@MtBOs_b9SN8l9eC*^ zgjOSxifglfm_~xSjfn6n0#j8*9U0U1hF(;G`O_x#`V!pG%lF}*@UeFAsd(skG6iej z-_Z73Dlpy6!e+%OW@T_F0GG4>;0wOR0o(=1If3S)*rIU#?%jJUonhYUW&y zvrPEsg@GpXV|?chGG9k0k{qvA+9+s7QPdpf5?T>#iz1M7p&QB@eI+v47$e~S(-i=# zs`AKPx>^-~#rZBfE~*({Cm0vyZ%2*fLSo~bFkk=o2 z$kKiVQ_HmOB?FF+38~gz&#}%gpcduH#F_rCd$(>?SlgiO+9hSnDjVtP3_$!Ca61H` zp1_udGpeAaQ8uK{it$pCMw{gmCB;=F`9PHrsC_AUwUqKjRb#S*z=cEKP!Jkxi5fkv z#&#jR+JQz!x^5-B=8Wvvu>m#Q=H@Nla{t?E?mFngrSmsFL0)yjua+?7Ugo*ppR3 zD*;#rtqNN*jc0p{qy&79q=bLiyFH2b{)Y?U2fY6UULS;!cKZfw6TY_sHlNbLVq%nc zMvPFtUB6y;WXG<1e}47-kB=TbTAZGmI)HlfUcGwN9-i(CmyMvw0I-2M5L8cnb=Y@p z=OO*)RBgQP<+2t3>Jbq9u(7ANE*aiTIV`>y9Medg>u{XAP5P|KFJX~F>yP4tp;*$XP=D?8WO0;l>0-dY?PnNg_l zA?yd%XV0)cn=8<{ddcl#6U zTi9p#ELD^3#rL-3HF>xzVRV74gh&5+n>NaU>C=@%TV72+x@X_*Q(yn^!@b+Lm!+qt z4+eZ|;bQkbdGe&i^8{Q8sOlp>%ns&6xz$?-yxeSM^B&V?RH*g+nDDsE{R4s@wDa~& zuL%#S5*BkJ7G4}~u4Qpd#WoV>42w%OeW0yiVxHd3+4s*8r4tsW#5J;hR^F-%Oj;1K ziA9!`$)C*ux`pD^e*N{=R=D;Om&PPq-Hs=*&4A*&0P9zPry2;uKF#sFCI`ZVM=SL@ z$Wl5W*s^D$}kSD1jT{QKpQEs_22dhOaZhl{`d89Bw*|IreFW;Fo0 zOA3T#DpX+E-~kpAK~8+g`%{dG>AzxX19` z&$tl2B{>1FKY;Cl?ZEq6V5`$;*Jo4=JX3nyw`GvJ!o1G@biB;AA(L7ysnd4DoQie7 zoE%f(uTjB~H+%V&PHTdNSRD(eG8StR7F7ZkP&pi{Xq=xY$^?)s3hxnU$s!oG0w$QU)3s>smW{3?9>|QzOx~RHS@mH3H|Qe4NUl)okh93-4B+ z$)0Q$-fZrFPn^J@x5v*KF+C(iSwz()facyT^jQ_OSU?XLaB1{dPoNcW6`ts`0BshQ zrpHnRa0P4uQsLcRfl;9BETw_r{S(;!Pxf$efqV$thy6DxXhvQxrG$}=3!97YEyE3H zbAtxTo?*k3gR^ESUoKyn{{6R2qvIJcE>ED~`}6Y*chv&F(<*aU3hu_)ne!kevZ zYM$1`!R;bE*Eu7jYTG%OTA$L<`NvS3^95wERj+4=CY zx=qi{uUzxmv~r1e$3{e@^$!YGdIkh2ov}zez}sjG52qy-Q&TLSM(|SW;at|i-&zZQ zXLYt@w#u+1SfU2BTFBCVvZ;A`FK6#lqXH5qu;%)oAOCX#surC39tDLKr+rU+HYYFU zKP|HV2wIY=@bpw?P4zn0ceHG&{9k+L0UlNLv~iQ{rqHEI2kA8sAbhMS3TY zK!5~k(tDLI9RZQ16v0BXAqt3!y#Rt@K|$#RlKaiv%UxvYTv@ z-Cy4GPFn?Pb{B}E_W+zHc#j_d^e>3ZsAkDFFxS^GXl3iWoC_^&10}e$(zFl&pctlw z0M`gxZihF=mjYbzr1%!R_A%+a4Bnp&-y;H^6Mb_Q+C8j&`><+RvX~YE4|$R+%hO;h zgBG@)+DiBQ96-=6c>JlK=fP2V9wovOH7X!z0j8JeSbkGN7K0Tmmu01*noW+ny~e9CBytpvc?XB8S9o9T<6IYk1_9 z?GZySZW}c8k1YcR|GI6^urnB&`wu>|u}A;2FL&+r`?}|PTv*+{%ReuLwYj#ianqag z>o^K^sTv+6w^H~H~e@nfn~iGRLQC1Y5{ibhnqaz;d{Qbu^uqDDV}X-{|&bpa*P z30@rS;fdcC{zfg~G0SKH5e6S`0Y6(a`1>`1_Zvdg7qFF}RUsP;(cC}7tpT};o{TA3 zGh8TF-;}Zdte`4^swOh<#f!mNWEuQ9d3$%h-2DVCzD9ri;Nz~#>OB>=v2I;s1GUHF znsL{9DR|rTXE70mGY;gyZxyt$HSXCyGLM|M zZk3)?tmMtvpmr9(hc-dH4RI9WTkynptJtkNRq?c7fMvr`IE&|8#5j?#4=Z-fV^E!9IZEC-7YOmdBUE+qk~w8qPR? zYjg9h4z#L;z)j{{8pg?C;E7{^Y3KdE%zSFMbgn znGZsI3W1$gzfczcGTH`QaX^6Oh`-Nq#{+eHL#SJ6DWHe@8v$3UT!t4i*fNVCz9C&*sd~7PbJq_NT3~yshfVaoP$Hu~a z#=`Yzcz-l}d?dV&F#Am&@hZ-UhL{QP z4~Nfp%~g0oL|9l0Tr&lKi?X_|u~f7W9(pKs8d=<5Zr;54=!U+-V#dLL$=SH3!X^=R zl@ehLuhar}D*KaUFMLXB|1boC_g#ns5ZHmX2Vxg_yxS?c zwux+qb?EuR?{5t}8CSxSZaKXD61=?x-bOXNfP7$#Iq*3X$nd!t@cvYYY4EWr2xj=$ z1b83g1&GlU&qKrzfH4rfgW*0Aa32J2Uj?)hvdx5Dz<_-c?$gE@7~3!FBj;j@giIe< zzIq<@KYUV-{<;jWCn{A1Dnpb9fXJaBm|)R`Iy6M~v`g5svQ00brG#gvgR>Uzd==E} z8aU~^0+8K<8xL?ZG#v9?TdC2X#V#%!$j@DyILAN*tvVCh1JJtWk4OsvYuItf6Fk4L zJChS*ICBIo1WYpTqy`~8<&A;I6%ml(MtO&$e&@P%ae-lUorV|c@1*=({;aGxNU2gT0&H9ws9P1d*A#`z(EUS! z?r8ztuL-=L3Gf~v_+n!OmW~~OC*eDo9Dv^udc8YI{caNq_!UyYFAF8SQmEmj;K;2clY1E5~TY6I-(LeMf~RnS^_v^<2LTjB9-F4>f$ zOKIQ}tJ>?NYA?daexuh3CYe8>1|bw1OYr#$x5LC(|Lg_d>)ohPSV)#EHPo)LG_Vld zHN&J%v&yYow<^xB@x*U)DpoXA)CTe*aVX1Ixx*Y=bD&9=i1QodUi^^4$LL(qMRo~R22 zaM=e&*iMA+gPHLI3&VaQRX60W+L&WpKWZdVDhY@Q5+nWk(bqe=SjmkX(PpY3( z=mOYIVg?p|UoPlEd`OA4gOpsm3%GEKtb+HK2xTi3?F3;PNEOQp zmMwzT1X!#nXyfY_-#_ES7l-F5zJEl~DnqS`_TAN_CI_fIaM=#13{!?BFVT}*OR+pi z7wTqHysuomr%usVL2G9n+y^yV?Z%aBlXeXmdV5=k4#rY|A#R~%HTw;2sRh9P0f7An z0(bbCQ}rZhQPsK|RjQ0Ay-UrCaLpZ|nbEFfGknZcwNzVs2HwWR7}nMvgm@id6}G#> zV|*UI-=NI7&vmL2ft$lorzJZyq4t+TPw<8KvoFDaQ zXh~U=ZCBi{gK1KIR8N2^VT!;+g+qnyW?+UIkC+(PUBtm3!=Nm7G3u!=s=`(--%~-W zn>{KORa)By)N~Skc=5_J?~EH~tc4Tj1_0}BfYkdCsAhj8HG6^7tZRoKt(0=jh<3Li zRUJ{PQ?sHb-ZfEA&K$vVFC6Y9W{vzrr@W7-Hhm+V$|l3(#Kc(VoCRm0g;4gPhpMB6 zkj7OAp29apvYCZewe3B9Y@tHNboe7IClB#mIs*Slzp%dvI@hH?>jt!O9(bmy9kDiX zQv=!SqJ{UWfb1nvo$#kp7RHce(CR$g<05&+{w+5~uy?zasy(=BVL6qIEuikSnfL^y zbI&v^)xBPdzcug z^0?3HR4P*mQwdO1H@FG~3)?*jye%oBk`lNKTL!IEw44~Tn>~6z6JY?^mS;cv>eY89 zPKw{!y0x(y_xI5NnPa%|1Ymmx0@dtAsbkV-_;OJNUqlz*$1-y z{Sux0&eCh)1U=_>P!0l5WY5EWB7?GAu3NBR^Hw1t4W)%38yQk@oMPHtaFEP`I?wDL zl`EGqjr_o!lu18H;5hGrqD^kxPDPN~K~)8-gzIGr3E02Md%8e?Jx^s|=g7l8MgNq( zqv17Qit2=q=|4JGC+ri|2|Fk;#=h(t@?&K^F4p3XgBV3{9jw5W6Iq^p75{x?kX1_Og{;j=3hZ>6kt{dTmxd*`d5kU4NynhOS`U|{&0o!2} zHJb>uJk-hE^gI=|R;NxCv_jeP>*_MSX3mSm*hiEYdzJDHQ{Zu9Vyt)0!n?cHt=p=+ z!|@nf2v+OLQ(+@R+Fa0|T~w#hPgAk9i%O5)zyuB@cTOvbu#^Vvy#mw@r0#$!p^EVj z#YJHgT%iBV?92W_|E0MC!73ScRM-UXlX`tesMl?ZdR@WO!$`SK6V^a9#1M#Z1+IM@ z{Xo*bx7qmK|!t4Dl0kg_tBM|^}GgHM38k_0GxyefRGnut&g^yp;p$N6@6KRj9r;JQFd zb531q79S%%R)(xpvUYYs5+Ivuprxs5{mvfy{K$ukml&_MXkn}X$i7CyV?M@RT>#ae z08r;~8#RFTGHpLFA&Y7jRBe*gtQ~ai#+zzpM60Uy53<$1rq{@Rs;)s>YzjP{A!fN( z{?FQ0t=g(`U|=13o+O%u;ASI3`X0!SqY=M(_wZxmij|0)kTt8BYZ zc)COxd^Pc;dZm0Umq&XYND+hrq$WJ!<25kgb2Z^}cr<+6UEuBT_IzIkE`DtZTDA{V z$V$6Fda}C2S2CItQd`Z2UH$X#-A7liiF>_gPh&AalP!b8WEuRxmH`fVLNyBjy$+By z7o}42i`PE60O{#0=zFYLS5@t0p=y7k*UpDzo2@5%Y&=#V_&MU9$e68r=Q?#-;@{qC zoZ@{>j3w`a!>hMNjhis?(MOGO0RfhvJx{-^OZ1Dos({ZOBqc081?M&TK6U_=5VZrT z3e#T|fAuuQj~W|5g{tbcGFZ70VKe;>x$%ILS#wCWP8OSj#d*IKd;c>Q=$G(`D)LYpIDQsYI1yy<3PX0IdZduMHnZRZ<5&_XI|H zhvP|%at=pb8T@g)4c`avk6#DB9>bPFi;qDORm*b_ zP;f+ndLEc1*V(WN724Fyk>e@+d-FI&-A0BqsI5)WMA)NC8@JgPoiCp;hUzmJw1-79 z>=*h){Y@U`WdU3n*Tl7z(sTz-6`pnwy&#VIe~|M2-3?4xq5dF_|6kGZUseZvthCo2 z6hmcHpOsr{jY$MK>x`@HOJ&c zzNDHIk|94YJkVhJ8i|ha3OuWkKUTvIpbSm{%z9-Vj;G2v98Ys#dnH8!CCrcW?eKnh ze}*mtmqCk6xsog9C)hbBq*|PxHP>8kPyz%CZ z_vg~t{($LPePcM0YLTwz!o=Ao10E(Z=VX<`1=G}yTllmd$FqB zNUC-s{5<{P6xZIc{RuHBmc{vql{h6gb*=5yo5jUqFv)cJl` z?%XcWvPb&?f#+KS6vOs{NETfbAhZIc9WZs^WEJ|SqC|fao+%&k`I!GR9rYQcKM9aZ zn4Z+A)K8S8)gdvnfGq$C)mI3rt3|gKR|jwcYYZi@5UfLd{o;lIR3kHF`@MhWJfC&T zo^MV43|aeC@bha?-p{WoTsH>LG(_+)bQm~N-SLr{kJa*MtpRjFdEpvDCBWshXZVtfwo{|F%c1-G2wb^`#@RRFFz z;EB9hvo%(QY#MnEkJ&Wy-XBGH>2S_#8mNK!5A>R9l;bKW@TW zj7O(du5mFMjwmB3DYK5ia*%!*UlO#=kUIR8pvUU*4-bNGt3vaR=p`ot+p#kt942X?2AIJP84c&n2%5VP*keUz#9VtGhghL zDaXn7nR9Kcn<3NWa=yL;D){>L1@QDN=jYcIuAhVJ4shKL;L`@)ZUt|L0gRr7x0^#W zV@OFjF`%S6lnPN%lP*OmAZZCp0%KGB-W2#an#1i{;N#-J$7vUQJG>wNESjIEAwO;e zyMU}|7pN*$wFx{0Ec&qb2-viO?CPJ79>08a?OJ0^ty<<9sZBI*F(`=jFM^1-o#a^x`k<7W_5w$Lx&J%a5Q1henymvmYH@QeVX zE>4Tvv*J9Z0jH`!B|xQL$`Cyvj`=!}N|?SQ>Q<#fRp$z<5={aNMX41ht;SN)3ZXg# zpgIVk+B-1w?_Gkje$yl<^Ui910gC{t(;z05_w^kIF$&%u0oTLedI(%c!1X}5?q3$b z3jpd30O|qo=}rLdOc3sfAcklU@3+MuAh$*+bHK-2ieT8aR72i}0>4gc{F)g2Jt*37 z;PcOU0z!GGuLaaG={KSHEi>(@7y09ZHB zb`$K*{1kmz01*1Jzr*KD$SS@p4tBZ21D@Pf38W%?@7#k|yPWF9(W?!|CWid^+df^m z@H2EG)7I8{u&Vt2UoKs`lx_Efxjp(6D{gcT2rxU0CQ>i`%QWKeE%GOi&@bh4f-b}F zYZ2dQ;wvRiDymZlPKGDfp)x=vL=TD1Du$>E($^H2Zls@UH62x?LY*rb115;ht7!P| zibwxJ4*y$yoIzJQ2V^|kJRsv|PdEejSMqag0a(5a;9L!{9Nu06*9#!#LCl7j0WlS! z3o#yIEW~KI-ScoAL-37)7|Z~M2uBD*^aD8cK@daqLM4nq7EtbnVAezzi|CpHVSX+C zXMh2Zr4#)_=)llr*a}r^LbhyTi^K#hS{NF@Cb9^uJXlYXTqy+D)PihHh?mbEJ%0V@ znl;ASC!R2tWyxY}f_MXf`Yvss{tBn(!TaX{w0{F||K(Aox&#;x;8&XqJb-Ro3VXD- zHK6@RsjKA*gkAL7#FOG^_<6AErfZG@TUv&Mv@`82i`v=;A~AOD+O@oEx(`{l=OgJ5=uJj{Tn(fNW2ShI0O(q0D*Ab2XK7{uHQn?LhK>f z?t<9Cz=hZ%p-U=v4SZ}BDqVrVhE9=osb zb`yBtwHTFcgESgV>dD-aSPRfz5}-XzC*hCP1_l&94u{bRRc-bHyIY5ZbgGES*@RP1 zur4u{o;${{;-Kir(As}LojbQV?D<)UZZxE1B&ohx1hSn^3kdqN zxqr~lPx}Xc3otzf5O^QrJ%GY{<^Lb72PqB+$eN%Puzgzt+}*5j;r-Wi(1o|x!}VGP zysUT^BYe?rkN`&8L4oiz1~I^JGQl_zkTFOdGnlR59OFhcGjLt7CD8WLK(+&01ro4m z5pYF;0PN$cJz%F^<;>W7Rj(da5v2Ny5dd2dBIe)ozkPS={SV^zO_*RTE?(S(>?^{P zMabgnBo|~MF5#X#On&hSDJti>BW#0vU5Faz2C{dO0a$4pTo%^BucT;Yrx^RR)2Ujg zyVGfOb_T{)&zPn2Q$>nAt4oYo4V_6>puK$ga?U21v&Z80(yFw8?^&8%+K+ncN0Ks} zL`Uvf1ipEcC47;J@i}-X^?VB61~7;2JClBq)2P05vId+Z2%f_qdMG~XAq1#TT!fGB zoq+(+zP^r|T^;^co^$wLYVQoV(Ap93YeQ$ichw!v6P5h@K86?h2L#ji6Ty_@|DkFZ zo&tBTqH@EG&u<R`PeC+=X!VefDc!qq*Am$ZLul^la5|1h6BJ|qAC4UH!w8g70LsV& zSl$lz@r@1l^}E&2*Y9Q@U%%@;eI3`j`Z=yV=jXiC*3bESnA3T-sh{&yLx=ObCw-k? zKIZHAtg4^i(F%TkAL$^P2&A4t^x(a~)&t<~mezs_U4Y)J1l^4YU9uV05qwvZ>RrK> zg96`$I`|6ksy4(V30$Fa8MM)&8X}UFtOP77SoC5k>D8Vp6k4k46>DL!L`!~VxF zfD@~6&cSQYge+DVz}we&>oi059%n&YZ74}exJtlYR3LkblI15T@ADR&ikDG^4}!J} zL}U1QpLGV_u97ih$EOPyZYdLE+%cxx#*m)My?pS2bk36}Oj;}$V$-K!PXAAQsr)!_Cj)?Yh^bmDtDRy?F0!~UBv?dRw~#YO0BO>y}GX zAj`7cb#A4gHE_QHiz>DR#K^yYKKK1k@4X*)VA?d}rOK6!1pwC70MM-vZ{mt02!!l6 zxC9lTipT0d0Wc?ZKMO<>wj&^nmTA5Rvuq!TWE!j37+pOtdazQxVuH&ZT-kSJxT?NuQkx2r0@q|% z18MpKql8zpJF&5AfBo#_mCv`oW^C);-B^$>pRpLA`ZB~0fau!*POQ8+0T4AI3-6nd z#l#o9e}y1>!)8KWYQfbGta5&dZsn7l-7Lq7o}wE0FGxMNIR=$PKhy|kdkCpLnX9`GaNo#D`ts%DEOyx{N% zX(}H$cG9u*K?3aoH?iLit_)a>noR^*#kbuhO2X~}t%a(U$+Hz|@=Pe%IW~YD!xjPk zQ;b4;AUSZoS6jeSxLWzH>0P}Fz~U0cG7zKxI(6o|9}m47cW~A$V`Z&c#*3LU87r}T z3n04-;sCt;5danINWTN1V#n97Xcr);;QcG`{x$Susj?tZLjt{>nqak4tyeX0{acxY z$DN)$sOeiO<>Ddu?@?vaPD-Aypj^&avM4*#;?NopAoz^3xH;tE0tcwUrZKgSSFszV zm>yKErJ}_rKgw`?;P?aJi^!aug#Sen+Alj(MtTGtP4NZ)5(2_hX9+ObP0&$6d*D7o zR0mQ^pjv>{3a%*$*wq$*T__bS*#xedTmjcn9>Vo5TR`<(lbX=7^IZR{a8(9%x4-f4 zl})EV{OH;z>s~fCb?Riy2Nk;zz>3Q?*(!Jsf|Fi9K$vZ{0MNgvPWFmYCmSp4WOWS* zsb~jWx7ZBr4z525_4)-hAsrDD?{-tk=*#5G&ZMM!6wO}79e1@MLLc%m8aM)Ox5=FA zbc4(}=iu|!Z1IMZl{!i>J(hd9K#LCo9}GPx%-Uta7YDsqiqX8!8fFiDLMvxi^z^7? zXbQdfmzzgkAD=DcN4{a9Xx+i~Kr&;0av-Y0)CQ<_P~EM7b%(~2^%SnM^vkpd;5PHs zh6fAfs@ekK7M`msp+yY^&tSE{y_90%R(DVH4nJ!|6YN^6Bp~gF`Cc`^^!eEC5@)~&IMGWw7~|n`xN*k z1(Zn!rw*VBM0Fr#nBE0ccTiOntGi^Eoh_hKt;<{R>QRM`J%4t zmem4wp6flp^?n031FiwbWY@1>eD(ELyLR7xXUY^~)l*Ly^K#}iz1TGnn*pS#-VOph zKNePjOnO~}wIr?XcEmqp!G$CX=_OK^fKMcWg9SP z@kM|y3M&4^bteT$l2JFlo zAl1Os9Z+=%EiF_#}d%hxw=beJxFjRzy?B8 zgorr*<+mSxxntL@{j+BqYg@E1=H|&`%m=uxz{y+W#l8daAypMftKc`d{u6?0WYLek zSIBw-)*B*;WoHL4T+b^TM}87RpH5Ozi#I#->ZZ5Ec9Bg~O|YEm>}OFU6xPZQf}gp! zpW}9W|DeC0bOxND*XeFN4U$M%N4DgAni*oMT5O)7Zp5Z$?nn#>*G>1jU#j*le^!DP zUpVw%p%A^`Ld-tngfV*U%-;6|(2gZAn4m?t2#*+7sSztv5(K)Qu1 z0XB#L+wc5W-@X0$*6lYCu*?E~Scs?c~KCqE_0^A-*H4;H=|ZQ@PNGpS??s|kkq_{8_|bKb&^%{tD2 zRCKe2%4iR>_6cL}0pl~A=4sH-uYtgx0;?uFGamD5`jgC%J3MZ=)Q2TaM15VQ3S zy}X3+c)dwgjb%lPU=g6*PQR#K0(N^9AX>#nINh_^gna-^FyC) z+Hzyxj2Xr{1Z<%~#w>v9QnCrQK)eC5pEgLN7t1R43|j?My#Fr&kIT30mYsUKi|@LH zVs!^rRkiXur>})-JuWPPL)6K&hvt-QA-M7&rxyiW(R&^2;~Uq@;eWl2f8hDr4*$=o zZvK$44&_V{e0}dSU#AJL$CoJw^dec6V@#o!Fzl#uRH1nnI5kRC0`x9Ls({pW)K`xCE|}W1$S$-3 zsw}lqCUmh^a5sw=;8=e7P`sWcAwGaOMqccRPFt&K=7zp!2-1Hg>^8R6xuul5J3q+^$IE5t>!4f1OUEn z7pyWBSgOEeXl^B#yWnXBP*$TFMXG{yg$MHn)OJ@pppH^Hu&i2Kd+1tRZPaQ#IV4*h zF=)4}Pz9?xtVL%Bxbs^7E2>oh)(MdxqQRx}zfCx~^Ubf`U$7{4`_Q4riu(19Ir;J# zvz$)zIE;YZBmn!KunE3~I0bQ*RO}zreu}NMI2h&{2Gz*gZIQm2M8zt>`j@f_N-e$m zLD@MBPIT0qqt%)%7` zIvaquz{&-M(qr`mtoLFE{FmTLfXxU|7NYIhPfxBqvU1(;Z%vvU_iEq1#=`2=jTzaq z8?yn9tYERg{&fM^k04GEU`?9<;#UYO6?;ts(VG@vO^RZ@Al62m7phh6&-==a9k5q; ztx~lvB}-sB&5Olk7S1o}?dQ1O*5UlUrmy1!!nGXR0!eF|w^Oz5yxJ~G1;Sl*wX$2x zVg^FmgH~Dx?9rMF0&!=}|9Mss@ECsUY+|hjlxt{ffeB0w z1)PcCsR8Ih4MgW^re94{rd~}@U^+@3`K3aYK$UB0c$6S-9}uGiTk^&Mxza_2D(}ov zml8^~DysrGd4>PQ)ye=XuzG3-{MS^gqF!-qVO@w`pT7F~;r-KR-FR)-aAQ?im@%hR zDPwxZjHYU150(`y9-$eqpF*6(WEZPg0xYXommezM#5_?z24_h z{r)KPH7M$8Ww)3*1L1xo#VkQ9lVSKDL6&!!JDw7{hn_Q48kSikByq;k4?c^!#8BNX zLg*2Amr=aLV3fg-WKb%wT%$uKfX)g~6{{y;{g1-c0g;PF7KVNO_Mvr$<}AGM#;DPE z)^+J(yjZiQF+EQnV;X>R9>h!7(S@6jg+=f#gqi3fz#`5%l)QrYG96#PG}OK?oe-Ow+nYh4s7bN=Wgl23!x*$&M@uj8il!Hc>!7i2EkT`i zHIKy!Fd9LLv55q<=>*-`1i-mAF^3_^pyW`1nP~X6P@t340oVedE;T9!UPtrR0S!py zuCCq|gRbQ^0ZX8=N|jp$k}j3|0ZxhapNs zH2(hJ;dubppLUNJb#p_{p2m^}4U8F&K59%33Ub*4So667TV}Bq7K`o$V2=~Xz9pOB zv;gcMDrDjF7%FHli}**xMT_n$3Dq+eR)AI6m1+lY)4)oxFK;?k2#hjYV143ogz0mB zj!Vt_oTq5U^ZO|TR@*V8qTKUM9cbyKyoehZXx_>IEBinlHj~za!ENhk@db-7Vtira z3!dsMtB`%rkigOb2SL-SlL%6PBdDNwER`;e7XzrRVgf_bVpG6a6`GngF1*4;0#qLD z6Rl?9BfbJsx&Nwz1x%YuA5|(;2~??4Ur zEFQPXBKU$T39$6(4A}$>Svp+==Ll|beBtemv^(@j-?oyRH_>pMzKEBmag z1y~c?0Y34Ae0*>B0M#1i=RErud95j^THQ^?(~TY7EB5nkS3tX1sXiD;uVJn}s3ofp z)cFURsa|OVTD3Ebhsuz@QG|NigUzvI*oA?8@d@B*PvsT8$fF!Yc0v@bNEuH5aX4a> z_oLnHx6G7ZiCtHXRCS6Oe~m(Q0hNiX(Vos5M~KW)q|U*4FMBZn~w zV2E{|IP7Hw`mdz)cEHiF<6f&VBIekmrBLDUa4tiu#Tmn{1HArcY66cuHm5l+P;o2cs_J0s#Z_uI;(s6 zUZ-T(CQ5v-5Q=spiLHUO{i8LWDAgs7$T2s~3uy~ld;l^bhV}vafCvZ7Y~zDBr{!Q7 zMZ(;J<^-w^RP))5I+l8y-AfjmR^4n~?d{2WZMp86;VA*i*?d%^0;0|Nh_8ZFDpTIj zYWmUyQ!b;T3JjM*l?f^as;W{sQKfpSod`k@iU(i78PsJAL4`}MuetMj{6|lZ+ z1J)R>4)AqcZ|CR#TRn&4>xv0O_}?%0|BVCbVQ4-#Bgv_E+o)=3JtfGOQHj?KIyH0U zK{t~{@S4_v*29EY>JOEXJ}nQHK>*NygF*-o1PXPD)iBGw08XYSnQ)wbmcZ7Qlx_!# zj+iKtL164C0V$5_?HHB+)TuKo}?A&TQbm#_D|Kj58d^WWJrAo9|>=epck)U>HFD>T%Y zR;-vYIae-YGC*}24KTq4h-(3~>m3dgPM8eZL$e4D(S)g^!Xo&J5?$Yu!aPlYJS(8d zabCnP0;X2S|8EGYUkb1u6O*pq7j>*}in#%5@@lC9R!(3I_VK;b+s|>WJt)@34(Iok z{QQn20&D82R?F2-Zs59Gw^3_ILWIl0jL^5s*dSTCcvzrIgmG@-u4-@h#1@BEP@hiP3gAJFepv=pO92CfvG~1 zj`8(KS-apV0m>y(RuJVQz67aMrYcNXoysG>6;O35)yh*%E>!;)z)GuN{3(%D1fufA zpMP!k_VgDwZtg$$$F=P{UtQ9?dHkHZb&aXz${FMH)T`l>>#jiR=}!ytYfGPD-yuk$Ip4KlOy0lV~78DpjZ!=m208W zW($cY8%XwQ;JQ-|YZCKBHc(CeDymjmL<#h1tY%FsG2sM^g06)*%9W;hrI)z}TKhn% zT6|z^A<&VxFhQp@DZEP5>KRG{cpoR(UWe-0o-jMga0QMC{y6)c5|RYUS`158oeWJC zoHBzi09uJlq*RDXMXG{SRi;)jl}CP6p-Q0IK~;DBx3U0|0M-8jSe-QA73*7re>(hW zhj(TzUG>_KQJ=2s)b;X`X3gVf)Tv`ks#wt&U$CI@Lbhzi3!qddQRaLu4KZ0xgSIyi ztX`v%rngBczDJNl1$!LgGg6kUVmaaU1I14YL^)WYs(z{j>M>CXz#UiYt?s4ytlMb_ zHroKJL>())woarDtY{yf_`$w@xBEH#uXXYd{H=k*|0DqQP}%>V^PCz3_LI8a=`Bwd zmuld;k+(z(z%Hhe&RJA(G>($tLrBeb#gi|cX6sV@`CTh+Y+7;e&u|&`Y9(wDEaWW~ zq&iMKGM7SdQPQU(0uDjP(4*GfD&`s-5&Ushmofw>E3x9luPvd;;N+vcszN0|br5Am zs)CeklDm{Ct5X%I4AZ2j)D%>z?x6Z0d91lev6lbggO8f;nKWm?`mTMxUeU7c)dkNq zji2$v6UOAqm5p&liyET=tfS!QkKW@HxSo?CgE1dKyM$((uBJo~&K|(dt=$AOREvjv zeN6>>1g=@Zeuj``t3brJ6b#UB1V|aCUn+j;F~w7TPoq@1F+f%WtPyGz|4GcDI@SpU z*5N+B@sWOx*j~<{OYQwLoUY^WKTH67OHRW};gwPMqFUe7Y#Z6B*aBN^;JTd0h*GWm zY%1-XNEJvilpya%d7*aZ0DD?RPPSiJb-0WN8* z)|^bCsZjF~tN_?G0NR(~^Xmy*Tj|*S1~nJ#6AJc20PGR83E=uTgI2`nN_?S!RE6nB z1l0E_QN^C>TcU1ND%Fjmes!sU>P-5%UZ6==!+jlhBK(7{_6o@KXNSN{-_>yj?4ubV zJE*So4LR#s8LXdNHG9C*BvvxFKf8wE^Ab`X;H1Kp=Zat*J$bPc2(TllB55Ec&b!jU zr50wXWkNL&HvUxPA>jDB$Jh7$-j3_zDQsz4c~%B4H8hyeMW9i2f(bcFo)z{a7?2W@ z5|#{12~7!32~QnBbr5w2QY)D1j{KegH5pL#`42!9z17SRc}T66-9LNT;Ma$bTfd?E zfD_A~ZF6bf(+%!Sef)9bh2UUgbg^Q_h({hVqBCVOVgQCXw{$#QPlcF9Kw3b6T28>i zh63!^Vz{z;MbPc1S+MUB3_m0=9wAU3RpOX{D1($ejh%;6Q%$(WQ>dXx3sriPARt|O zl@dT&5<(BsLsbdV)Bs|kD+CZlKw1jDi*!T;6+sDzih_zjkSZu3!ad);|G>>;=1e9t zXLk4Oo-NPsecq`P@S=4$Vpm4*vCaae*~R-jGErLodr*2M{3vs>_u55^5^#qMMDyQA zBa8OZ!Ib7+Px{Iy8=em#;{RD1p zr3K=~ovF_z=>k7_S~5A5^A8WZTwQwF?yn9g#j;QP6ULsD#VI4b#$LA%{QKNKFWA20 zD+PrX!c|$!IsL3?QJ3tN<+aHVUopwwVp6ck8=#~rLQ$;FB{Be}r+B8vy3BaON#2oe z26b<|)&{b?Q=0QA?yzLm?jR~#_(^y?_LiE^R9IYS(3)bTBZQny^Pmee;|Z3@5GyS* zy}S&0>+fFCgowpRpjVUz6s=6;TlEsD4|~z~$#VA38l*9iE$Qzm?C_-|n~Oz1_!|LI z=7)bT-2RIbvFjWdkw~>$ksLgEAW}U65=4Dh2S(hG_)$j%MALVrRDFrEq_MQuc;1xd z;Wxt^TuVN~a^~LGL{ffPWNt4!;KU~m?A&{Bfe$SWk5);BPsR2XeF#~)t1|CFms>!> znR%!7)7Tp#QBS6BB65mx7anfwmj`GTl)r0Od3~u3OmokQ$o&x(kt7F_3*Q=`^q<42 z$0$80B$+8LEQM}fUBiVkpBFb2DU0QyCu>p8I6#~vvE+N^TvBPp8GLB05Bf)j(O>elw5^t=p3i|AOHm9#08|Tw{^&-r^|9G(^)DZq|?Zx1M!*P-K z)Acrrw!~A-UL<{Q`O;!$dGt=sSmB%7Nqc^>8mC^|jycJ}HYnh`GV{z=A!|GWqy?mq}vH zgS6{ik9yS$2r@-=_&!7#e9M2g4oTW5Nhx0yk2!9+T3KVAOJ z9o-DvB8ZT~U0(O$AH4HI#MLm-fL%^f!nA+?g$pHoKLV9>Q37>V?C$NHmAZ*0ZaF5E z-~n$&LGsIqz)j>{HOj_`L0sW}hyl^fv_zzK1*%sY1WE(tsA*v{x|A<8Uqg%jPU8`gl<4wdJ{^C(_$rosU~)K6B0)U$~T0n8;b& z{TSUr$Ph~te|T&;)KKG=c(`#9p5kAs=BlGgwPdfjzA$KY_F0*ETIQx1Z2f+wZK;{C}t3DBb*Cs(vP=U0vSw?!SEPXH%^%V~5U3)1Iwh~q+ z_a6#q!%orL=@hQ!UunvzE@e#?UHryx@`*RvK>y4y0+EsCr^j)9+9YZeE`AxF4ldHE zFPezv$4Hl7Zjx=g`G6>Y+mc3%wL4`h?5Sq?9}>wK*J(SZJ)VM4Gv2TwOaA z?azM0-#o)Q^YPNy-;Gx{p~aJ@7x3d)Tc-w6+%-0goH{Oa4=v$ya_pDsC(~(NBQ|Nt zEB&Mik{|P6B}9YNAFsya@XI6}L^h#62&(qK%02eOf19#-rzDZK_MoS7WqP1uXeL%n zD&wn+?n z6|%z=_<^HrS`7C=vYz!qXEK6@W_M!&o-Rjdw9a!4=4M2=-n3J#oNo-NbIop0(HrU7 zA&58CFOirDNLj%q{H#4+@v8LAi15!=vCn|F~Wos<+$GKL8P`H_0-64vr>4!nDv{fhE$r{kS@*gy5^3oo*G7ZIN%Ji6>ZmEDv7#wws3w4>zmY?)nZ zZR>@s)t{4knd|N?qI2L%;Oh;FnFCcC%}ej5EOV%XE=Vz3Xp2VvF}%rA`;8eZ?}hFk z2aa5Bo4!oP2yv{i_1#XIWFC|(QO@4gw9V46QFL~Rc)01{&t%WKfB|*i^uLZWz3Dl5 z|B_ecsPpEC-@aXx2$p=<$(;Rlj^O8d>rM=L$*Vqb#&c+lfZ`oRG9#meEgXePEnhbw? zWq&sfcfTY*NY&*5U-3C(pMS_Tqi;cNpAAEv^%o{uW_r?lXAT2Z^dpLnQSWksrfL)M zE^Q;%#Ux$+y)BwY%sgt~NeaV0+3sLXg>k95BYG78f@SVuDemE`eS1E zw0w}0J$zKb_-%{z0fclt>v6f#_c8aOSOP#e99azUi!luFJUB@ z*Y|T;It^jJdcV(pi6?H0weysJUtJQ1r*%*3|M5lc^DFX%+GPBg$n*O%Ti{GB_k2#q{o$8qFj&G5C zK9I^PEt1kkGwR}Z{;oBDFP?dpsP-^;uZAW$n4K#jSeA#O2TT1jT21-jnI8 zze*#UqQ>*GOp2XpM?&xL#`{B+6CZ*{+-UnF@5jmutytWi@VQFwS;+ytjq-NO+b;i% zK1AyyTm|3C9Nn(u)E`o!+b}8GKdX@#TOm(=gSoFM<_9oiDo|+FG-#p#G=Tv*#SIFt zozgcqv>q^ROQDJb*NXnPA1)YQI=U-qOcc?1$1O4;r zQJ*gz1^HSh1>ma1EHsxog-eW%SBeaWb2!zozzS|*UP9%vK&{o0_tyN40NYRxoO|+= z3mca()pqD7y7T7UsQ1pVJTdgu5XO^I`;R6+Zp#(QC%=shb-^C)EN$Hi}lGdE32{Xdf3)4q5#*6vt8XfNFVH6oeu1$d-#4t;7@ z7JklB`>r2f-Tl)0S9NEkLQ%q`mk>3SXYiYT9QW>b^@iHVi63)SVIO<+E?@l95u~=# z6#Vtm(Nn+ysP*di?+YLO6|&wN?xbx!V99yZs&1GteE=T%(XeV1Fj_3ZtBi?V?TZXt zy4KJ=RlXEv_c1on-*0vNuTX2q@j%Ltwd_|{tLeclVz;hpxA^_qsGKO(Im;BAC zz9;#-7V&ZW^Pd04(3xvo(La5vKb5FfAx8)ZXN_JJOFLqK=Go$zTNUyDlJ;MPuuZa| zf>J*{$pQlLM7icWKl4llL5V>Y#30$T?Yf{4ooM~6n`L}rAT2TQDN)FoVpFY(5b)$Z z^EA9Q7|>JDjns6~0s>4+t%ZG@6r4v}TuI#D-TTYQ&$!HHRG%zL+&0Nv=ee46^&9Ki z!wt@qilf}j*(34dwY$C*zXXq?v3KY9Q!_9UQ&mn+JU(S%MSLET_yq45kJZhyxrxf% z2rEK-%XpxFOF~UH9IV_~+>+~DCC_hM&H&9r-h|l_{wG0fi`q?1r3BjS|hk=`#Yxc`Fpy%$#P5{U@o_tn~snIlxurZ5NE&EuL0W zJR%Ajaw7Vn&tALLzh%IL6Xs+zK}>lb$<-50YMn*qvCPJ9a8q|?V_z;~OzKzl^bZ1# zrc)JU$H`oC<}eO0#&I6ciY-Y&`_WP(T`c~2+!c-H=E=#CjJk55w-;>Q&?!WI+Y7AS zQvpvAv4iMd&7oh02{CcW%?&41T;jt^%?H|w173OxUz^G4&o~toILUae^4T!BqBRR2 zXxD%8$D=boE#>S?C(X^IN;ZC0J{$Ipg?y?PNMH{mpoYc}Qh*DQi;5|e6C_ZQbBRAV0dJwJJ! z&ix|E=oxQxO=nJnGmp=0W~t3C9!tM;8G&=s5>=9yt6FbKZ`aT=jK`4iXevQ`u6P| z9ho)piZuI(+>@UHmY=Xg;gY^r>&uTx!RO!&qQ>M95=#k3fzk=Jf`{mQ!AL`muN=9z zgeG-@T$>WurY)&x49CRgUMxx-hj6>8{I~wQN%rFaN10v3&*VU+R=SoZof*3@gamL* z>HvXu#f)CUCH1myiV9k2>Vk$c`^6Mg>~Q6&;d|@i8rz5T>1(>dRvlGfAzwDF6!c9g!v%x5&( zKN&Y!DBZF;by86|l@Gycs_F;+zy}MlV+V>+fHw8?0Y{wU3Zc!Oau*NJgLsv6k>=6h z+LW_O<=41&6F2`vI^?h}%);kq-z{XWe&fvKVug#6RNkl!U4|QrJyGrxI0FCxT8ZH=F;!Z<~fO@H&nKMa$@h4S|YVVVR!!Y8_LR1m!ig(g$A zYt#Gi(@h98AP686YW0U15E`Rp?5(mPSJ7Vx|9MvJP!5h`9=t=aMPV4Wofr4pEs_hk5eh*5!8!pr_E|1?yNzMDH}pT zDep!?xcV}(Hw{)6*gEHQrM|reo4MCp1&mz_jV}1qXKnQGcxU2aT3_b4=k}1WQ<-|2 za&4v(Mmr&TOVpA3)JYeG(~eD_=%MY*zAyEJ41ZedhO?149TUGPMYSGkPLdsNAlAsr zj2}R(e5I+Ds0u8blh-&bivb=pCEb|p~hp*9isn+&M^~%S?o{8 z3Jt+-35xq@wf^3ekzRYu!F1weLBk=3#jk?<{G^(x12a92zAw6ApQ93j#;;a;Rs&1O z;JCep^Jea9Z5*$XstBgfSKl14&V?^a>R?HR8v{3S5SAD<*P zagaxWP@Rqhrhv{_gEQ}Tdx3`Dc~i7jx@kXqoc-e7>Yo93Nj)Kw>~YS}C6k1c*^ERh z0cxjVb;#nZY&u$m-}`jT{3f5afhDPSe~QSbJ@)=}&3w7hr4Sw{nu>Zvt%d^X;Nq4q z0?m#_*S+=Nd*|Ns#n3|6|QQJ6ZP&T4h9 zJao-!{jXPfkvfha(+DjhpU(NmZBxKsG$#<>bH%v{JUI_L6_OEV2*pOq$W`T{?YpIL zDmS?qW~z5&SLKoA^FGJ9`a>1wwZHN~N>Q9N;BTUwO6pL{o6#rEeCD9*-{bet2MBLd zW!D0}O*!1mV>N`-)4TNAbDcPZGU%+kd_KcYpTktH&I^xj12>uxZ{ki=mlnRPOL)5> z>%2V%t6fjRe0YjBPeHp$15_qoeXQq$Jc>t8w1cAquWiJZgY86DYQ4VJNaTxb?n+EL z3SEKmKfPv3{m(Zd{<|BT$^+$*sw1n;T!g+igM%t ziF^NU1z8}ho2CBp=3X6nCJ=^m(+qHq*9ajbm&z73n|sTm7%<}_jw(bs)dQ4g`F5<1 ztDjKo6)8V}z`Khr*Mi`+UhfcBG42Tf{aTP~dp1#BObXran;pF^bYq zUR;Tss)QTXfvBlugWPTP(w8-r*AaeWPrvTHcmqR&sqs5YRVCfEt_U=oI(6QoykQdjP z+#u#6JlY@HszCbk@MT>mf*9t$RtplxqD$#x+?!KjJ=S*p95by6^T@?!v?lM7 z>sc=1=jVb{If&h8&o+E8G#U+3Y%pY|8I+<^===u#)xC6%>A!5VaybE01DX`1h7v$+ zBE?S=#1!SU2=h~Q%q|O|9kkc5$5a`>-GE6cLWBE8$+dU7sRGyo@azA(NqYq#o`T@usGxxI$ zO5?74baF@}e;rlzlx`T?*a}s!c*p&4eSWl;40EDbR*Aw_=TjGvaqKRlZjPC{}&&D?<@MP6^2`PLX`_uhOht@X9Q*kzo2Z!%J0emZ51zzx zI+xpQKq#{-viTI4YjOWG>my4*xJOf`e3A}l%t(S8ToqdhvZzKFJweEeH?EaJgrByU zX7;#Q$9Z%X9P_C2&B?7@;p!iEzre^YRW84M6{rW7LrGaK5w;aVXpa6Jj9w@}SLy$6 zDf5UY$7FQPz9=>(EEGSIen~_x;M-2VrSgx?HF?#XSwmG*wFr1uRtM$iZC)R!W1-p@ zXfYrUz`89~)Z<%6glefa@;0yKudttq7#l-D-?eMmY27ssEJGmLB;{#*WMT-VnkW3&G<@b zuU^+G0RU072WU^X;4>9PqjHrU(C|-4LbX0G4&Iw#Sh4JG>=;Le(b_$WF4?Y{c~tp| z(pmR-{QSzcB^o{kB8enXtj_6tsRBxwhTy7@p010)eK^O8LQpPIP-E%VA!TED(m!>gJZAY`XX)m#8z#5mW;hWvYVX4_dM8Og<4UtvOe)W&-UQCNe> zITj_~M3TS$vP__@Pf^ENau+f9Z3OcGBW*R69AuPS%Tx<<^ZwFZ|65bZC-@R7*kxX6 zs)pUkc+3t?y-iyS0fE?fviCQ8k~gK(0q@$M7fTlaz)~&tHBXs;>M+y}WCV!@$ucPV^YyEdq=!s2Cb*&u2&4-nKFPc*V`4FJPXG4fbvE?6g( zC~h%f$Mm6+>9gZt-i21kB(Hw1%PlO^e}yt0v;c^J4pWl#_51&pEa8dN?Y*cf6deh; z6(pi*K8*=xtC-hRBygY%ubs=~YxB2Fq05L#o`zFyD5Xx#Ir}IS2;6Q~E4Im^N-b9O zT&Ft89=l`@DRi|(7EaU*MiIK`3D59 z&v^aJxz2yyp1o|k4TL9ysLH?8a*pusKx4%s>E43~csV0dxAf0;ADf3nU(E;R+JHH; z0u}Jnlq4=Me(}F~U+<57&x4^eWcy&4_?_T3Ae_T>j*L;kX~T3#LSB0t;JX1F!fl#dee%BX*Hu~*JL5&|>evI5j<N$X-x@xNjRyoxX5ol`hzrs7z zsT79Mdyo$~!RW&?Ld%}zlshZcMA@x`?7BavmDDz-~g=}nlGKaJ^IzXbm z+rg=BtiD`C`m#SSreot=?oZoE3+SuJuT6x75!D6TI|}R*)7w}zX@o$ithmiq<-qJ! z4A6tn$0&5oo|i2Q0DRD|j})1y>!(2t9=kp+mh=y#Sjy;`_7j!VswYLvc^sZkbx_KI zG-!QRSC8F-N52~@uH@^Q>4b(^ZU1}bK;~Bx%wHV;B|!4Uhy6p;$|>MrJLX5P(Zpsx z63^oHsozN3yB|`fkoN&&8DvTGEvmuxM=-qvNsp&0kx_GIbPmkMFkr?nk2n#7W);Mw zj7+qhqBoy@_ZBUJC3cD=FRMz~oeJy!l-@sGEUjVzZs)kE@$8?sZ(g5J>OBjHYg#o( zk@kiKX1Hn#OeN`k{x``x$TXR{$`pa)?CqjA@~%LA35bk;ffvTO$$zLWuDn!W=&9L4 z>*@=px!b8-ZIF8L82SoYe{&4aMB`I)zgtErBn)67?g~>{(UCU&8q#$s9?&(pv;*zE z#4~bfV=)8wEX?x;My=y0##Im!0##wj54_A=;FYyVy_N=vUsW1`Mabi+aSQsu6gPPt zMHQlcw(D|h!!(jqIuX%zqQX<#YHi78y4UHCFAhX36?#O`LUGXp_rWUCD$s@D8mNtOf5`WtPo^!o2oe>xv_;KMRJ`sM!C={{ zOUQcql)l0EB9}H5a>JWRq@oR_{zZqy=7S#C0Y5m)5;)qdJK`#YVt{ zNIC8^)U@Z!u1|Mi4AY$4&q|}0Aeh-NAMpR(s#0_Bv*wLFj^!Ou z%I;Gev$zClMo*mDvGiWGQoiKGrft=xPq)y)h9!F*dv?NfZKs;I*^0bU`54 zKm>w?kBjX9F={m1KIkN3tvq5~BZ6WtN#(GAKwwO4?BL+gvu6$YH*axpa<@^ z{?~@^BrOIb5a3liOEb5G$(0XZ;-}Sw5ZIx#-#sBhdhPo$m?_L^z_T{1umWhexF|*? z+%BoShIRDSWbor}58jV@IeUePT8F)G2Vabx!Hu5rBA))Qz=P-SkM{HSRmOen*r?kN zjPBP6(p(UeME`Anr?Q)Jzi(kW7*^hOtzLin+k8TYwIZ=(@yM?-zbUTnLYwjFzm?*L z$j>;%qFUb?bySCtdP_`^^zExbAT=;i%zu!tThnV&W+TUIl8|3dcl24dSW;>BhzFf6 z7vVCb`A4pNfbQ_>mUk>eHfAiH3_Y3di3Xa!Q!DOsVNy;u=__xfV77p1=+UR7qwe3~_!=HwOJ*zrSFP4#t{EsT{hrk`a}{02FPjK(9JJ z(k0f2>ZsNWQtJW}UaUDCP=8nu*fEEuS^hi8tpP6{$gT40-up5X zN%))2Pwf?fj1oCJoB$xf&?CH4Lw?nv*5|}Fa}$=dz119c$vG8JX-s#X=W!i&E65=? zoH>8ot%}EU(w3X83##~-S$cWFk=MU(B+UG z>jFwfGhTyio4%%T5?I~d6njo#te#KR0DiI-NBfSK5-;09r48L!-{WIQ4M@9XPD(pk znJi@AV+ZxCoCIbxPUVP!2Huf<+TJrEleeQ!5!N)WcKP2I)->Y=bTrMErW$B99VfwI ztl@knwI%RP{5h#R5L?6t||! zuM`d@(3@ZSq%r5$AYuk zu_m9CT{FzZ3sf3AE&Ep4_5Ihhhj9xzrNfUrnCA;|@)+al470?y zjXVZez`&=B?TG03?LsCeCE>kR*|0`YFo6$ziV2SJ!s@7;=jJy zYf9S6EfgQ$3T;07nw{*KW%{!l6~6Hr9WEo zVBU4LhOKx4GP*>sPgeZ%Cg+MlY8Y6?aw{ZJii@tcp*wMkt*x)*{EbsisWVC@u#Gz; zzZO}|O;?e!-IK^1anC+d9g&i=mIh&Ju?YLqp}h&FP%*$pm&(B_=N)2f&dX@ptNx%a z%qAVy$73t_{oF1T^z&NC6YCNqv}A?p{DF{*UzCM2hvMGApv-B5E41Pxj-3O?*2wE!Fl>?WQMeBuQ~j zKrjD6cl=24MA{LIQO%Ca)E{(*_`RzN)spLxKBTL;!Yt z#PiM=R+g2d$G8FH-hJT!>@Rzqd@kcm(4JCbl5@@3nQ0qJ>YpY6#=5^WgGWa-|c1AG&*$%+dVWPw?cbi+;m6FdIK_O=<0Y&Nvq~iG5Y9sb~x=o zw@NJpevjICOJNL+wUmIf|9*hcH{dGYPN9CJY;U#fi1 zT;Z-w4+gQ|AQr{SrmwZvSZIKMPz1o>kEec(d{U-UK1WeVsF0P4z$~BnWl>Tt4HS>5 zjT8=$x=pHtu!e7`h%*Z*e)jCeiCs=}L~P#EoxUJ8{hoRvHI2g+0x9rtT2j%|(*nqO z+}XWi#UWR>f5cdrp|7x}Hp)@ZSn^$e2*$t4#9eqr+*_gA35;2X%1InaiA|?=??@$VIBL5F6*p-JV0Zip9{6P};Fk zG5z(@a(!Ut+o5ST&TpcHj3WT&wl z4bop>6DBA$coH>ozNWEVc?1dFFt(wX-Y4gHTu|opFKBHEpW~`Et!?u-3a{G%$OZW@ zY@o9kiPHLV|5#Lwt-T{hx2>0Cdiv0nhbfIchLClumR7>2!rGQ2NJ;Bq11}Nax^U6H z-yQ>D5za{950Lh`#_u(BK8uv!pv}%{v7kil%EH`)=4`#weCCR$ z9Z0jF@5B<6Dp&)p|Il#v{3@`v_lkaRYN5&@2gcx)bR3(=Jf^a{WnYuSVy?hZ98D8= z*S>c`I3k7;S=l|&rOnPm=qem~{RDb-JR-H3MNbfUTZ{e)x8f_APNMqC>L%j2?lb;= z{H{9;Ic!6}Q?%Lm3EQ_{_Pm2$VYDhMS^kXu=AlfqvdZuinhV#c%^npv4>>+ta(k$T zAbfuWUTkXu7h1E4sxc< zsRt!(eQ{0%56#}`h?ly9A_&n-FCn>sCe>*x8R)n>T3}7&qR4|zF4$-U5CXkVW&Nq) zAbmsk{&S;PHJ~9rhQmM* z$URTL@Zi<}o8qKGGh7g6DT<6fDJKe$HvaW@<50XBz8nX<5JkF&9C*Dry10gywA)mf zIwIIalMfy+hZwWfPmRH-)#%=(1(Zbqql{qY{Vwvf0(>z>Ts!@NI&i%}Lk5^MC135MpFWoZXaN%#8YSIq~aUb7PY{*f*ZO=EH|Pt;PJqHjGfDYlSTQUT91H z2MX6Tz7Y*w%aZuUf(@vOiizX8DF4R=Qn3S-dH`{L7R6&ZMJ(1^E|9eyDAHyc;i$1e zX&GebjxU7~si0kcx4ddlVS+Sx_mcZRl|(5vc$P%eJ_kpRBC=ptV_lBz9uQr3L_dr@> zI(8a9wS=3?k~sOY)*h_e_~D+Qk`fmg=Ww|T0B>BDz?StF7v12om?4o#AG^5Vk#7;X zQ4~`V^l$doEF9NVi*5kLcwjJv8@$@T^f`^?SWxWmAJ9yw!*(X%VcM3&0&ccNM;wJH zJ+P07rxD!T+G1O}{5D>*_q6m81bwf&)2aC<1tEt1EhCu74o2uv*Ero3qlk4!r0Kq5 z-cXvWiI)hxi6=B~jXU3%wCYZ<-ztz42AsA07ZcBOrmyKnJ`whJ8xvYYI<`Wqw>lvP zJKIf>f2U6IBbi3EpC(mTubq_-?V;&^u;PUf$DXu$gLVE(G$Hr6SKE`F2kHKLD$HyB zQ&0VJ*W+C7gDK1N=N}ar=$_Pu-Nch1mu!jq8gkYF9eK*LjXUO)mJxQv`I6h6doGY~m~)?A%?$@1 zo{XrhTC9=?+NIQ50`kptyBVmczP8eKOn^?%1b5IRJAN_<)+xBZ)_;TeONdE?tOKEV zarDA56mw%9ud%oBsxhKN$5^)xKgg|pLmfS8daSEOX8AT-lcWieoN9C zjK^(d4i?j}_q1tS3AUW!*WVz!F|2q!`7PX^pMTDY!HxEy++Ud(2y|T9V)C5gQKVfq z-)=n$S}Vi+SrUW1TCLc2Oo8G4iS1XAzyr&b-h=C|45!KoB$zm;fMn+;mA&iUj=1O7 z?#p`F9Xv8I;6D82l%WW`pEW!!ob&5&^xkZ@^e9@4ooS{HSls@_U}ZK7JN?;qhQ!zb zpAkR=etq&O^!KfF)9duLovU{*mz(h_p65Gs+8trS{W33O%|?q&uy!%+NI6C?v?oNDfNSloerQnms*1P7!IJH(v^ zdGw3nThct2Kohi*zo8I3aJWvIVEsp8Uh_D=#JK=d?6iqA8%e>V`3S(AJZ?xb^FN=4 zmpNYWxe8;>{YV;`kv)A?3DnD-y??E%(Y%9{7sQy;3~s=5%6-p8bYJ-utmp|g^R51P zo?VTw$~^ItUxHfFeel&|lP2i(OGb?NjBFE^kxXZOJ8I~9rQ9b_Z5dqPg52iO5wr1uNEL_Zy!A~l;R(ww%5-3>=Fz406qm4DnNf1;fH_#tcBh;DD5;L}@7mwo z_oTer^nvdql2PC>QKSNp;4str3rFVY>KB&HRJ@gMtlhLazLyb!GJK;Pq7FMrEdcnz zI=j6p;#rBLx3gbZaUt{%M(sXVv+NbP=tlKjXF!LD@$e}rR}GM0*DK>t7Pfcg>WM6$ zCcb(S5rqtW=tiWL&@MGqc8rA~z2`MFCTCgW)^pGgeEdVEM<4#=xCmm!MUnHbc9^A& zC)S5f4j0FL9okPiM+x7d3>VW|hjz!U7fDrz{o>Fz`kL74%S+cp&I*E?qDWDg>CjHCwy$ZiG=(^h z{5VMZyzrW-X4z{2Pf@}rC03>l(TvsKIFD!R)}U6-IVG&28>j?@uAzbDx5kGG8(9Cd ztfOk2yhP#NxY{AQ9fIykG}GR0>NE46GxjB$(`o!nZ(N?}R&M^?pA1*->9v1Y*vU_! z8Z30QKj`8Te=T38)P^bKKr}-#xlT+WQ{{9RkSIMLYe`P_yjHEh?SO|Xc<1E**QQIJ zy05t_o--y|a0e$TfXeS{w-sIsuVj^|qUnnO8eUg^urS8%m<$ZMgPa6G@u$MPPFsoO z-9+SBoHSq^dK;r{{kE;hkM4ayl7_R8Yt=aobtTK5xC37Uu@~P>-Awh;zl!XSqSeF! zV&I1M#5N%X{e26tR{U5^GLJq0X})E2Qa~6RM3Y`W`Zsgw7$dAa^tR zQHsx)0FiWh`|BJV8!RVE5`e)e$CS~+C!hCZVu0+F*DE_p`dBwGEz&~2Y+3B1)IEh0 zA6AY8+?xoB=7jInKc`!RynXSvHDLwFwkNoG~e^sVTzyPb>{LhH*os zov%@fCUEmA^P)XHMUvcPZK8_Fn|x;%-d+RQ?hG_2M8$Rs#p&8hjc{cPU+ymap{WM^ zq;xyh*R+?s*9{I=Vj*YNPVh7T5nv2d$XKmycSFleBZZa&j_**A0H9tqndr+0O?$9lLCK))U875%-SB2nm^)no5<; zxx|brk??7}VKW$KYOB!NdEXT4^;;n<~~CX3`Jp1W@qfr~je=edXSe~fsJE&J6?5r@B|CnRb{|AdZ$pHWW literal 0 HcmV?d00001 diff --git a/web/src/admin/admin-overview/charts/SyncStatusChart.ts b/web/src/admin/admin-overview/charts/SyncStatusChart.ts index aed6315fab..6c8887fb2b 100644 --- a/web/src/admin/admin-overview/charts/SyncStatusChart.ts +++ b/web/src/admin/admin-overview/charts/SyncStatusChart.ts @@ -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"; diff --git a/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.choices.ts b/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.choices.ts index d0e2c4d0c7..b0f1dfa83a 100644 --- a/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.choices.ts +++ b/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.choices.ts @@ -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``, - 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``, - 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``, - 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``, - 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``, - 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``, - ProviderModelEnum.RacRacprovider, - (provider: OneOfProvider) => ({ + modelName: ProviderModelEnum.RacRacprovider, + converter: (provider: OneOfProvider) => ({ providerModel: ProviderModelEnum.RacRacprovider, ...(provider as RACProviderRequest), }), - () => html`` - ], - [ - "samlprovider", - msg("SAML (Security Assertion Markup Language)"), - msg("Configure SAML provider manually"), - () => + note: () => html``, + 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``, - 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``, - 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``, - 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( - _providerModelsTable.map(([modelName, _0, _1, renderer]) => [modelName, renderer]), + providerModelsList.map((tc) => [tc.formName, tc.renderer]), ); export default providerModelsList; diff --git a/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.ts b/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.ts index ff8196240b..9c89baf94f 100644 --- a/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.ts +++ b/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.ts @@ -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`
- - - ${type.description}${type.note ? type.note() : nothing} -
`; - } - render() { + const selectedTypes = providerModelsList.filter( + (t) => t.formName === this.wizard.providerModel, + ); return providerModelsList.length > 0 ? html`
- ${map(providerModelsList, this.renderProvider)} + 0 ? selectedTypes[0] : undefined} + @select=${(ev: CustomEvent) => { + this.dispatchWizardUpdate({ + update: { + ...this.wizard, + providerModel: ev.detail.formName, + errors: {}, + }, + status: this.valid ? "valid" : "invalid", + }); + }} + >
` : html``; } diff --git a/web/src/admin/applications/wizard/commit/ak-application-wizard-commit-application.ts b/web/src/admin/applications/wizard/commit/ak-application-wizard-commit-application.ts index 3122830171..c3c7f9d336 100644 --- a/web/src/admin/applications/wizard/commit/ak-application-wizard-commit-application.ts +++ b/web/src/admin/applications/wizard/commit/ak-application-wizard-commit-application.ts @@ -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) { + willUpdate(_changedProperties: PropertyValues) { if (this.commitState === idleState) { this.response = undefined; this.commitState = runningState; diff --git a/web/src/admin/applications/wizard/methods/ak-application-wizard-authentication-method.ts b/web/src/admin/applications/wizard/methods/ak-application-wizard-authentication-method.ts index 9c940c9424..28139bc465 100644 --- a/web/src/admin/applications/wizard/methods/ak-application-wizard-authentication-method.ts +++ b/web/src/admin/applications/wizard/methods/ak-application-wizard-authentication-method.ts @@ -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() { diff --git a/web/src/admin/common/ak-license-notice.ts b/web/src/admin/common/ak-license-notice.ts index db8eeca1fa..4cc978ac4f 100644 --- a/web/src/admin/common/ak-license-notice.ts +++ b/web/src/admin/common/ak-license-notice.ts @@ -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` - - ${this.notice} - ${msg("Learn more")} + + ${this.notice} `; } diff --git a/web/src/admin/outposts/ServiceConnectionWizard.ts b/web/src/admin/outposts/ServiceConnectionWizard.ts index 5f221fee31..4feed47476 100644 --- a/web/src/admin/outposts/ServiceConnectionWizard.ts +++ b/web/src/admin/outposts/ServiceConnectionWizard.ts @@ -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 = async () => { - this.host.isValid = false; - this.shadowRoot - ?.querySelectorAll("input[type=radio]") - .forEach((radio) => { - if (radio.checked) { - radio.dispatchEvent(new CustomEvent("change")); - } - }); - }; - - render(): TemplateResult { - return html`
- ${this.connectionTypes.map((type) => { - return html`
- { - this.host.steps = [ - "initial", - `type-${type.component}-${type.modelName}`, - ]; - this.host.isValid = true; - }} - /> - - ${type.description} -
`; - })} -
`; - } -} - @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.")} > - ) => { + if (!this.wizard) return; + this.wizard.steps = [ + "initial", + `type-${ev.detail.component}-${ev.detail.modelName}`, + ]; + this.wizard.isValid = true; + }} > - + ${this.connectionTypes.map((type) => { return html` msg("Select type"); - - activeCallback: () => Promise = async () => { - this.host.isValid = false; - this.shadowRoot - ?.querySelectorAll("input[type=radio]") - .forEach((radio) => { - if (radio.checked) { - radio.dispatchEvent(new CustomEvent("change")); - } - }); - }; - - render(): TemplateResult { - return html`
- ${this.policyTypes.map((type) => { - return html`
- { - 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; - }} - /> - - ${type.description} -
`; - })} -
`; - } -} - @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.")} > - - + ) => { + 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; + }} + > + ${this.policyTypes.map((type) => { return html` msg("Select type"); - - activeCallback: () => Promise = async () => { - this.host.isValid = false; - this.shadowRoot - ?.querySelectorAll("input[type=radio]") - .forEach((radio) => { - if (radio.checked) { - radio.dispatchEvent(new CustomEvent("change")); - } - }); - }; - - render(): TemplateResult { - return html`
- ${this.mappingTypes.map((type) => { - const requiresEnteprise = type.requiresEnterprise && !this.hasEnterpriseLicense; - return html`
- { - this.host.steps = [ - "initial", - `type-${type.component}-${type.modelName}`, - ]; - this.host.isValid = true; - }} - ?disabled=${type.requiresEnterprise ? this.hasEnterpriseLicense : false} - /> - - ${type.description} - ${requiresEnteprise - ? html`` - : nothing} -
`; - })} -
`; - } -} - @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 { 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.")} > - ) => { + if (!this.wizard) return; + this.wizard.steps = [ + "initial", + `type-${ev.detail.component}-${ev.detail.modelName}`, + ]; + this.wizard.isValid = true; + }} > - + ${this.mappingTypes.map((type) => { return html` msg("Select type"); - - activeCallback: () => Promise = async () => { - this.host.isValid = false; - this.shadowRoot - ?.querySelectorAll("input[type=radio]") - .forEach((radio) => { - if (radio.checked) { - radio.dispatchEvent(new CustomEvent("change")); - } - }); - }; - - renderHint(): TemplateResult { - return html`
-
${msg("Try the new application wizard")}
-
- ${msg( - "The new application wizard greatly simplifies the steps required to create applications and providers.", - )} -
- -
-
`; - } - - render(): TemplateResult { - return html`
- ${this.providerTypes.map((type) => { - const requiresEnterprise = type.requiresEnterprise && !this.hasEnterpriseLicense; - return html`
- { - this.host.steps = ["initial", `type-${type.component}`]; - this.host.isValid = true; - }} - ?disabled=${requiresEnterprise} - /> - - ${type.description} - ${requiresEnterprise - ? html`` - : nothing} - -
`; - })} -
`; - } -} - @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 { this.providerTypes = await new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList(); } @@ -129,8 +57,17 @@ export class ProviderWizard extends AKElement { return this.finalHandler(); }} > - - + ) => { + if (!this.wizard) return; + this.wizard.steps = ["initial", `type-${ev.detail.component}`]; + this.wizard.isValid = true; + }} + > + ${this.providerTypes.map((type) => { return html` msg("Select type"); - - activeCallback: () => Promise = async () => { - this.host.isValid = false; - this.shadowRoot - ?.querySelectorAll("input[type=radio]") - .forEach((radio) => { - if (radio.checked) { - radio.dispatchEvent(new CustomEvent("change")); - } - }); - }; - - render(): TemplateResult { - return html`
- ${this.sourceTypes.map((type) => { - return html`
- { - this.host.steps = [ - "initial", - `type-${type.component}-${type.modelName}`, - ]; - this.host.isValid = true; - }} - /> - - ${type.description} -
`; - })} -
`; - } -} - @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.")} > - - + ) => { + if (!this.wizard) return; + this.wizard.steps = [ + "initial", + `type-${ev.detail.component}-${ev.detail.modelName}`, + ]; + this.wizard.isValid = true; + }} + > + ${this.sourceTypes.map((type) => { return html` msg("Select type"); - - static get styles(): CSSResult[] { - return [PFBase, PFForm, PFButton, PFRadio]; - } - - activeCallback: () => Promise = async () => { - this.host.isValid = false; - this.shadowRoot - ?.querySelectorAll("input[type=radio]") - .forEach((radio) => { - if (radio.checked) { - radio.dispatchEvent(new CustomEvent("change")); - } - }); - }; - - render(): TemplateResult { - return html`
- ${this.stageTypes.map((type) => { - const requiresEnterprise = type.requiresEnterprise && !this.hasEnterpriseLicense; - return html`
- { - 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} - /> - - ${type.description}${ - requiresEnterprise ? html`` : nothing - } - -
`; - })} -
`; - } -} - @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.")} > - - + ) => { + 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; + }} + > + ${this.stageTypes.map((type) => { return html`
- ${this.renderNavigation()} + ${this.renderNavigation()} ${this.renderMainSection()}
${this.renderFooter()} diff --git a/web/src/elements/Alert.ts b/web/src/elements/Alert.ts index e75d2c3936..139c504203 100644 --- a/web/src/elements/Alert.ts +++ b/web/src/elements/Alert.ts @@ -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`
+ return html`
diff --git a/web/src/elements/Interface/BrandContextController.ts b/web/src/elements/Interface/BrandContextController.ts index ce47875443..ac3106ed58 100644 --- a/web/src/elements/Interface/BrandContextController.ts +++ b/web/src/elements/Interface/BrandContextController.ts @@ -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"; diff --git a/web/src/elements/Interface/ConfigContextController.ts b/web/src/elements/Interface/ConfigContextController.ts index 35e38d54ea..c626a7a9c9 100644 --- a/web/src/elements/Interface/ConfigContextController.ts +++ b/web/src/elements/Interface/ConfigContextController.ts @@ -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"; diff --git a/web/src/elements/Interface/EnterpriseContextController.ts b/web/src/elements/Interface/EnterpriseContextController.ts index faa3b4823b..f0cc1ac2a8 100644 --- a/web/src/elements/Interface/EnterpriseContextController.ts +++ b/web/src/elements/Interface/EnterpriseContextController.ts @@ -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"; diff --git a/web/src/elements/SyncStatusCard.ts b/web/src/elements/SyncStatusCard.ts index db8a4f17d7..9009322027 100644 --- a/web/src/elements/SyncStatusCard.ts +++ b/web/src/elements/SyncStatusCard.ts @@ -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"; diff --git a/web/src/elements/ak-locale-context/configureLocale.ts b/web/src/elements/ak-locale-context/configureLocale.ts index 9d007111d3..ddf47c85b1 100644 --- a/web/src/elements/ak-locale-context/configureLocale.ts +++ b/web/src/elements/ak-locale-context/configureLocale.ts @@ -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["getLocale"]; diff --git a/web/src/elements/wizard/TypeCreateWizardPage.ts b/web/src/elements/wizard/TypeCreateWizardPage.ts new file mode 100644 index 0000000000..567edd579d --- /dev/null +++ b/web/src/elements/wizard/TypeCreateWizardPage.ts @@ -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 = 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`
+ ${this.types.map((type, idx) => { + const requiresEnterprise = type.requiresEnterprise && !this.hasEnterpriseLicense; + return html`
{ + if (requiresEnterprise) { + return; + } + this.selectDispatch(type); + this.selectedType = type; + }} + > + ${type.iconUrl + ? html`
+
+ ${msg(str`${type.name} +
+
` + : nothing} +
${type.name}
+
${type.description}
+ ${requiresEnterprise + ? html` ` + : nothing} +
`; + })} +
`; + } + + renderList(): TemplateResult { + return html`
+ ${this.types.map((type) => { + const requiresEnterprise = type.requiresEnterprise && !this.hasEnterpriseLicense; + return html`
+ { + this.selectDispatch(type); + }} + ?disabled=${requiresEnterprise} + /> + + ${type.description} + ${requiresEnterprise + ? html`` + : nothing} + +
`; + })} +
`; + } + + render(): TemplateResult { + switch (this.layout) { + case TypeCreateWizardPageLayouts.grid: + return this.renderGrid(); + case TypeCreateWizardPageLayouts.list: + return this.renderList(); + } + } +} diff --git a/web/src/user/user-settings/tokens/UserTokenForm.ts b/web/src/user/user-settings/tokens/UserTokenForm.ts index eb94821616..d0fce872f2 100644 --- a/web/src/user/user-settings/tokens/UserTokenForm.ts +++ b/web/src/user/user-settings/tokens/UserTokenForm.ts @@ -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"; diff --git a/web/tsconfig.json b/web/tsconfig.json index 887178d6d3..c7fd0f6358 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -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/*"],