mirror of
https://github.com/goauthentik/authentik
synced 2026-05-13 02:16:30 +02:00
Compare commits
54 Commits
endpoints/
...
docs-first
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a440cd3669 | ||
|
|
d6b07d5348 | ||
|
|
6f87b9c165 | ||
|
|
68f70a0953 | ||
|
|
ad6ce84e06 | ||
|
|
239f4a84a1 | ||
|
|
83b6112f8d | ||
|
|
a75c2fa77e | ||
|
|
d76b5d804d | ||
|
|
248756363a | ||
|
|
ff87929dcf | ||
|
|
742472c60c | ||
|
|
3b0fa0b076 | ||
|
|
6d7afa44fe | ||
|
|
f1089bded8 | ||
|
|
6de1affa22 | ||
|
|
bd72cb2815 | ||
|
|
c42dad58b0 | ||
|
|
1e7e4d11d7 | ||
|
|
45cdf97f0e | ||
|
|
86e37201c1 | ||
|
|
ed8c373427 | ||
|
|
2e4abd410c | ||
|
|
dbaff40c60 | ||
|
|
f956ca1300 | ||
|
|
d990f53596 | ||
|
|
dfbbbe03c2 | ||
|
|
4da35e143e | ||
|
|
bc06779f4f | ||
|
|
727ba71280 | ||
|
|
7480cb3e86 | ||
|
|
7027b7c4bf | ||
|
|
b47016cd30 | ||
|
|
d18ab068c4 | ||
|
|
3b552530f1 | ||
|
|
cd46166c8e | ||
|
|
8e37908ecb | ||
|
|
9ec9f116b0 | ||
|
|
942a0029a0 | ||
|
|
597a67075c | ||
|
|
b31517cd79 | ||
|
|
a473d0618a | ||
|
|
7858ab874d | ||
|
|
0fd8069593 | ||
|
|
9d210d92f3 | ||
|
|
9ce75e06be | ||
|
|
ee11194957 | ||
|
|
297d4ca38b | ||
|
|
f120e179f4 | ||
|
|
c9a2405247 | ||
|
|
c58d7ff13e | ||
|
|
e8996c9b8d | ||
|
|
ec2b9ba0e0 | ||
|
|
8bb702b95a |
9
Makefile
9
Makefile
@@ -5,7 +5,6 @@ SHELL := /usr/bin/env bash
|
||||
PWD = $(shell pwd)
|
||||
UID = $(shell id -u)
|
||||
GID = $(shell id -g)
|
||||
NPM_VERSION = $(shell python -m scripts.generate_semver)
|
||||
PY_SOURCES = authentik packages tests scripts lifecycle .github
|
||||
DOCKER_IMAGE ?= "authentik:test"
|
||||
|
||||
@@ -50,6 +49,14 @@ ifeq ($(UNAME_S),Darwin)
|
||||
endif
|
||||
endif
|
||||
|
||||
NPM_VERSION :=
|
||||
UV_EXISTS := $(shell command -v uv 2> /dev/null)
|
||||
ifdef UV_EXISTS
|
||||
NPM_VERSION := $(shell $(UV) run python -m scripts.generate_semver)
|
||||
else
|
||||
NPM_VERSION = $(shell python -m scripts.generate_semver)
|
||||
endif
|
||||
|
||||
all: lint-fix lint gen web test ## Lint, build, and test everything
|
||||
|
||||
HELP_WIDTH := $(shell grep -h '^[a-z][^ ]*:.*\#\#' $(MAKEFILE_LIST) 2>/dev/null | \
|
||||
|
||||
@@ -30,7 +30,6 @@ from drf_spectacular.utils import (
|
||||
extend_schema_field,
|
||||
inline_serializer,
|
||||
)
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
@@ -42,6 +41,7 @@ from rest_framework.fields import (
|
||||
IntegerField,
|
||||
ListField,
|
||||
SerializerMethodField,
|
||||
UUIDField,
|
||||
)
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
@@ -78,6 +78,7 @@ from authentik.core.models import (
|
||||
TokenIntents,
|
||||
User,
|
||||
UserTypes,
|
||||
default_token_duration,
|
||||
)
|
||||
from authentik.endpoints.connectors.agent.auth import AgentAuth
|
||||
from authentik.events.models import Event, EventAction
|
||||
@@ -87,6 +88,7 @@ from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
||||
from authentik.flows.views.executor import QS_KEY_TOKEN
|
||||
from authentik.lib.avatars import get_avatar
|
||||
from authentik.lib.utils.reflection import ConditionalInheritance
|
||||
from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator
|
||||
from authentik.rbac.api.roles import RoleSerializer
|
||||
from authentik.rbac.decorators import permission_required
|
||||
from authentik.rbac.models import Role, get_permission_choices
|
||||
@@ -238,14 +240,14 @@ class UserSerializer(ModelSerializer):
|
||||
and self.instance.type == UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||
and user_type != UserTypes.INTERNAL_SERVICE_ACCOUNT.value
|
||||
):
|
||||
raise ValidationError("Can't change internal service account to other user type.")
|
||||
raise ValidationError(_("Can't change internal service account to other user type."))
|
||||
if not self.instance and user_type == UserTypes.INTERNAL_SERVICE_ACCOUNT.value:
|
||||
raise ValidationError("Setting a user to internal service account is not allowed.")
|
||||
raise ValidationError(_("Setting a user to internal service account is not allowed."))
|
||||
return user_type
|
||||
|
||||
def validate(self, attrs: dict) -> dict:
|
||||
if self.instance and self.instance.type == UserTypes.INTERNAL_SERVICE_ACCOUNT:
|
||||
raise ValidationError("Can't modify internal service account users")
|
||||
raise ValidationError(_("Can't modify internal service account users"))
|
||||
return super().validate(attrs)
|
||||
|
||||
class Meta:
|
||||
@@ -397,6 +399,18 @@ class UserServiceAccountSerializer(PassiveSerializer):
|
||||
)
|
||||
|
||||
|
||||
class UserRecoveryLinkSerializer(PassiveSerializer):
|
||||
"""Payload to create a recovery link"""
|
||||
|
||||
token_duration = CharField(required=False)
|
||||
|
||||
|
||||
class UserRecoveryEmailSerializer(UserRecoveryLinkSerializer):
|
||||
"""Payload to create and email a recovery link"""
|
||||
|
||||
email_stage = UUIDField()
|
||||
|
||||
|
||||
class UsersFilter(FilterSet):
|
||||
"""Filter for users"""
|
||||
|
||||
@@ -458,14 +472,14 @@ class UsersFilter(FilterSet):
|
||||
try:
|
||||
value = loads(value)
|
||||
except ValueError:
|
||||
raise ValidationError(detail="filter: failed to parse JSON") from None
|
||||
raise ValidationError(_("filter: failed to parse JSON")) from None
|
||||
if not isinstance(value, dict):
|
||||
raise ValidationError(detail="filter: value must be key:value mapping")
|
||||
raise ValidationError(_("filter: value must be key:value mapping"))
|
||||
qs = {}
|
||||
for key, _value in value.items():
|
||||
qs[f"attributes__{key}"] = _value
|
||||
try:
|
||||
_ = len(queryset.filter(**qs))
|
||||
__ = len(queryset.filter(**qs))
|
||||
return queryset.filter(**qs)
|
||||
except ValueError:
|
||||
return queryset
|
||||
@@ -543,14 +557,16 @@ class UserViewSet(
|
||||
def list(self, request, *args, **kwargs):
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
def _create_recovery_link(self, for_email=False) -> tuple[str, Token]:
|
||||
def _create_recovery_link(
|
||||
self, token_duration: str | None, for_email=False
|
||||
) -> tuple[str, Token]:
|
||||
"""Create a recovery link (when the current brand has a recovery flow set),
|
||||
that can either be shown to an admin or sent to the user directly"""
|
||||
brand: Brand = self.request._request.brand
|
||||
brand: Brand = self.request.brand
|
||||
# Check that there is a recovery flow, if not return an error
|
||||
flow = brand.flow_recovery
|
||||
if not flow:
|
||||
raise ValidationError({"non_field_errors": "No recovery flow set."})
|
||||
raise ValidationError({"non_field_errors": _("No recovery flow set.")})
|
||||
user: User = self.get_object()
|
||||
planner = FlowPlanner(flow)
|
||||
planner.allow_empty_flows = True
|
||||
@@ -564,11 +580,15 @@ class UserViewSet(
|
||||
)
|
||||
except FlowNonApplicableException:
|
||||
raise ValidationError(
|
||||
{"non_field_errors": "Recovery flow not applicable to user"}
|
||||
{"non_field_errors": _("Recovery flow not applicable to user")}
|
||||
) from None
|
||||
_plan = FlowToken.pickle(plan)
|
||||
if for_email:
|
||||
_plan = pickle_flow_token_for_email(plan)
|
||||
expires = default_token_duration()
|
||||
if token_duration:
|
||||
timedelta_string_validator(token_duration)
|
||||
expires = now() + timedelta_from_string(token_duration)
|
||||
token, __ = FlowToken.objects.update_or_create(
|
||||
identifier=f"{user.uid}-password-reset",
|
||||
defaults={
|
||||
@@ -576,6 +596,7 @@ class UserViewSet(
|
||||
"flow": flow,
|
||||
"_plan": _plan,
|
||||
"revoke_on_execution": not for_email,
|
||||
"expires": expires,
|
||||
},
|
||||
)
|
||||
querystring = urlencode({QS_KEY_TOKEN: token.key})
|
||||
@@ -723,60 +744,60 @@ class UserViewSet(
|
||||
|
||||
@permission_required("authentik_core.reset_user_password")
|
||||
@extend_schema(
|
||||
request=UserRecoveryLinkSerializer,
|
||||
responses={
|
||||
"200": LinkSerializer(many=False),
|
||||
},
|
||||
request=None,
|
||||
)
|
||||
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
|
||||
def recovery(self, request: Request, pk: int) -> Response:
|
||||
@validate(UserRecoveryLinkSerializer)
|
||||
def recovery(self, request: Request, pk: int, body: UserRecoveryLinkSerializer) -> Response:
|
||||
"""Create a temporary link that a user can use to recover their account"""
|
||||
link, _ = self._create_recovery_link()
|
||||
link, _ = self._create_recovery_link(
|
||||
token_duration=body.validated_data.get("token_duration")
|
||||
)
|
||||
return Response({"link": link})
|
||||
|
||||
@permission_required("authentik_core.reset_user_password")
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="email_stage",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.STR,
|
||||
required=True,
|
||||
)
|
||||
],
|
||||
request=UserRecoveryEmailSerializer,
|
||||
responses={
|
||||
"204": OpenApiResponse(description="Successfully sent recover email"),
|
||||
},
|
||||
request=None,
|
||||
)
|
||||
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
|
||||
def recovery_email(self, request: Request, pk: int) -> Response:
|
||||
@validate(UserRecoveryEmailSerializer)
|
||||
def recovery_email(
|
||||
self, request: Request, pk: int, body: UserRecoveryEmailSerializer
|
||||
) -> Response:
|
||||
"""Send an email with a temporary link that a user can use to recover their account"""
|
||||
for_user: User = self.get_object()
|
||||
if for_user.email == "":
|
||||
email_error_message = _("User does not have an email address set.")
|
||||
stage_error_message = _("Email stage not found.")
|
||||
user: User = self.get_object()
|
||||
if not user.email:
|
||||
LOGGER.debug("User doesn't have an email address")
|
||||
raise ValidationError({"non_field_errors": "User does not have an email address set."})
|
||||
link, token = self._create_recovery_link(for_email=True)
|
||||
# Lookup the email stage to assure the current user can access it
|
||||
stages = get_objects_for_user(
|
||||
request.user, "authentik_stages_email.view_emailstage"
|
||||
).filter(pk=request.query_params.get("email_stage"))
|
||||
if not stages.exists():
|
||||
LOGGER.debug("Email stage does not exist/user has no permissions")
|
||||
raise ValidationError({"non_field_errors": "Email stage does not exist."})
|
||||
email_stage: EmailStage = stages.first()
|
||||
raise ValidationError({"non_field_errors": email_error_message})
|
||||
if not (stage := EmailStage.objects.filter(pk=body.validated_data["email_stage"]).first()):
|
||||
LOGGER.debug("Email stage does not exist")
|
||||
raise ValidationError({"non_field_errors": stage_error_message})
|
||||
if not request.user.has_perm("authentik_stages_email.view_emailstage", stage):
|
||||
LOGGER.debug("User has no view access to email stage")
|
||||
raise ValidationError({"non_field_errors": stage_error_message})
|
||||
link, token = self._create_recovery_link(
|
||||
token_duration=body.validated_data.get("token_duration"), for_email=True
|
||||
)
|
||||
message = TemplateEmailMessage(
|
||||
subject=_(email_stage.subject),
|
||||
to=[(for_user.name, for_user.email)],
|
||||
template_name=email_stage.template,
|
||||
language=for_user.locale(request),
|
||||
subject=_(stage.subject),
|
||||
to=[(user.name, user.email)],
|
||||
template_name=stage.template,
|
||||
language=user.locale(request),
|
||||
template_context={
|
||||
"url": link,
|
||||
"user": for_user,
|
||||
"user": user,
|
||||
"expires": token.expires,
|
||||
},
|
||||
)
|
||||
send_mails(email_stage, message)
|
||||
send_mails(stage, message)
|
||||
return Response(status=204)
|
||||
|
||||
@permission_required("authentik_core.impersonate")
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""Test Users API"""
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from json import loads
|
||||
|
||||
from django.urls.base import reverse
|
||||
from django.utils.timezone import now
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.brands.models import Brand
|
||||
@@ -127,13 +128,62 @@ class TestUsersAPI(APITestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_recovery_duration(self):
|
||||
"""Test user recovery token duration"""
|
||||
Token.objects.all().delete()
|
||||
flow = create_test_flow(
|
||||
FlowDesignation.RECOVERY,
|
||||
authentication=FlowAuthenticationRequirement.REQUIRE_UNAUTHENTICATED,
|
||||
)
|
||||
brand: Brand = create_test_brand()
|
||||
brand.flow_recovery = flow
|
||||
brand.save()
|
||||
self.client.force_login(self.admin)
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk}),
|
||||
data={"token_duration": "days=33"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
expires = Token.objects.first().expires
|
||||
expected_expires = now() + timedelta(days=33)
|
||||
self.assertTrue(timedelta(minutes=-1) < expected_expires - expires < timedelta(minutes=1))
|
||||
|
||||
def test_recovery_duration_update(self):
|
||||
"""Test user recovery token duration update"""
|
||||
Token.objects.all().delete()
|
||||
flow = create_test_flow(
|
||||
FlowDesignation.RECOVERY,
|
||||
authentication=FlowAuthenticationRequirement.REQUIRE_UNAUTHENTICATED,
|
||||
)
|
||||
brand: Brand = create_test_brand()
|
||||
brand.flow_recovery = flow
|
||||
brand.save()
|
||||
self.client.force_login(self.admin)
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk}),
|
||||
data={"token_duration": "days=33"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
expires = Token.objects.first().expires
|
||||
expected_expires = now() + timedelta(days=33)
|
||||
self.assertTrue(timedelta(minutes=-1) < expected_expires - expires < timedelta(minutes=1))
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk}),
|
||||
data={"token_duration": "days=66"},
|
||||
)
|
||||
expires = Token.objects.first().expires
|
||||
expected_expires = now() + timedelta(days=66)
|
||||
self.assertTrue(timedelta(minutes=-1) < expected_expires - expires < timedelta(minutes=1))
|
||||
|
||||
def test_recovery_email_no_flow(self):
|
||||
"""Test user recovery link (no recovery flow set)"""
|
||||
self.client.force_login(self.admin)
|
||||
self.user.email = ""
|
||||
self.user.save()
|
||||
stage = EmailStage.objects.create(name="email")
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk})
|
||||
reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk}),
|
||||
data={"email_stage": stage.pk},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
@@ -142,7 +192,8 @@ class TestUsersAPI(APITestCase):
|
||||
self.user.email = "foo@bar.baz"
|
||||
self.user.save()
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk})
|
||||
reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk}),
|
||||
data={"email_stage": stage.pk},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertJSONEqual(response.content, {"non_field_errors": "No recovery flow set."})
|
||||
@@ -160,7 +211,7 @@ class TestUsersAPI(APITestCase):
|
||||
reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk})
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertJSONEqual(response.content, {"non_field_errors": "Email stage does not exist."})
|
||||
self.assertJSONEqual(response.content, {"email_stage": ["This field is required."]})
|
||||
|
||||
def test_recovery_email(self):
|
||||
"""Test user recovery link"""
|
||||
@@ -178,8 +229,8 @@ class TestUsersAPI(APITestCase):
|
||||
reverse(
|
||||
"authentik_api:user-recovery-email",
|
||||
kwargs={"pk": self.user.pk},
|
||||
)
|
||||
+ f"?email_stage={stage.pk}"
|
||||
),
|
||||
data={"email_stage": stage.pk},
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec, rsa
|
||||
from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PrivateKey
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
|
||||
from cryptography.x509.oid import NameOID
|
||||
from django.db import models
|
||||
@@ -21,6 +23,8 @@ class PrivateKeyAlg(models.TextChoices):
|
||||
|
||||
RSA = "rsa", _("rsa")
|
||||
ECDSA = "ecdsa", _("ecdsa")
|
||||
ED25519 = "ed25519", _("Ed25519")
|
||||
ED448 = "ed448", _("Ed448")
|
||||
|
||||
|
||||
class CertificateBuilder:
|
||||
@@ -56,6 +60,10 @@ class CertificateBuilder:
|
||||
return rsa.generate_private_key(
|
||||
public_exponent=65537, key_size=4096, backend=default_backend()
|
||||
)
|
||||
if self.alg == PrivateKeyAlg.ED25519:
|
||||
return Ed25519PrivateKey.generate()
|
||||
if self.alg == PrivateKeyAlg.ED448:
|
||||
return Ed448PrivateKey.generate()
|
||||
raise ValueError(f"Invalid alg: {self.alg}")
|
||||
|
||||
def build(
|
||||
@@ -98,18 +106,25 @@ class CertificateBuilder:
|
||||
self.__builder = self.__builder.add_extension(
|
||||
x509.SubjectAlternativeName(alt_names), critical=True
|
||||
)
|
||||
algo = hashes.SHA256()
|
||||
# EdDSA doesn't take a hash algorithm
|
||||
if isinstance(self.__private_key, (Ed25519PrivateKey | Ed448PrivateKey)):
|
||||
algo = None
|
||||
self.__certificate = self.__builder.sign(
|
||||
private_key=self.__private_key,
|
||||
algorithm=hashes.SHA256(),
|
||||
algorithm=algo,
|
||||
backend=default_backend(),
|
||||
)
|
||||
|
||||
@property
|
||||
def private_key(self):
|
||||
"""Return private key in PEM format"""
|
||||
format = serialization.PrivateFormat.TraditionalOpenSSL
|
||||
if isinstance(self.__private_key, (Ed25519PrivateKey | Ed448PrivateKey)):
|
||||
format = serialization.PrivateFormat.PKCS8
|
||||
return self.__private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
format=format,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
).decode("utf-8")
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ from rest_framework.fields import (
|
||||
)
|
||||
|
||||
from authentik.api.v3.config import ConfigSerializer, ConfigView
|
||||
from authentik.brands.api import CurrentBrandSerializer
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.crypto.apps import MANAGED_KEY
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
@@ -39,7 +37,6 @@ class AgentConfigSerializer(PassiveSerializer):
|
||||
|
||||
system_config = SerializerMethodField()
|
||||
license_status = SerializerMethodField(required=False, allow_null=True)
|
||||
brand = SerializerMethodField(required=False, allow_null=True)
|
||||
|
||||
def get_device_id(self, instance: AgentConnector) -> str:
|
||||
device: Device = self.context["device"]
|
||||
@@ -73,10 +70,6 @@ class AgentConfigSerializer(PassiveSerializer):
|
||||
except ModuleNotFoundError:
|
||||
return None
|
||||
|
||||
def get_brand(self, instance: AgentConnector) -> CurrentBrandSerializer:
|
||||
brand: Brand = self.context["request"]._request.brand
|
||||
return CurrentBrandSerializer(brand, context=self.context).data
|
||||
|
||||
|
||||
class EnrollSerializer(PassiveSerializer):
|
||||
|
||||
|
||||
2
go.mod
2
go.mod
@@ -30,7 +30,7 @@ require (
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2026020.17-0.20260126165226-52b0b9497497
|
||||
goauthentik.io/api/v3 v3.2026020.17-0.20260203144237-cf0a7b7393e7
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
golang.org/x/sync v0.19.0
|
||||
|
||||
2
go.sum
2
go.sum
@@ -216,6 +216,8 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
goauthentik.io/api/v3 v3.2026020.17-0.20260126165226-52b0b9497497 h1:uebnevXt0MnVIdmBPh39hCggT5Mz/DW8diDvv1n9W50=
|
||||
goauthentik.io/api/v3 v3.2026020.17-0.20260126165226-52b0b9497497/go.mod h1:uYa+yGMglhJy8ymyUQ8KQiJjOb3UZTuPQ24Ot2s9BCo=
|
||||
goauthentik.io/api/v3 v3.2026020.17-0.20260203144237-cf0a7b7393e7 h1:0dDYUvv3LXNgYgY0uSpws78J0EPBWGyk6hw45OZwFmY=
|
||||
goauthentik.io/api/v3 v3.2026020.17-0.20260203144237-cf0a7b7393e7/go.mod h1:uYa+yGMglhJy8ymyUQ8KQiJjOb3UZTuPQ24Ot2s9BCo=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
|
||||
@@ -36,6 +36,7 @@ services:
|
||||
- ${COMPOSE_PORT_HTTP:-9000}:9000
|
||||
- ${COMPOSE_PORT_HTTPS:-9443}:9443
|
||||
restart: unless-stopped
|
||||
shm_size: 512mb
|
||||
volumes:
|
||||
- ./data:/data
|
||||
- ./custom-templates:/templates
|
||||
@@ -54,6 +55,7 @@ services:
|
||||
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.0-rc1}
|
||||
restart: unless-stopped
|
||||
shm_size: 512mb
|
||||
user: root
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
@@ -39,7 +39,7 @@ dependencies = [
|
||||
"geopy==2.4.1",
|
||||
"google-api-python-client==2.188.0",
|
||||
"gssapi==1.11.1",
|
||||
"gunicorn==25.0.0",
|
||||
"gunicorn==25.0.1",
|
||||
"jsonpatch==1.33",
|
||||
"jwcrypto==1.5.6",
|
||||
"kubernetes==35.0.0",
|
||||
@@ -77,7 +77,7 @@ dependencies = [
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"aws-cdk-lib==2.236.0",
|
||||
"aws-cdk-lib==2.237.0",
|
||||
"bandit==1.9.3",
|
||||
"black==26.1.0",
|
||||
"bpython==0.26",
|
||||
|
||||
43
schema.yml
43
schema.yml
@@ -4459,6 +4459,11 @@ paths:
|
||||
required: true
|
||||
tags:
|
||||
- core
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserRecoveryLinkRequest'
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
@@ -4478,11 +4483,6 @@ paths:
|
||||
description: Send an email with a temporary link that a user can use to recover
|
||||
their account
|
||||
parameters:
|
||||
- in: query
|
||||
name: email_stage
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
@@ -4491,6 +4491,12 @@ paths:
|
||||
required: true
|
||||
tags:
|
||||
- core
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserRecoveryEmailRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
@@ -33466,15 +33472,9 @@ components:
|
||||
- $ref: '#/components/schemas/LicenseStatusEnum'
|
||||
readOnly: true
|
||||
nullable: true
|
||||
brand:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/CurrentBrand'
|
||||
readOnly: true
|
||||
nullable: true
|
||||
required:
|
||||
- auth_terminate_session_on_expiry
|
||||
- authorization_flow
|
||||
- brand
|
||||
- device_id
|
||||
- jwks_auth
|
||||
- jwks_challenge
|
||||
@@ -33671,6 +33671,8 @@ components:
|
||||
enum:
|
||||
- rsa
|
||||
- ecdsa
|
||||
- ed25519
|
||||
- ed448
|
||||
type: string
|
||||
App:
|
||||
type: object
|
||||
@@ -56522,6 +56524,25 @@ components:
|
||||
- plex_token
|
||||
- source
|
||||
- user
|
||||
UserRecoveryEmailRequest:
|
||||
type: object
|
||||
description: Payload to create and email a recovery link
|
||||
properties:
|
||||
token_duration:
|
||||
type: string
|
||||
minLength: 1
|
||||
email_stage:
|
||||
type: string
|
||||
format: uuid
|
||||
required:
|
||||
- email_stage
|
||||
UserRecoveryLinkRequest:
|
||||
type: object
|
||||
description: Payload to create a recovery link
|
||||
properties:
|
||||
token_duration:
|
||||
type: string
|
||||
minLength: 1
|
||||
UserRequest:
|
||||
type: object
|
||||
description: User Serializer
|
||||
|
||||
@@ -41,6 +41,7 @@ base = {
|
||||
"AUTHENTIK_POSTGRESQL__USER": "${PG_USER:-authentik}",
|
||||
"AUTHENTIK_SECRET_KEY": "${AUTHENTIK_SECRET_KEY:?secret key required}",
|
||||
},
|
||||
"shm_size": "512mb",
|
||||
"image": authentik_image,
|
||||
"ports": ["${COMPOSE_PORT_HTTP:-9000}:9000", "${COMPOSE_PORT_HTTPS:-9443}:9443"],
|
||||
"restart": "unless-stopped",
|
||||
@@ -59,6 +60,7 @@ base = {
|
||||
"AUTHENTIK_POSTGRESQL__USER": "${PG_USER:-authentik}",
|
||||
"AUTHENTIK_SECRET_KEY": "${AUTHENTIK_SECRET_KEY:?secret key required}",
|
||||
},
|
||||
"shm_size": "512mb",
|
||||
"image": authentik_image,
|
||||
"restart": "unless-stopped",
|
||||
"user": "root",
|
||||
|
||||
16
uv.lock
generated
16
uv.lock
generated
@@ -368,7 +368,7 @@ requires-dist = [
|
||||
{ name = "geopy", specifier = "==2.4.1" },
|
||||
{ name = "google-api-python-client", specifier = "==2.188.0" },
|
||||
{ name = "gssapi", specifier = "==1.11.1" },
|
||||
{ name = "gunicorn", specifier = "==25.0.0" },
|
||||
{ name = "gunicorn", specifier = "==25.0.1" },
|
||||
{ name = "jsonpatch", specifier = "==1.33" },
|
||||
{ name = "jwcrypto", specifier = "==1.5.6" },
|
||||
{ name = "kubernetes", specifier = "==35.0.0" },
|
||||
@@ -406,7 +406,7 @@ requires-dist = [
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "aws-cdk-lib", specifier = "==2.236.0" },
|
||||
{ name = "aws-cdk-lib", specifier = "==2.237.0" },
|
||||
{ name = "bandit", specifier = "==1.9.3" },
|
||||
{ name = "black", specifier = "==26.1.0" },
|
||||
{ name = "bpython", specifier = "==0.26" },
|
||||
@@ -517,7 +517,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-cdk-lib"
|
||||
version = "2.236.0"
|
||||
version = "2.237.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aws-cdk-asset-awscli-v1" },
|
||||
@@ -528,9 +528,9 @@ dependencies = [
|
||||
{ name = "publication" },
|
||||
{ name = "typeguard" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/6d/2b54697444a806257f19ed603ef34ff71b6c3beb6f916587087ffb016ff5/aws_cdk_lib-2.236.0.tar.gz", hash = "sha256:1ed9f3798101d3271fd219bc101b9eab41dead3a25dde516a3c16b8274e0e77a", size = 47209293, upload-time = "2026-01-23T17:39:34.624Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fd/dc/d8f8a177b586b7724a6bf72d7273fefee6642e956816a6c6d972548a2d47/aws_cdk_lib-2.237.0.tar.gz", hash = "sha256:82b0d9880c2352ea71d9e62e4d55331150c95795e1c9429b7ce2bd47cda8ff9d", size = 47318762, upload-time = "2026-02-02T13:44:05.471Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/53/82/6c47d68033aab1e778e226477d2863663ee59bac4345560a83f34f737e77/aws_cdk_lib-2.236.0-py3-none-any.whl", hash = "sha256:b724c1313a184ce5d62ff63f0594a1a4e12d098f7036fe07e7557a73229d0037", size = 47857717, upload-time = "2026-01-23T17:38:45.327Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/63/ff0f4df41a8c29266e3960269b7b1dee4c11ae68a897d6c7f90b6a1cd32d/aws_cdk_lib-2.237.0-py3-none-any.whl", hash = "sha256:10e60c9060c9b461234afcc2932273a08033e2b796ca7f732b64a7be40d295d3", size = 47970102, upload-time = "2026-02-02T13:43:27.586Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1748,14 +1748,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "gunicorn"
|
||||
version = "25.0.0"
|
||||
version = "25.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "packaging" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/46/4d/74e685d22a7d8dcc920e4e84da2842c057141236757143e1c167e7bd64df/gunicorn-25.0.0.tar.gz", hash = "sha256:4b1ab820ffc316352da7146005a9e66044a131434b17a7d2a9fb0604f1268864", size = 9618408, upload-time = "2026-02-01T13:34:59.463Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9e/83/e8327358129ca4dffd4fa6b6004aa5085dc80e913dec9b253401d6bd23ad/gunicorn-25.0.1.tar.gz", hash = "sha256:573e053aa950246e307ea908bd7ddce1870d41a40aec0c935938c586f0b9b946", size = 9693127, upload-time = "2026-02-02T13:34:05.767Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/9e/1be849edbea786424848e206e38a8b6025d140b7b9430037c5dc91e8a83f/gunicorn-25.0.0-py3-none-any.whl", hash = "sha256:b9b88f0ebc5a0fcd1e9e29a1f045f56f5402c6423ee3e3f675f679d0db5345d1", size = 169675, upload-time = "2026-02-01T13:34:54.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/dc/f1da097b7e0de5cd7552c10667305879093125cd62ff7372ad07d184ed8f/gunicorn-25.0.1-py3-none-any.whl", hash = "sha256:23cbe968c6ae3c8efc3d118c8353fa0763efc2102d89d0d3cea696cede7ff6b1", size = 169961, upload-time = "2026-02-02T13:34:02.744Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
88
web/package-lock.json
generated
88
web/package-lock.json
generated
@@ -44,10 +44,10 @@
|
||||
"@patternfly/patternfly": "^4.224.2",
|
||||
"@playwright/test": "^1.58.1",
|
||||
"@sentry/browser": "^10.38.0",
|
||||
"@storybook/addon-docs": "^10.2.3",
|
||||
"@storybook/addon-links": "^10.2.3",
|
||||
"@storybook/web-components": "^10.2.3",
|
||||
"@storybook/web-components-vite": "^10.2.3",
|
||||
"@storybook/addon-docs": "^10.2.4",
|
||||
"@storybook/addon-links": "^10.2.4",
|
||||
"@storybook/web-components": "^10.2.4",
|
||||
"@storybook/web-components-vite": "^10.2.4",
|
||||
"@types/codemirror": "^5.60.17",
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
"@types/guacamole-common-js": "^1.5.5",
|
||||
@@ -77,7 +77,7 @@
|
||||
"globals": "^17.3.0",
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
"hastscript": "^9.0.1",
|
||||
"knip": "^5.82.1",
|
||||
"knip": "^5.83.0",
|
||||
"lex": "^2025.11.0",
|
||||
"lit": "^3.3.2",
|
||||
"lit-analyzer": "^2.0.3",
|
||||
@@ -3188,15 +3188,15 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@storybook/addon-docs": {
|
||||
"version": "10.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.2.3.tgz",
|
||||
"integrity": "sha512-IPprt2qp4HN1uyE1Ki1sH0ZOE5B6z5sKzEMfrKMGokYKYk/AAJVfSiVIKju3q525GrBFlNhRW2+fB4pQfklv2w==",
|
||||
"version": "10.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.2.4.tgz",
|
||||
"integrity": "sha512-FzscAmdBiOGnGrxiEM+8eTg43kjqgjLfObg+lbJVRR/a0DmZ3xfAPNB0+VKYQbN0FacNcWLM9LZ/7U0hRBPBnQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@storybook/csf-plugin": "10.2.3",
|
||||
"@storybook/csf-plugin": "10.2.4",
|
||||
"@storybook/icons": "^2.0.1",
|
||||
"@storybook/react-dom-shim": "10.2.3",
|
||||
"@storybook/react-dom-shim": "10.2.4",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"ts-dedent": "^2.0.0"
|
||||
@@ -3206,13 +3206,13 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^10.2.3"
|
||||
"storybook": "^10.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-links": {
|
||||
"version": "10.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.2.3.tgz",
|
||||
"integrity": "sha512-ewOUga9zhcGQRGTTl7PyaV8kwLL4Jj1oeXWF2fq4fx+Fhzcn+d99gu3uV+zrGZa1gueBIRwf+p6NJTO//xSVUw==",
|
||||
"version": "10.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.2.4.tgz",
|
||||
"integrity": "sha512-4ifXpDmCmDyWS6LPr5KYsriJBMX46G35ZzYeQQdMks4OaXjZlEjL07fgxmH6Jen9ijDNqWlqWAMX/NVo3QUxUg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0"
|
||||
@@ -3223,7 +3223,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.2.3"
|
||||
"storybook": "^10.2.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
@@ -3232,12 +3232,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/builder-vite": {
|
||||
"version": "10.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.2.3.tgz",
|
||||
"integrity": "sha512-sKSccERL23gqC+nkTD+4io3ZF8vvNXkNiSi16X6BC29sGsbgWohw3Nv6tIGwVkIlQh0b1z24EQgXYWbdqivHGw==",
|
||||
"version": "10.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.2.4.tgz",
|
||||
"integrity": "sha512-/hcT1xj3CL5GkJ5v5/EguZdttDwNE6weNXK7vKzp034tnGcLycOossDsTiUQkBowSL+Ylc8aKj+ZgvddPNfOig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/csf-plugin": "10.2.3",
|
||||
"@storybook/csf-plugin": "10.2.4",
|
||||
"ts-dedent": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
@@ -3245,14 +3245,14 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^10.2.3",
|
||||
"storybook": "^10.2.4",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/csf-plugin": {
|
||||
"version": "10.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.2.3.tgz",
|
||||
"integrity": "sha512-/b/C8C40ukzXs3Xauud2+yOJqwBdOkADfRtJ9O4TzrhftzkEdqsNI03xXZySeh7eXW8eI3Vq4t75Ljuj27Xytw==",
|
||||
"version": "10.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.2.4.tgz",
|
||||
"integrity": "sha512-kupPQEV+4N9mzsZHYaokvhO/KHBjYdWda9PNmPQwy0TR7r2mzthgaNH72TjmgN1L6DIbsuyOG1wtczcPJn4+Jg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"unplugin": "^2.3.5"
|
||||
@@ -3264,7 +3264,7 @@
|
||||
"peerDependencies": {
|
||||
"esbuild": "*",
|
||||
"rollup": "*",
|
||||
"storybook": "^10.2.3",
|
||||
"storybook": "^10.2.4",
|
||||
"vite": "*",
|
||||
"webpack": "*"
|
||||
},
|
||||
@@ -3300,9 +3300,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-dom-shim": {
|
||||
"version": "10.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.2.3.tgz",
|
||||
"integrity": "sha512-xMZXvjfQCsmzOTqFCRQ1/gxs//jDGLlnmBCikH4NSGPPogRPaNUkxgdNjOResd6pB+G3ZYAOspJkmGEEbq8dVw==",
|
||||
"version": "10.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.2.4.tgz",
|
||||
"integrity": "sha512-i22OtrZ7GeZPt/odLf0vqyDhRSKyaLsHkkKSBcANQfzRRnBZmiz2FchOtWm9uvoDWybQsTruZq7kTdtpEhwyGw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -3311,13 +3311,13 @@
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.2.3"
|
||||
"storybook": "^10.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/web-components": {
|
||||
"version": "10.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/web-components/-/web-components-10.2.3.tgz",
|
||||
"integrity": "sha512-yrypL1aEQR6b9HQYX+xDCXZ05syy+RrYBe6e8KGaL2e3w9hdmB3Zb4SKGmaZsTTkY6UuSeLYG7Rk5IK6SyfhwA==",
|
||||
"version": "10.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/web-components/-/web-components-10.2.4.tgz",
|
||||
"integrity": "sha512-UotANIirYETFuK4IkFLGnmBzTfHsK9rGTXi74IdSPkbKTFCg7b/vpd90f9oe/zjUpkNPjrZivkxfaUirm78uyw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
@@ -3330,24 +3330,24 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"lit": "^2.0.0 || ^3.0.0",
|
||||
"storybook": "^10.2.3"
|
||||
"storybook": "^10.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/web-components-vite": {
|
||||
"version": "10.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/web-components-vite/-/web-components-vite-10.2.3.tgz",
|
||||
"integrity": "sha512-eJsu4v8G8s/xRkaeRj/tgGrJXDkUXDagAZ3xEFgahhShrYLc+wEUZ2fDj0ZE33UN+RGKsR0a/r0vdM5N6S3Mlw==",
|
||||
"version": "10.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/web-components-vite/-/web-components-vite-10.2.4.tgz",
|
||||
"integrity": "sha512-SAd/mtxMRv055IEp80J23DYfPn29izNXdcp4B7a44UKCVN0H3/enotie5fYQnG+jU3oPwg5JRPtFz3hyzrb+sw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/builder-vite": "10.2.3",
|
||||
"@storybook/web-components": "10.2.3"
|
||||
"@storybook/builder-vite": "10.2.4",
|
||||
"@storybook/web-components": "10.2.4"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^10.2.3"
|
||||
"storybook": "^10.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@swagger-api/apidom-ast": {
|
||||
@@ -10741,9 +10741,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/knip": {
|
||||
"version": "5.82.1",
|
||||
"resolved": "https://registry.npmjs.org/knip/-/knip-5.82.1.tgz",
|
||||
"integrity": "sha512-1nQk+5AcnkqL40kGQXfouzAEXkTR+eSrgo/8m1d0BMei4eAzFwghoXC4gOKbACgBiCof7hE8wkBVDsEvznf85w==",
|
||||
"version": "5.83.0",
|
||||
"resolved": "https://registry.npmjs.org/knip/-/knip-5.83.0.tgz",
|
||||
"integrity": "sha512-FfmaHMntpZB13B1oJQMSs1hTOZxd0TOn+FYB3oWEI02XlxTW3RH4H7d8z5Us3g0ziHCYyl7z0B1xi8ENP3QEKA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -15292,9 +15292,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/storybook": {
|
||||
"version": "10.2.3",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.2.3.tgz",
|
||||
"integrity": "sha512-kjsJ0hctkTO0ipHiyv1MY39wP4tAyVM7rPQGyVMU1iQ7NYHxthiiCHhFB/szmVjXdJa58fu3ZH5cwENMn8Y5eA==",
|
||||
"version": "10.2.4",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.2.4.tgz",
|
||||
"integrity": "sha512-LwF0VZsT4qkgx66Ad/q0QgZZrU2a5WftaADDEcJ3bGq3O2fHvwWPlSZjM1HiXD4vqP9U5JiMqQkV1gkyH0XJkw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
|
||||
@@ -119,10 +119,10 @@
|
||||
"@patternfly/patternfly": "^4.224.2",
|
||||
"@playwright/test": "^1.58.1",
|
||||
"@sentry/browser": "^10.38.0",
|
||||
"@storybook/addon-docs": "^10.2.3",
|
||||
"@storybook/addon-links": "^10.2.3",
|
||||
"@storybook/web-components": "^10.2.3",
|
||||
"@storybook/web-components-vite": "^10.2.3",
|
||||
"@storybook/addon-docs": "^10.2.4",
|
||||
"@storybook/addon-links": "^10.2.4",
|
||||
"@storybook/web-components": "^10.2.4",
|
||||
"@storybook/web-components-vite": "^10.2.4",
|
||||
"@types/codemirror": "^5.60.17",
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
"@types/guacamole-common-js": "^1.5.5",
|
||||
@@ -152,7 +152,7 @@
|
||||
"globals": "^17.3.0",
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
"hastscript": "^9.0.1",
|
||||
"knip": "^5.82.1",
|
||||
"knip": "^5.83.0",
|
||||
"lex": "^2025.11.0",
|
||||
"lit": "^3.3.2",
|
||||
"lit-analyzer": "^2.0.3",
|
||||
|
||||
@@ -53,8 +53,6 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
|
||||
settingsRequest,
|
||||
});
|
||||
|
||||
this.dispatchEvent(new CustomEvent("ak-admin-setting-changed"));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,12 +51,9 @@ export class AdminSettingsPage extends AKElement {
|
||||
@state()
|
||||
protected settings?: Settings;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
public override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.#refresh();
|
||||
|
||||
this.addEventListener("ak-admin-setting-changed", this.#refresh);
|
||||
}
|
||||
|
||||
#refresh = () => {
|
||||
@@ -65,14 +62,6 @@ export class AdminSettingsPage extends AKElement {
|
||||
});
|
||||
};
|
||||
|
||||
#save = () => {
|
||||
return this.form?.submit(new SubmitEvent("submit")).then(this.#refresh);
|
||||
};
|
||||
|
||||
#reset = () => {
|
||||
return this.form?.reset();
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.settings) return nothing;
|
||||
|
||||
@@ -80,17 +69,14 @@ export class AdminSettingsPage extends AKElement {
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter">
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__body">
|
||||
<ak-admin-settings-form id="form" .settings=${this.settings}>
|
||||
<ak-admin-settings-form
|
||||
id="form"
|
||||
.settings=${this.settings}
|
||||
action-label=${msg("Update settings")}
|
||||
@ak-form-submitted=${{ handleEvent: this.#refresh, passive: true }}
|
||||
>
|
||||
</ak-admin-settings-form>
|
||||
</div>
|
||||
<div class="pf-c-card__footer">
|
||||
<ak-spinner-button .callAction=${this.#save} class="pf-m-primary"
|
||||
>${msg("Save")}</ak-spinner-button
|
||||
>
|
||||
<ak-spinner-button .callAction=${this.#reset} class="pf-m-secondary"
|
||||
>${msg("Cancel")}</ak-spinner-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
|
||||
@@ -94,7 +94,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Application(s)")}
|
||||
object-label=${msg("Application(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.usedBy=${(item: Application) => {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreApplicationsUsedByList({
|
||||
|
||||
@@ -400,6 +400,7 @@ export class ApplicationViewPage extends AKElement {
|
||||
</div>
|
||||
</section>
|
||||
<ak-rbac-object-permission-page
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-permissions"
|
||||
|
||||
@@ -54,7 +54,7 @@ export class ApplicationEntitlementsPage extends Table<ApplicationEntitlement> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Application entitlement(s)")}
|
||||
object-label=${msg("Application entitlement(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.usedBy=${(item: ApplicationEntitlement) => {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsUsedByList({
|
||||
|
||||
@@ -88,7 +88,7 @@ export class BlueprintListPage extends TablePage<BlueprintInstance> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Blueprint(s)")}
|
||||
object-label=${msg("Blueprint(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: BlueprintInstance) => {
|
||||
return [{ key: msg("Name"), value: item.name }];
|
||||
|
||||
@@ -50,7 +50,7 @@ export class BrandListPage extends TablePage<Brand> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Brand(s)")}
|
||||
object-label=${msg("Brand(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: Brand) => {
|
||||
return [{ key: msg("Domain"), value: item.domain }];
|
||||
|
||||
@@ -57,6 +57,14 @@ export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
|
||||
label: msg("ECDSA"),
|
||||
value: AlgEnum.Ecdsa,
|
||||
},
|
||||
{
|
||||
label: msg("ED25519"),
|
||||
value: AlgEnum.Ed25519,
|
||||
},
|
||||
{
|
||||
label: msg("ED448"),
|
||||
value: AlgEnum.Ed448,
|
||||
},
|
||||
]}
|
||||
>
|
||||
</ak-radio>
|
||||
|
||||
@@ -61,7 +61,7 @@ export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
const count = this.selectedElements.length;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${count === 1 ? msg("Certificate-Key Pair") : msg("Certificate-Key Pairs")}
|
||||
object-label=${count === 1 ? msg("Certificate-Key Pair") : msg("Certificate-Key Pairs")}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: CertificateKeyPair) => {
|
||||
return [
|
||||
|
||||
@@ -76,7 +76,7 @@ export class DeviceAccessGroupsListPage extends TablePage<DeviceAccessGroup> {
|
||||
renderToolbarSelected() {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Device Group(s)")}
|
||||
object-label=${msg("Device Group(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: DeviceAccessGroup) => {
|
||||
return [{ key: msg("Name"), value: item.name }];
|
||||
|
||||
@@ -72,7 +72,7 @@ export class ConnectorsListPage extends TablePage<Connector> {
|
||||
renderToolbarSelected() {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Connector(s)")}
|
||||
object-label=${msg("Connector(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: Connector) => {
|
||||
return [{ key: msg("Name"), value: item.name }];
|
||||
|
||||
@@ -54,7 +54,7 @@ export class EnrollmentTokenListPage extends Table<EnrollmentToken> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Enrollment Token(s)")}
|
||||
object-label=${msg("Enrollment Token(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: EnrollmentToken) => {
|
||||
return [
|
||||
|
||||
@@ -1,21 +1,33 @@
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { writeToClipboard } from "#common/clipboard";
|
||||
|
||||
import TokenCopyButton from "#elements/buttons/TokenCopyButton/ak-token-copy-button";
|
||||
|
||||
import { EndpointsApi } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-enrollment-token-copy-button")
|
||||
export class EnrollmentTokenCopyButton extends TokenCopyButton {
|
||||
callAction: () => Promise<unknown> = () => {
|
||||
public override entityLabel = msg("Enrollment Token");
|
||||
|
||||
public override callAction(): Promise<null> {
|
||||
if (!this.identifier) {
|
||||
return Promise.reject();
|
||||
throw new TypeError("No `identifier` set for `EnrollmentTokenCopyButton`");
|
||||
}
|
||||
return new EndpointsApi(DEFAULT_CONFIG).endpointsAgentsEnrollmentTokensViewKeyRetrieve({
|
||||
tokenUuid: this.identifier,
|
||||
|
||||
// Safari permission hack.
|
||||
const text = new ClipboardItem({
|
||||
"text/plain": new EndpointsApi(DEFAULT_CONFIG)
|
||||
.endpointsAgentsEnrollmentTokensViewKeyRetrieve({
|
||||
tokenUuid: this.identifier,
|
||||
})
|
||||
.then((tokenView) => new Blob([tokenView.key], { type: "text/plain" })),
|
||||
});
|
||||
};
|
||||
|
||||
return writeToClipboard(text, this.entityLabel).then(() => null);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -148,7 +148,7 @@ export class DeviceListPage extends TablePage<EndpointDevice> {
|
||||
renderToolbarSelected() {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Endpoint Device(s)")}
|
||||
object-label=${msg("Endpoint Device(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: EndpointDevice) => {
|
||||
return [{ key: msg("Name"), value: item.name }];
|
||||
|
||||
@@ -113,7 +113,7 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("License(s)")}
|
||||
object-label=${msg("License(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: License) => {
|
||||
return [
|
||||
|
||||
@@ -57,7 +57,7 @@ export class DataExportListPage extends TablePage<DataExport> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Data export(s)")}
|
||||
object-label=${msg("Data export(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: DataExport) => {
|
||||
return [
|
||||
|
||||
@@ -57,7 +57,7 @@ export class RuleListPage extends TablePage<NotificationRule> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Notification rule(s)")}
|
||||
object-label=${msg("Notification rule(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.usedBy=${(item: NotificationRule) => {
|
||||
return new EventsApi(DEFAULT_CONFIG).eventsRulesUsedByList({
|
||||
|
||||
@@ -55,7 +55,7 @@ export class TransportListPage extends TablePage<NotificationTransport> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Notification transport(s)")}
|
||||
object-label=${msg("Notification transport(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.usedBy=${(item: NotificationTransport) => {
|
||||
return new EventsApi(DEFAULT_CONFIG).eventsTransportsUsedByList({
|
||||
|
||||
@@ -77,7 +77,7 @@ export class FileListPage extends WithCapabilitiesConfig(TablePage<FileItem>) {
|
||||
const disabled = !this.selectedElements.length;
|
||||
const count = this.selectedElements.length;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${count === 1 ? msg("file") : msg("files")}
|
||||
object-label=${count === 1 ? msg("file") : msg("files")}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: FileItem) => {
|
||||
return [
|
||||
|
||||
@@ -56,7 +56,7 @@ export class BoundStagesList extends Table<FlowStageBinding> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Stage binding(s)")}
|
||||
object-label=${msg("Stage binding(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: FlowStageBinding) => {
|
||||
return [
|
||||
|
||||
@@ -61,7 +61,7 @@ export class FlowListPage extends TablePage<Flow> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Flow(s)")}
|
||||
object-label=${msg("Flow(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.usedBy=${(item: Flow) => {
|
||||
return new FlowsApi(DEFAULT_CONFIG).flowsInstancesUsedByList({
|
||||
|
||||
@@ -298,6 +298,7 @@ export class FlowViewPage extends AKElement {
|
||||
</div>
|
||||
</div>
|
||||
<ak-rbac-object-permission-page
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-permissions"
|
||||
|
||||
@@ -51,7 +51,7 @@ export class GroupListPage extends TablePage<Group> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Group(s)")}
|
||||
object-label=${msg("Group(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.usedBy=${(item: Group) => {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreGroupsUsedByList({
|
||||
|
||||
@@ -247,6 +247,7 @@ export class GroupViewPage extends AKElement {
|
||||
${this.renderTabRoles(this.group)}
|
||||
</section>
|
||||
<ak-rbac-object-permission-page
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-permissions"
|
||||
|
||||
@@ -113,12 +113,12 @@ export class RelatedGroupList extends Table<Group> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Group(s)")}
|
||||
actionLabel=${msg("Remove from Group(s)")}
|
||||
actionSubtext=${msg(
|
||||
object-label=${msg("Group(s)")}
|
||||
action-label=${msg("Remove from Group(s)")}
|
||||
action-subtext=${msg(
|
||||
str`Are you sure you want to remove user ${this.targetUser?.username} from the following groups?`,
|
||||
)}
|
||||
buttonLabel=${msg("Remove")}
|
||||
button-label=${msg("Remove")}
|
||||
.objects=${this.selectedElements}
|
||||
.delete=${(item: Group) => {
|
||||
if (!this.targetUser) return;
|
||||
|
||||
@@ -15,11 +15,8 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { PFSize } from "#common/enums";
|
||||
import { parseAPIResponseError, pluckErrorDetail } from "#common/errors/network";
|
||||
import { MessageLevel } from "#common/messages";
|
||||
|
||||
import { Form } from "#elements/forms/Form";
|
||||
import { showMessage } from "#elements/messages/MessageContainer";
|
||||
import { WithBrandConfig } from "#elements/mixins/branding";
|
||||
import { CapabilitiesEnum, WithCapabilitiesConfig } from "#elements/mixins/capabilities";
|
||||
import { getURLParam, updateURLParams } from "#elements/router/RouteMatch";
|
||||
@@ -29,6 +26,8 @@ import { UserOption } from "#elements/user/utils";
|
||||
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
import { renderRecoveryButtons } from "#admin/users/UserListPage";
|
||||
|
||||
import { CoreApi, CoreUsersListTypeEnum, Group, RbacApi, Role, User } from "@goauthentik/api";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
@@ -195,10 +194,10 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
const targetLabel = this.targetGroup?.name || this.targetRole?.name;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("User(s)")}
|
||||
actionLabel=${msg("Remove User(s)")}
|
||||
object-label=${msg("User(s)")}
|
||||
action-label=${msg("Remove User(s)")}
|
||||
action=${msg("removed")}
|
||||
actionSubtext=${targetLabel
|
||||
action-subtext=${targetLabel
|
||||
? msg(str`Are you sure you want to remove the selected users from ${targetLabel}?`)
|
||||
: msg("Are you sure you want to remove the selected users?")}
|
||||
.objects=${this.selectedElements}
|
||||
@@ -303,7 +302,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-user-active-form
|
||||
.obj=${item}
|
||||
objectLabel=${msg("User")}
|
||||
object-label=${msg("User")}
|
||||
.delete=${() => {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreUsersPartialUpdate({
|
||||
id: item.pk || 0,
|
||||
@@ -326,78 +325,10 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update password")}</span>
|
||||
<span slot="header">
|
||||
${msg(str`Update ${item.name || item.username}'s password`)}
|
||||
</span>
|
||||
<ak-user-password-form
|
||||
username=${item.username}
|
||||
email=${ifDefined(item.email)}
|
||||
slot="form"
|
||||
.instancePk=${item.pk}
|
||||
></ak-user-password-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||
${msg("Set password")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
${this.brand.flowRecovery
|
||||
? html`
|
||||
<ak-action-button
|
||||
class="pf-m-secondary"
|
||||
.apiRequest=${() => {
|
||||
return new CoreApi(DEFAULT_CONFIG)
|
||||
.coreUsersRecoveryCreate({
|
||||
id: item.pk,
|
||||
})
|
||||
.then((rec) => {
|
||||
showMessage({
|
||||
level: MessageLevel.success,
|
||||
message: msg(
|
||||
"Successfully generated recovery link",
|
||||
),
|
||||
description: rec.link,
|
||||
});
|
||||
})
|
||||
.catch(async (error: unknown) => {
|
||||
const parsedError =
|
||||
await parseAPIResponseError(error);
|
||||
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: pluckErrorDetail(parsedError),
|
||||
});
|
||||
});
|
||||
}}
|
||||
>
|
||||
${msg("Copy recovery link")}
|
||||
</ak-action-button>
|
||||
${item.email
|
||||
? html`<ak-forms-modal .closeAfterSuccessfulSubmit=${false}>
|
||||
<span slot="submit"> ${msg("Send link")} </span>
|
||||
<span slot="header">
|
||||
${msg("Send recovery link to user")}
|
||||
</span>
|
||||
<ak-user-reset-email-form slot="form" .user=${item}>
|
||||
</ak-user-reset-email-form>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="pf-c-button pf-m-secondary"
|
||||
>
|
||||
${msg("Email recovery link")}
|
||||
</button>
|
||||
</ak-forms-modal>`
|
||||
: html`<span
|
||||
>${msg(
|
||||
"Recovery link cannot be emailed, user has no email address saved.",
|
||||
)}</span
|
||||
>`}
|
||||
`
|
||||
: html` <p>
|
||||
${msg(
|
||||
"To let a user directly reset a their password, configure a recovery flow on the currently active brand.",
|
||||
)}
|
||||
</p>`}
|
||||
${renderRecoveryButtons({
|
||||
user: item,
|
||||
brandHasRecoveryFlow: Boolean(this.brand.flowRecovery),
|
||||
})}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
@@ -439,7 +370,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
||||
: nothing}
|
||||
<ak-dropdown class="pf-c-dropdown">
|
||||
<button
|
||||
class="pf-m-secondary pf-c-dropdown__toggle"
|
||||
class="pf-c-button pf-m-secondary pf-c-dropdown__toggle"
|
||||
type="button"
|
||||
id="add-user-toggle"
|
||||
aria-haspopup="menu"
|
||||
@@ -449,10 +380,9 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
||||
<span class="pf-c-dropdown__toggle-text">${msg("Add new user")}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul
|
||||
<menu
|
||||
class="pf-c-dropdown__menu"
|
||||
hidden
|
||||
role="menu"
|
||||
id="add-user-menu"
|
||||
aria-labelledby="add-user-toggle"
|
||||
tabindex="-1"
|
||||
@@ -526,7 +456,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
||||
</a>
|
||||
</ak-forms-modal>
|
||||
</li>
|
||||
</ul>
|
||||
</menu>
|
||||
</ak-dropdown>
|
||||
${super.renderToolbar()}
|
||||
`;
|
||||
|
||||
@@ -211,7 +211,7 @@ export class OutpostListPage extends TablePage<Outpost> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Outpost(s)")}
|
||||
object-label=${msg("Outpost(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.usedBy=${(item: Outpost) => {
|
||||
return new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesUsedByList({
|
||||
|
||||
@@ -143,7 +143,7 @@ export class OutpostServiceConnectionListPage extends TablePage<ServiceConnectio
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Outpost integration(s)")}
|
||||
object-label=${msg("Outpost integration(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.usedBy=${(item: ServiceConnection) => {
|
||||
return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsAllUsedByList({
|
||||
|
||||
@@ -148,7 +148,7 @@ export class BoundPoliciesList<T extends PolicyBinding = PolicyBinding> extends
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Policy binding(s)")}
|
||||
object-label=${msg("Policy binding(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: PolicyBinding) => {
|
||||
return [
|
||||
|
||||
@@ -99,7 +99,7 @@ export class PolicyListPage extends TablePage<Policy> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Policy / Policies")}
|
||||
object-label=${msg("Policy / Policies")}
|
||||
.objects=${this.selectedElements}
|
||||
.usedBy=${(item: Policy) => {
|
||||
return new PoliciesApi(DEFAULT_CONFIG).policiesAllUsedByList({
|
||||
|
||||
@@ -58,7 +58,7 @@ export class ReputationListPage extends TablePage<Reputation> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Reputation")}
|
||||
object-label=${msg("Reputation")}
|
||||
.objects=${this.selectedElements}
|
||||
.usedBy=${(item: Reputation) => {
|
||||
return new PoliciesApi(DEFAULT_CONFIG).policiesReputationScoresUsedByList({
|
||||
|
||||
@@ -67,7 +67,7 @@ export class PropertyMappingListPage extends TablePage<PropertyMapping> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Property Mapping(s)")}
|
||||
object-label=${msg("Property Mapping(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.usedBy=${(item: PropertyMapping) => {
|
||||
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsAllUsedByList({
|
||||
|
||||
@@ -68,7 +68,7 @@ export class ProviderListPage extends TablePage<Provider> {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Provider(s)")}
|
||||
object-label=${msg("Provider(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.usedBy=${(item: Provider) => {
|
||||
return new ProvidersApi(DEFAULT_CONFIG).providersAllUsedByList({
|
||||
|
||||
@@ -53,7 +53,7 @@ export class GoogleWorkspaceProviderGroupList extends Table<GoogleWorkspaceProvi
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Google Workspace Group(s)")}
|
||||
object-label=${msg("Google Workspace Group(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.delete=${(item: GoogleWorkspaceProviderGroup) => {
|
||||
return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceGroupsDestroy({
|
||||
|
||||
@@ -53,7 +53,7 @@ export class GoogleWorkspaceProviderUserList extends Table<GoogleWorkspaceProvid
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Google Workspace User(s)")}
|
||||
object-label=${msg("Google Workspace User(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.delete=${(item: GoogleWorkspaceProviderUser) => {
|
||||
return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceUsersDestroy({
|
||||
|
||||
@@ -142,6 +142,7 @@ export class GoogleWorkspaceProviderViewPage extends AKElement {
|
||||
</div>
|
||||
</section>
|
||||
<ak-rbac-object-permission-page
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-permissions"
|
||||
|
||||
@@ -111,6 +111,7 @@ export class LDAPProviderViewPage extends WithSession(AKElement) {
|
||||
</div>
|
||||
</div>
|
||||
<ak-rbac-object-permission-page
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-permissions"
|
||||
|
||||
@@ -50,7 +50,7 @@ export class MicrosoftEntraProviderGroupList extends Table<MicrosoftEntraProvide
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Microsoft Entra Group(s)")}
|
||||
object-label=${msg("Microsoft Entra Group(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.delete=${(item: MicrosoftEntraProviderGroup) => {
|
||||
return new ProvidersApi(DEFAULT_CONFIG).providersMicrosoftEntraGroupsDestroy({
|
||||
|
||||
@@ -53,7 +53,7 @@ export class MicrosoftEntraProviderUserList extends Table<MicrosoftEntraProvider
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Microsoft Entra User(s)")}
|
||||
object-label=${msg("Microsoft Entra User(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.delete=${(item: MicrosoftEntraProviderUser) => {
|
||||
return new ProvidersApi(DEFAULT_CONFIG).providersMicrosoftEntraUsersDestroy({
|
||||
|
||||
@@ -142,6 +142,7 @@ export class MicrosoftEntraProviderViewPage extends AKElement {
|
||||
</div>
|
||||
</div>
|
||||
<ak-rbac-object-permission-page
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-permissions"
|
||||
|
||||
@@ -172,6 +172,7 @@ export class OAuth2ProviderViewPage extends AKElement {
|
||||
</div>
|
||||
</div>
|
||||
<ak-rbac-object-permission-page
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-permissions"
|
||||
|
||||
@@ -239,6 +239,7 @@ export class ProxyProviderViewPage extends AKElement {
|
||||
</div>
|
||||
</div>
|
||||
<ak-rbac-object-permission-page
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-permissions"
|
||||
|
||||
@@ -45,7 +45,7 @@ export class ConnectionTokenListPage extends Table<ConnectionToken> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Connection Token(s)")}
|
||||
object-label=${msg("Connection Token(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: ConnectionToken) => {
|
||||
return [
|
||||
|
||||
@@ -57,7 +57,7 @@ export class EndpointListPage extends Table<Endpoint> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Endpoint(s)")}
|
||||
object-label=${msg("Endpoint(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: Endpoint) => {
|
||||
return [
|
||||
|
||||
@@ -129,6 +129,7 @@ export class RACProviderViewPage extends AKElement {
|
||||
</div>
|
||||
</div>
|
||||
<ak-rbac-object-permission-page
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-permissions"
|
||||
|
||||
@@ -175,6 +175,7 @@ export class RadiusProviderViewPage extends AKElement {
|
||||
</div>
|
||||
</div>
|
||||
<ak-rbac-object-permission-page
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-permissions"
|
||||
|
||||
@@ -269,6 +269,7 @@ export class SAMLProviderViewPage extends AKElement {
|
||||
</div>
|
||||
</div>
|
||||
<ak-rbac-object-permission-page
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-permissions"
|
||||
|
||||
@@ -50,7 +50,7 @@ export class SCIMProviderGroupList extends Table<SCIMProviderGroup> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("SCIM Group(s)")}
|
||||
object-label=${msg("SCIM Group(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.delete=${(item: SCIMProviderGroup) => {
|
||||
return new ProvidersApi(DEFAULT_CONFIG).providersScimGroupsDestroy({
|
||||
|
||||
@@ -50,7 +50,7 @@ export class SCIMProviderUserList extends Table<SCIMProviderUser> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("SCIM User(s)")}
|
||||
object-label=${msg("SCIM User(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.delete=${(item: SCIMProviderUser) => {
|
||||
return new ProvidersApi(DEFAULT_CONFIG).providersScimUsersDestroy({
|
||||
|
||||
@@ -148,6 +148,7 @@ export class SCIMProviderViewPage extends AKElement {
|
||||
</div>
|
||||
</div>
|
||||
<ak-rbac-object-permission-page
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-permissions"
|
||||
|
||||
@@ -108,6 +108,7 @@ export class SSFProviderViewPage extends AKElement {
|
||||
</div>
|
||||
</div>
|
||||
<ak-rbac-object-permission-page
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-permissions"
|
||||
|
||||
@@ -250,6 +250,7 @@ export class WSFederationProviderViewPage extends AKElement {
|
||||
</div>
|
||||
</div>
|
||||
<ak-rbac-object-permission-page
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-permissions"
|
||||
|
||||
@@ -45,7 +45,7 @@ export class InitialPermissionsListPage extends TablePage<InitialPermissions> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Initial Permissions")}
|
||||
object-label=${msg("Initial Permissions")}
|
||||
.objects=${this.selectedElements}
|
||||
.usedBy=${(item: InitialPermissions) => {
|
||||
return new RbacApi(DEFAULT_CONFIG).rbacInitialPermissionsUsedByList({
|
||||
|
||||
@@ -8,7 +8,7 @@ import { AKElement } from "#elements/Base";
|
||||
import { RbacPermissionsAssignedByRolesListModelEnum } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
import { css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
@@ -17,53 +17,42 @@ import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
|
||||
|
||||
@customElement("ak-rbac-object-permission-page")
|
||||
export class ObjectPermissionPage extends AKElement {
|
||||
static styles = [
|
||||
PFGrid,
|
||||
PFPage,
|
||||
PFCard,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@property()
|
||||
public model?: RbacPermissionsAssignedByRolesListModelEnum;
|
||||
|
||||
@property()
|
||||
objectPk?: string | number;
|
||||
public objectPk?: string | number;
|
||||
|
||||
@property({ type: Boolean })
|
||||
embedded = false;
|
||||
|
||||
static styles = [PFGrid, PFPage, PFCard];
|
||||
public embedded = false;
|
||||
|
||||
render() {
|
||||
return this.model === RbacPermissionsAssignedByRolesListModelEnum.AuthentikRbacRole
|
||||
? html`<ak-tabs pageIdentifier="permissionPage" ?vertical=${!this.embedded}>
|
||||
${this.renderPermissionsAssignedToRole()} ${this.renderPermissionsOnObject()}
|
||||
${this.renderPermissionsAssignedToRole()}
|
||||
</ak-tabs>`
|
||||
: this.renderPermissionsOnObject();
|
||||
}
|
||||
|
||||
renderPermissionsOnObject() {
|
||||
return html` <div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-object-role"
|
||||
id="page-object-role"
|
||||
aria-label="${msg("Permissions on this object")}"
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-l-grid pf-m-gutter">
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
||||
<div class="pf-c-card__title">${msg("Permissions on this object")}</div>
|
||||
<div class="pf-c-card__body">
|
||||
${msg("Permissions set on roles which affect this object.")}
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<ak-rbac-role-object-permission-table
|
||||
.model=${this.model}
|
||||
.objectPk=${this.objectPk}
|
||||
>
|
||||
</ak-rbac-role-object-permission-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
return html`<div class="pf-c-card__body">
|
||||
<ak-rbac-role-object-permission-table .model=${this.model} .objectPk=${this.objectPk}>
|
||||
</ak-rbac-role-object-permission-table>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
renderPermissionsAssignedToRole() {
|
||||
protected renderPermissionsAssignedToRole() {
|
||||
return html`
|
||||
<div
|
||||
role="tabpanel"
|
||||
@@ -111,6 +100,24 @@ export class ObjectPermissionPage extends AKElement {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-object-role"
|
||||
id="page-object-role"
|
||||
aria-label="${msg("Permissions on this object")}"
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-l-grid pf-m-gutter">
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
||||
<div class="pf-c-card__title">${msg("Permissions on this object")}</div>
|
||||
<div class="pf-c-card__body">
|
||||
${msg("Permissions set on roles which affect this object.")}
|
||||
</div>
|
||||
${this.renderPermissionsOnObject()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ export class RoleAssignedObjectPermissionTable extends Table<RoleAssignedObjectP
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Permission(s)")}
|
||||
object-label=${msg("Permission(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: RoleAssignedObjectPermission) => {
|
||||
return [{ key: msg("Permission"), value: item.name }];
|
||||
|
||||
@@ -150,12 +150,12 @@ export class RelatedRoleList extends Table<Role> {
|
||||
}
|
||||
const disabled = !this.selectedElements.length;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Role(s)")}
|
||||
actionLabel=${msg("Remove from Role(s)")}
|
||||
actionSubtext=${msg(
|
||||
object-label=${msg("Role(s)")}
|
||||
action-label=${msg("Remove from Role(s)")}
|
||||
action-subtext=${msg(
|
||||
str`Are you sure you want to remove user ${this.targetUser?.username} from the following roles?`,
|
||||
)}
|
||||
buttonLabel=${msg("Remove")}
|
||||
button-label=${msg("Remove")}
|
||||
.objects=${this.selectedElements}
|
||||
.delete=${(item: Role) => {
|
||||
if (!this.targetUser) return;
|
||||
|
||||
@@ -64,7 +64,7 @@ export class RoleAssignedGlobalPermissionsTable extends Table<Permission> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Permission(s)")}
|
||||
object-label=${msg("Permission(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.delete=${(item: Permission) => {
|
||||
return new RbacApi(
|
||||
|
||||
@@ -46,7 +46,7 @@ export class RoleAssignedObjectPermissionTable extends Table<ExtraRoleObjectPerm
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Permission(s)")}
|
||||
object-label=${msg("Permission(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: ExtraRoleObjectPermission) => {
|
||||
return [
|
||||
|
||||
@@ -52,7 +52,7 @@ export class RoleListPage extends TablePage<Role> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Role(s)")}
|
||||
object-label=${msg("Role(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.usedBy=${(item: Role) => {
|
||||
return new RbacApi(DEFAULT_CONFIG).rbacRolesUsedByList({
|
||||
|
||||
@@ -141,6 +141,7 @@ export class RoleViewPage extends AKElement {
|
||||
</div>
|
||||
</section>
|
||||
<ak-rbac-object-permission-page
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-permissions"
|
||||
|
||||
@@ -55,7 +55,7 @@ export class SourceListPage extends TablePage<Source> {
|
||||
this.selectedElements.some((item) => item.component === "");
|
||||
const nonBuiltInSources = this.selectedElements.filter((item) => item.component !== "");
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Source(s)")}
|
||||
object-label=${msg("Source(s)")}
|
||||
.objects=${nonBuiltInSources}
|
||||
.usedBy=${(item: Source) => {
|
||||
return new SourcesApi(DEFAULT_CONFIG).sourcesAllUsedByList({
|
||||
|
||||
@@ -206,6 +206,7 @@ export class KerberosSourceViewPage extends AKElement {
|
||||
</div>
|
||||
</div>
|
||||
<ak-rbac-object-permission-page
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-permissions"
|
||||
|
||||
@@ -204,6 +204,7 @@ export class LDAPSourceViewPage extends AKElement {
|
||||
</div>
|
||||
</div>
|
||||
<ak-rbac-object-permission-page
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-permissions"
|
||||
|
||||
@@ -272,6 +272,7 @@ export class OAuthSourceViewPage extends AKElement {
|
||||
</div>
|
||||
</div>
|
||||
<ak-rbac-object-permission-page
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-permissions"
|
||||
|
||||
@@ -156,6 +156,7 @@ export class PlexSourceViewPage extends AKElement {
|
||||
</div>
|
||||
</div>
|
||||
<ak-rbac-object-permission-page
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-permissions"
|
||||
|
||||
@@ -235,6 +235,7 @@ export class SAMLSourceViewPage extends AKElement {
|
||||
</div>
|
||||
</div>
|
||||
<ak-rbac-object-permission-page
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-permissions"
|
||||
|
||||
@@ -217,6 +217,7 @@ export class SCIMSourceViewPage extends AKElement {
|
||||
</div>
|
||||
</div>
|
||||
<ak-rbac-object-permission-page
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-permissions"
|
||||
|
||||
@@ -1,31 +1,5 @@
|
||||
import "#admin/stages/register";
|
||||
import "#admin/rbac/ObjectPermissionModal";
|
||||
import "#admin/stages/StageWizard";
|
||||
import "#admin/stages/authenticator_duo/AuthenticatorDuoStageForm";
|
||||
import "#admin/stages/authenticator_duo/DuoDeviceImportForm";
|
||||
import "#admin/stages/authenticator_email/AuthenticatorEmailStageForm";
|
||||
import "#admin/stages/authenticator_endpoint_gdtc/AuthenticatorEndpointGDTCStageForm";
|
||||
import "#admin/stages/authenticator_sms/AuthenticatorSMSStageForm";
|
||||
import "#admin/stages/authenticator_static/AuthenticatorStaticStageForm";
|
||||
import "#admin/stages/authenticator_totp/AuthenticatorTOTPStageForm";
|
||||
import "#admin/stages/authenticator_validate/AuthenticatorValidateStageForm";
|
||||
import "#admin/stages/authenticator_webauthn/AuthenticatorWebAuthnStageForm";
|
||||
import "#admin/stages/captcha/CaptchaStageForm";
|
||||
import "#admin/stages/consent/ConsentStageForm";
|
||||
import "#admin/stages/deny/DenyStageForm";
|
||||
import "#admin/stages/dummy/DummyStageForm";
|
||||
import "#admin/stages/email/EmailStageForm";
|
||||
import "#admin/stages/endpoint/EndpointStageForm";
|
||||
import "#admin/stages/identification/IdentificationStageForm";
|
||||
import "#admin/stages/invitation/InvitationStageForm";
|
||||
import "#admin/stages/mtls/MTLSStageForm";
|
||||
import "#admin/stages/password/PasswordStageForm";
|
||||
import "#admin/stages/prompt/PromptStageForm";
|
||||
import "#admin/stages/redirect/RedirectStageForm";
|
||||
import "#admin/stages/source/SourceStageForm";
|
||||
import "#admin/stages/user_delete/UserDeleteStageForm";
|
||||
import "#admin/stages/user_login/UserLoginStageForm";
|
||||
import "#admin/stages/user_logout/UserLogoutStageForm";
|
||||
import "#admin/stages/user_write/UserWriteStageForm";
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
@@ -73,7 +47,7 @@ export class StageListPage extends TablePage<Stage> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Stage(s)")}
|
||||
object-label=${msg("Stage(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.usedBy=${(item: Stage) => {
|
||||
return new StagesApi(DEFAULT_CONFIG).stagesAllUsedByList({
|
||||
|
||||
@@ -1,28 +1,5 @@
|
||||
import "#admin/stages/register";
|
||||
import "#admin/common/ak-license-notice";
|
||||
import "#admin/stages/authenticator_duo/AuthenticatorDuoStageForm";
|
||||
import "#admin/stages/authenticator_email/AuthenticatorEmailStageForm";
|
||||
import "#admin/stages/authenticator_sms/AuthenticatorSMSStageForm";
|
||||
import "#admin/stages/authenticator_static/AuthenticatorStaticStageForm";
|
||||
import "#admin/stages/authenticator_totp/AuthenticatorTOTPStageForm";
|
||||
import "#admin/stages/authenticator_validate/AuthenticatorValidateStageForm";
|
||||
import "#admin/stages/authenticator_webauthn/AuthenticatorWebAuthnStageForm";
|
||||
import "#admin/stages/captcha/CaptchaStageForm";
|
||||
import "#admin/stages/consent/ConsentStageForm";
|
||||
import "#admin/stages/deny/DenyStageForm";
|
||||
import "#admin/stages/dummy/DummyStageForm";
|
||||
import "#admin/stages/email/EmailStageForm";
|
||||
import "#admin/stages/identification/IdentificationStageForm";
|
||||
import "#admin/stages/invitation/InvitationStageForm";
|
||||
import "#admin/stages/mtls/MTLSStageForm";
|
||||
import "#admin/stages/endpoint/EndpointStageForm";
|
||||
import "#admin/stages/password/PasswordStageForm";
|
||||
import "#admin/stages/prompt/PromptStageForm";
|
||||
import "#admin/stages/redirect/RedirectStageForm";
|
||||
import "#admin/stages/source/SourceStageForm";
|
||||
import "#admin/stages/user_delete/UserDeleteStageForm";
|
||||
import "#admin/stages/user_login/UserLoginStageForm";
|
||||
import "#admin/stages/user_logout/UserLogoutStageForm";
|
||||
import "#admin/stages/user_write/UserWriteStageForm";
|
||||
import "#elements/wizard/FormWizardPage";
|
||||
import "#elements/wizard/TypeCreateWizardPage";
|
||||
import "#elements/wizard/Wizard";
|
||||
|
||||
@@ -88,7 +88,7 @@ export class InvitationListPage extends TablePage<Invitation> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Invitation(s)")}
|
||||
object-label=${msg("Invitation(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.usedBy=${(item: Invitation) => {
|
||||
return new StagesApi(DEFAULT_CONFIG).stagesInvitationInvitationsUsedByList({
|
||||
|
||||
@@ -50,7 +50,7 @@ export class PromptListPage extends TablePage<Prompt> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Prompt(s)")}
|
||||
object-label=${msg("Prompt(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.usedBy=${(item: Prompt) => {
|
||||
return new StagesApi(DEFAULT_CONFIG).stagesPromptPromptsUsedByList({
|
||||
|
||||
31
web/src/admin/stages/register.ts
Normal file
31
web/src/admin/stages/register.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @file Register all stage forms for admin interface.
|
||||
*/
|
||||
|
||||
import "#admin/stages/StageWizard";
|
||||
import "#admin/stages/authenticator_duo/AuthenticatorDuoStageForm";
|
||||
import "#admin/stages/authenticator_email/AuthenticatorEmailStageForm";
|
||||
import "#admin/stages/authenticator_sms/AuthenticatorSMSStageForm";
|
||||
import "#admin/stages/authenticator_static/AuthenticatorStaticStageForm";
|
||||
import "#admin/stages/authenticator_totp/AuthenticatorTOTPStageForm";
|
||||
import "#admin/stages/authenticator_validate/AuthenticatorValidateStageForm";
|
||||
import "#admin/stages/authenticator_webauthn/AuthenticatorWebAuthnStageForm";
|
||||
import "#admin/stages/captcha/CaptchaStageForm";
|
||||
import "#admin/stages/consent/ConsentStageForm";
|
||||
import "#admin/stages/deny/DenyStageForm";
|
||||
import "#admin/stages/dummy/DummyStageForm";
|
||||
import "#admin/stages/email/EmailStageForm";
|
||||
import "#admin/stages/identification/IdentificationStageForm";
|
||||
import "#admin/stages/invitation/InvitationStageForm";
|
||||
import "#admin/stages/mtls/MTLSStageForm";
|
||||
import "#admin/stages/endpoint/EndpointStageForm";
|
||||
import "#admin/stages/password/PasswordStageForm";
|
||||
import "#admin/stages/prompt/PromptStageForm";
|
||||
import "#admin/stages/redirect/RedirectStageForm";
|
||||
import "#admin/stages/source/SourceStageForm";
|
||||
import "#admin/stages/user_delete/UserDeleteStageForm";
|
||||
import "#admin/stages/user_login/UserLoginStageForm";
|
||||
import "#admin/stages/user_logout/UserLogoutStageForm";
|
||||
import "#admin/stages/user_write/UserWriteStageForm";
|
||||
import "#admin/stages/authenticator_duo/DuoDeviceImportForm";
|
||||
import "#admin/stages/authenticator_endpoint_gdtc/AuthenticatorEndpointGDTCStageForm";
|
||||
@@ -60,7 +60,7 @@ export class TokenListPage extends TablePage<Token> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Token(s)")}
|
||||
object-label=${msg("Token(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: Token) => {
|
||||
return [{ key: msg("Identifier"), value: item.identifier }];
|
||||
@@ -86,7 +86,7 @@ export class TokenListPage extends TablePage<Token> {
|
||||
return html`
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Create")}</span>
|
||||
<span slot="header">${msg("Create Token")}</span>
|
||||
<span slot="header">${msg("New Token")}</span>
|
||||
<ak-token-form slot="form"> </ak-token-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
|
||||
</ak-forms-modal>
|
||||
|
||||
@@ -76,7 +76,7 @@ export class UserDeviceTable extends Table<Device> {
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Device(s)")}
|
||||
object-label=${msg("Device(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.delete=${(item: Device) => {
|
||||
return this.deleteWrapper(item);
|
||||
|
||||
@@ -6,6 +6,7 @@ import "#admin/users/UserForm";
|
||||
import "#admin/users/UserImpersonateForm";
|
||||
import "#admin/users/UserPasswordForm";
|
||||
import "#admin/users/UserResetEmailForm";
|
||||
import "#admin/users/UserRecoveryLinkForm";
|
||||
import "#components/ak-status-label";
|
||||
import "#elements/TreeView";
|
||||
import "#elements/buttons/ActionButton/index";
|
||||
@@ -15,12 +16,9 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { PFSize } from "#common/enums";
|
||||
import { parseAPIResponseError } from "#common/errors/network";
|
||||
import { userTypeToLabel } from "#common/labels";
|
||||
import { MessageLevel } from "#common/messages";
|
||||
import { DefaultUIConfig } from "#common/ui/config";
|
||||
|
||||
import { showAPIErrorMessage, showMessage } from "#elements/messages/MessageContainer";
|
||||
import { WithBrandConfig } from "#elements/mixins/branding";
|
||||
import { CapabilitiesEnum, WithCapabilitiesConfig } from "#elements/mixins/capabilities";
|
||||
import { WithSession } from "#elements/mixins/session";
|
||||
@@ -28,7 +26,6 @@ import { getURLParam, updateURLParams } from "#elements/router/RouteMatch";
|
||||
import { PaginatedResponse, TableColumn, Timestamp } from "#elements/table/Table";
|
||||
import { TablePage } from "#elements/table/TablePage";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
import { writeToClipboard } from "#elements/utils/writeToClipboard";
|
||||
|
||||
import { CoreApi, CoreUsersExportCreateRequest, User, UserPath } from "@goauthentik/api";
|
||||
|
||||
@@ -41,33 +38,56 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||
|
||||
export const requestRecoveryLink = (user: User) =>
|
||||
new CoreApi(DEFAULT_CONFIG)
|
||||
.coreUsersRecoveryCreate({
|
||||
id: user.pk,
|
||||
})
|
||||
.then((rec) =>
|
||||
writeToClipboard(rec.link).then((wroteToClipboard) =>
|
||||
showMessage({
|
||||
level: MessageLevel.success,
|
||||
message: rec.link,
|
||||
description: wroteToClipboard
|
||||
? msg("A copy of this recovery link has been placed in your clipboard")
|
||||
: "",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.catch((error: unknown) => parseAPIResponseError(error).then(showAPIErrorMessage));
|
||||
|
||||
export const renderRecoveryEmailRequest = (user: User) =>
|
||||
html`<ak-forms-modal .closeAfterSuccessfulSubmit=${false} id="ak-email-recovery-request">
|
||||
<span slot="submit">${msg("Send link")}</span>
|
||||
<span slot="header">${msg("Send recovery link to user")}</span>
|
||||
<ak-user-reset-email-form slot="form" .user=${user}> </ak-user-reset-email-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||
${msg("Email recovery link")}
|
||||
</button>
|
||||
</ak-forms-modal>`;
|
||||
export const renderRecoveryButtons = ({
|
||||
user,
|
||||
brandHasRecoveryFlow,
|
||||
}: {
|
||||
user: User;
|
||||
brandHasRecoveryFlow: boolean;
|
||||
}) =>
|
||||
html` <ak-forms-modal size=${PFSize.Medium} id="update-password-request">
|
||||
<span slot="submit">${msg("Update password")}</span>
|
||||
<span slot="header">
|
||||
${msg(str`Update ${user.name || user.username}'s password`)}
|
||||
</span>
|
||||
<ak-user-password-form
|
||||
username=${user.username}
|
||||
email=${ifDefined(user.email)}
|
||||
slot="form"
|
||||
.instancePk=${user.pk}
|
||||
></ak-user-password-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||
${msg("Set password")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
${brandHasRecoveryFlow
|
||||
? html`
|
||||
<ak-forms-modal id="ak-link-recovery-request">
|
||||
<span slot="submit"> ${msg("Create link")} </span>
|
||||
<span slot="header"> ${msg("Create recovery link")} </span>
|
||||
<ak-user-recovery-link-form slot="form" .user=${user}>
|
||||
</ak-user-recovery-link-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||
${msg("Create recovery link")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
${user.email
|
||||
? html`<ak-forms-modal id="ak-email-recovery-request">
|
||||
<span slot="submit">${msg("Send link")}</span>
|
||||
<span slot="header">${msg("Send recovery link to user")}</span>
|
||||
<ak-user-reset-email-form slot="form" .user=${user}>
|
||||
</ak-user-reset-email-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||
${msg("Email recovery link")}
|
||||
</button>
|
||||
</ak-forms-modal>`
|
||||
: html`<p>
|
||||
${msg("To email a recovery link, set an email address for this user.")}
|
||||
</p>`}
|
||||
`
|
||||
: html` <p>
|
||||
${msg("To create a recovery link, set a recovery flow for the current brand.")}
|
||||
</p>`}`;
|
||||
|
||||
const recoveryButtonStyles = css`
|
||||
#recovery-request-buttons {
|
||||
@@ -170,7 +190,7 @@ export class UserListPage extends WithBrandConfig(
|
||||
</button>
|
||||
</ak-user-bulk-revoke-sessions>
|
||||
<ak-forms-delete-bulk
|
||||
objectLabel=${msg("User(s)")}
|
||||
object-label=${msg("User(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: User) => {
|
||||
return [
|
||||
@@ -318,8 +338,8 @@ export class UserListPage extends WithBrandConfig(
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-user-active-form
|
||||
object-label=${msg("User")}
|
||||
.obj=${item}
|
||||
objectLabel=${msg("User")}
|
||||
.delete=${() => {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreUsersPartialUpdate({
|
||||
id: item.pk,
|
||||
@@ -342,42 +362,10 @@ export class UserListPage extends WithBrandConfig(
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text" id="recovery-request-buttons">
|
||||
<ak-forms-modal size=${PFSize.Medium} id="update-password-request">
|
||||
<span slot="submit">${msg("Update password")}</span>
|
||||
<span slot="header">
|
||||
${msg(str`Update ${item.name || item.username}'s password`)}
|
||||
</span>
|
||||
<ak-user-password-form
|
||||
username=${item.username}
|
||||
email=${ifDefined(item.email)}
|
||||
slot="form"
|
||||
.instancePk=${item.pk}
|
||||
></ak-user-password-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||
${msg("Set password")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
${this.brand.flowRecovery
|
||||
? html`
|
||||
<ak-action-button
|
||||
class="pf-m-secondary"
|
||||
.apiRequest=${() => requestRecoveryLink(item)}
|
||||
>
|
||||
${msg("Create recovery link")}
|
||||
</ak-action-button>
|
||||
${item.email
|
||||
? renderRecoveryEmailRequest(item)
|
||||
: html`<span
|
||||
>${msg(
|
||||
"Recovery link cannot be emailed, user has no email address saved.",
|
||||
)}</span
|
||||
>`}
|
||||
`
|
||||
: html` <p>
|
||||
${msg(
|
||||
"To let a user directly reset their password, configure a recovery flow on the currently active brand.",
|
||||
)}
|
||||
</p>`}
|
||||
${renderRecoveryButtons({
|
||||
user: item,
|
||||
brandHasRecoveryFlow: Boolean(this.brand.flowRecovery),
|
||||
})}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
49
web/src/admin/users/UserRecoveryLinkForm.ts
Normal file
49
web/src/admin/users/UserRecoveryLinkForm.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import "#components/ak-text-input";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { writeToClipboard } from "#common/clipboard";
|
||||
|
||||
import { Form } from "#elements/forms/Form";
|
||||
|
||||
import { CoreApi, Link, User, UserRecoveryLinkRequest } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-user-recovery-link-form")
|
||||
export class UserRecoveryLinkForm extends Form<UserRecoveryLinkRequest> {
|
||||
@property({ attribute: false })
|
||||
user!: User;
|
||||
|
||||
async send(data: UserRecoveryLinkRequest): Promise<Link> {
|
||||
const response = await new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryCreate({
|
||||
id: this.user.pk,
|
||||
userRecoveryLinkRequest: data,
|
||||
});
|
||||
|
||||
await writeToClipboard(response.link, msg("Recovery link"));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`
|
||||
<ak-text-input
|
||||
name="tokenDuration"
|
||||
label=${msg("Token duration")}
|
||||
value="days=1"
|
||||
.bighelp=${html`<p class="pf-c-form__helper-text">
|
||||
${msg("If a recovery token already exists, its duration is updated.")}
|
||||
</p>`}
|
||||
>
|
||||
</ak-text-input>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-user-recovery-link-form": UserRecoveryLinkForm;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import "#components/ak-text-input";
|
||||
import "#elements/forms/HorizontalFormElement";
|
||||
import "#elements/forms/SearchSelect/index";
|
||||
|
||||
@@ -8,11 +9,11 @@ import { Form } from "#elements/forms/Form";
|
||||
|
||||
import {
|
||||
CoreApi,
|
||||
CoreUsersRecoveryEmailCreateRequest,
|
||||
Stage,
|
||||
StagesAllListRequest,
|
||||
StagesApi,
|
||||
User,
|
||||
UserRecoveryEmailRequest,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
@@ -20,48 +21,59 @@ import { html, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-user-reset-email-form")
|
||||
export class UserResetEmailForm extends Form<CoreUsersRecoveryEmailCreateRequest> {
|
||||
export class UserResetEmailForm extends Form<UserRecoveryEmailRequest> {
|
||||
@property({ attribute: false })
|
||||
user!: User;
|
||||
|
||||
getSuccessMessage(): string {
|
||||
return msg("Successfully sent email.");
|
||||
return msg("Successfully queued email.");
|
||||
}
|
||||
|
||||
async send(data: CoreUsersRecoveryEmailCreateRequest): Promise<void> {
|
||||
data.id = this.user.pk;
|
||||
return new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryEmailCreate(data);
|
||||
async send(data: UserRecoveryEmailRequest): Promise<void> {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryEmailCreate({
|
||||
id: this.user.pk,
|
||||
userRecoveryEmailRequest: data,
|
||||
});
|
||||
}
|
||||
|
||||
protected override renderForm(): TemplateResult {
|
||||
return html`<ak-form-element-horizontal
|
||||
label=${msg("Email stage")}
|
||||
required
|
||||
name="emailStage"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Stage[]> => {
|
||||
const args: StagesAllListRequest = {
|
||||
ordering: "name",
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const stages = await new StagesApi(DEFAULT_CONFIG).stagesEmailList(args);
|
||||
return stages.results;
|
||||
}}
|
||||
.groupBy=${(items: Stage[]) => {
|
||||
return groupBy(items, (stage) => stage.verboseNamePlural);
|
||||
}}
|
||||
.renderElement=${(stage: Stage): string => {
|
||||
return stage.name;
|
||||
}}
|
||||
.value=${(stage: Stage | undefined): string | undefined => {
|
||||
return stage?.pk;
|
||||
}}
|
||||
label=${msg("Email stage")}
|
||||
required
|
||||
name="emailStage"
|
||||
>
|
||||
</ak-search-select>
|
||||
</ak-form-element-horizontal>`;
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Stage[]> => {
|
||||
const args: StagesAllListRequest = {
|
||||
ordering: "name",
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const stages = await new StagesApi(DEFAULT_CONFIG).stagesEmailList(args);
|
||||
return stages.results;
|
||||
}}
|
||||
.groupBy=${(items: Stage[]) => {
|
||||
return groupBy(items, (stage) => stage.verboseNamePlural);
|
||||
}}
|
||||
.renderElement=${(stage: Stage): string => {
|
||||
return stage.name;
|
||||
}}
|
||||
.value=${(stage: Stage | undefined): string | undefined => {
|
||||
return stage?.pk;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-text-input
|
||||
name="tokenDuration"
|
||||
label=${msg("Token duration")}
|
||||
value="days=1"
|
||||
.bighelp=${html`<p class="pf-c-form__helper-text">
|
||||
${msg("If a recovery token already exists, its duration is updated.")}
|
||||
</p>`}
|
||||
>
|
||||
</ak-text-input>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import { PFSize } from "#common/enums";
|
||||
import { userTypeToLabel } from "#common/labels";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { WithBrandConfig } from "#elements/mixins/branding";
|
||||
import { WithCapabilitiesConfig } from "#elements/mixins/capabilities";
|
||||
import { WithSession } from "#elements/mixins/session";
|
||||
import { Timestamp } from "#elements/table/shared";
|
||||
@@ -39,7 +40,7 @@ import { Timestamp } from "#elements/table/shared";
|
||||
import { setPageDetails } from "#components/ak-page-navbar";
|
||||
import { type DescriptionPair, renderDescriptionList } from "#components/DescriptionList";
|
||||
|
||||
import { renderRecoveryEmailRequest, requestRecoveryLink } from "#admin/users/UserListPage";
|
||||
import { renderRecoveryButtons } from "#admin/users/UserListPage";
|
||||
|
||||
import {
|
||||
CapabilitiesEnum,
|
||||
@@ -64,7 +65,7 @@ import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
|
||||
import PFSizing from "@patternfly/patternfly/utilities/Sizing/sizing.css";
|
||||
|
||||
@customElement("ak-user-view")
|
||||
export class UserViewPage extends WithCapabilitiesConfig(WithSession(AKElement)) {
|
||||
export class UserViewPage extends WithBrandConfig(WithCapabilitiesConfig(WithSession(AKElement))) {
|
||||
@property({ type: Number })
|
||||
set userId(id: number) {
|
||||
new CoreApi(DEFAULT_CONFIG)
|
||||
@@ -103,9 +104,9 @@ export class UserViewPage extends WithCapabilitiesConfig(WithSession(AKElement))
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
#ak-email-recovery-request,
|
||||
#update-password-request .pf-c-button,
|
||||
#ak-email-recovery-request .pf-c-button {
|
||||
#ak-email-recovery-request .pf-c-button,
|
||||
#ak-link-recovery-request .pf-c-button {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
@@ -129,7 +130,7 @@ export class UserViewPage extends WithCapabilitiesConfig(WithSession(AKElement))
|
||||
[msg("Type"), userTypeToLabel(user.type)],
|
||||
[msg("Superuser"), html`<ak-status-label type="warning" ?good=${user.isSuperuser}></ak-status-label>`],
|
||||
[msg("Actions"), this.renderActionButtons(user)],
|
||||
[msg("Recovery"), this.renderRecoveryButtons(user)],
|
||||
[msg("Recovery"), renderRecoveryButtons({user, brandHasRecoveryFlow: Boolean(this.brand.flowRecovery)})],
|
||||
];
|
||||
|
||||
return html`
|
||||
@@ -155,7 +156,7 @@ export class UserViewPage extends WithCapabilitiesConfig(WithSession(AKElement))
|
||||
</ak-forms-modal>
|
||||
<ak-user-active-form
|
||||
.obj=${user}
|
||||
objectLabel=${msg("User")}
|
||||
object-label=${msg("User")}
|
||||
.delete=${() => {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreUsersPartialUpdate({
|
||||
id: user.pk,
|
||||
@@ -199,43 +200,6 @@ export class UserViewPage extends WithCapabilitiesConfig(WithSession(AKElement))
|
||||
</div> `;
|
||||
}
|
||||
|
||||
renderRecoveryButtons(user: User) {
|
||||
return html`<div class="ak-button-collection">
|
||||
<ak-forms-modal size=${PFSize.Medium} id="update-password-request">
|
||||
<span slot="submit">${msg("Update password")}</span>
|
||||
<span slot="header">
|
||||
${msg(str`Update ${user.name || user.username}'s password`)}
|
||||
</span>
|
||||
|
||||
<ak-user-password-form
|
||||
username=${user.username}
|
||||
email=${ifDefined(user.email)}
|
||||
slot="form"
|
||||
.instancePk=${user.pk}
|
||||
>
|
||||
</ak-user-password-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-secondary pf-m-block">
|
||||
<pf-tooltip position="top" content=${msg("Enter a new password for this user")}>
|
||||
${msg("Set password")}
|
||||
</pf-tooltip>
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
<ak-action-button
|
||||
id="reset-password-button"
|
||||
class="pf-m-secondary pf-m-block"
|
||||
.apiRequest=${() => requestRecoveryLink(user)}
|
||||
>
|
||||
<pf-tooltip
|
||||
position="top"
|
||||
content=${msg("Create a link for this user to reset their password")}
|
||||
>
|
||||
${msg("Create Recovery Link")}
|
||||
</pf-tooltip>
|
||||
</ak-action-button>
|
||||
${user.email ? renderRecoveryEmailRequest(user) : nothing}
|
||||
</div> `;
|
||||
}
|
||||
|
||||
renderTabCredentialsToken(user: User): TemplateResult {
|
||||
return html`
|
||||
<ak-tabs pageIdentifier="userCredentialsTokens" vertical>
|
||||
@@ -529,6 +493,7 @@ export class UserViewPage extends WithCapabilitiesConfig(WithSession(AKElement))
|
||||
${this.renderTabApplications(this.user)}
|
||||
</div>
|
||||
<ak-rbac-object-permission-page
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-permissions"
|
||||
|
||||
@@ -131,12 +131,15 @@ export class DevRepeatedRequestsMiddleware implements Middleware, Disposable {
|
||||
this.#requests.push(reqSig);
|
||||
|
||||
if (count > 2) {
|
||||
showMessage({
|
||||
level: MessageLevel.warning,
|
||||
message: "[Dev] Consecutive requests detected",
|
||||
description: html`${count} identical requests to
|
||||
<pre>${reqSig}</pre>`,
|
||||
});
|
||||
showMessage(
|
||||
{
|
||||
level: MessageLevel.warning,
|
||||
message: "[Dev] Consecutive requests detected",
|
||||
description: html`${count} identical requests to
|
||||
<pre>${reqSig}</pre>`,
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
this.#logger.trace("Repeated request", reqSig);
|
||||
}
|
||||
|
||||
94
web/src/common/clipboard.ts
Normal file
94
web/src/common/clipboard.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { APIMessage, MessageLevel } from "#common/messages";
|
||||
|
||||
import { showMessage } from "#elements/messages/MessageContainer";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
|
||||
function castToClipboardItem(input: string, mimeType = "text/plain"): ClipboardItem {
|
||||
return new ClipboardItem({
|
||||
[mimeType]: new Blob([input], {
|
||||
type: mimeType,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function doWriteToClipboard(...data: string[] | ClipboardItem[]): Promise<void> {
|
||||
if (data.every((item) => typeof item === "string")) {
|
||||
return navigator.clipboard.write(data.map((item) => castToClipboardItem(item)));
|
||||
}
|
||||
|
||||
return navigator.clipboard.write(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies data to the clipboard.
|
||||
*
|
||||
* @param data The data to copy. Either a plain-text `string` or a {@linkcode ClipboardItem}.
|
||||
* @param entityLabel Localized label for the copied entity, used in success message.
|
||||
* @param description Optional description for the success message.
|
||||
*
|
||||
* @return A promise resolving to `true` on success, `false` on failure.
|
||||
*/
|
||||
export function writeToClipboard(
|
||||
data: string | ClipboardItem | string[] | ClipboardItem[] | null | undefined,
|
||||
entityLabel?: string,
|
||||
description?: string,
|
||||
): Promise<boolean> {
|
||||
if (!data || (Array.isArray(data) && data.length === 0)) {
|
||||
console.warn("Cannot write empty data to clipboard");
|
||||
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
const normalized = typeof data === "string" ? castToClipboardItem(data) : data;
|
||||
|
||||
return doWriteToClipboard(...(Array.isArray(normalized) ? normalized : [normalized]))
|
||||
.then(() => {
|
||||
const message: APIMessage = {
|
||||
level: MessageLevel.info,
|
||||
icon: "fas fa-clipboard-check",
|
||||
message: entityLabel
|
||||
? msg(str`${entityLabel} copied to clipboard.`, {
|
||||
id: "clipboard.write.success.message.entity",
|
||||
})
|
||||
: msg("Copied to clipboard.", {
|
||||
id: "clipboard.write.success.generic",
|
||||
}),
|
||||
description,
|
||||
};
|
||||
|
||||
showMessage(message, true);
|
||||
|
||||
return true;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to write to clipboard:", error);
|
||||
const fallbackDescription = msg(
|
||||
"Clipboard not available. Please copy the value manually.",
|
||||
{
|
||||
id: "clipboard.write.failure.description",
|
||||
},
|
||||
);
|
||||
|
||||
if (typeof data === "string") {
|
||||
showMessage(
|
||||
{
|
||||
level: MessageLevel.warning,
|
||||
message: data,
|
||||
description: fallbackDescription,
|
||||
},
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
showMessage(
|
||||
{
|
||||
level: MessageLevel.warning,
|
||||
message: fallbackDescription,
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
@@ -22,6 +22,13 @@ export class AKEnterpriseRefreshEvent extends Event {
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface WindowEventMap {
|
||||
[AKRefreshEvent.eventName]: AKRefreshEvent;
|
||||
[AKEnterpriseRefreshEvent.eventName]: AKEnterpriseRefreshEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export interface EventUser {
|
||||
pk: number;
|
||||
email?: string;
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface APIMessage {
|
||||
level: MessageLevel;
|
||||
message: string;
|
||||
description?: string | TemplateResult;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export class AKMessageEvent extends Event {
|
||||
|
||||
@@ -319,10 +319,9 @@ export class ListSelect extends AKElement implements IListSelect {
|
||||
tabindex="1"
|
||||
part="ak-list-select-wrapper"
|
||||
>
|
||||
<ul
|
||||
<menu
|
||||
class="pf-c-dropdown__menu pf-m-static"
|
||||
id="ak-list-select-list"
|
||||
role="listbox"
|
||||
tabindex="0"
|
||||
part="ak-list-select"
|
||||
>
|
||||
@@ -330,7 +329,7 @@ export class ListSelect extends AKElement implements IListSelect {
|
||||
${this.#options.grouped
|
||||
? this.renderMenuGroups(this.#options.options)
|
||||
: this.renderMenuItems(this.#options.options)}
|
||||
</ul>
|
||||
</menu>
|
||||
</div> `;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { parseAPIResponseError, pluckErrorDetail } from "#common/errors/network";
|
||||
import { MessageLevel } from "#common/messages";
|
||||
|
||||
import { BaseTaskButton } from "#elements/buttons/SpinnerButton/BaseTaskButton";
|
||||
@@ -20,7 +21,7 @@ import { customElement, property } from "lit/decorators.js";
|
||||
*/
|
||||
|
||||
@customElement("ak-action-button")
|
||||
export class ActionButton extends BaseTaskButton {
|
||||
export class ActionButton<R = unknown> extends BaseTaskButton<R> {
|
||||
/**
|
||||
* The command to run when the button is pressed. Must return a promise. If the promise is a
|
||||
* reject or throw, we process the content of the promise and deliver it to the Notification
|
||||
@@ -28,27 +29,22 @@ export class ActionButton extends BaseTaskButton {
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
|
||||
@property({ attribute: false })
|
||||
apiRequest: () => Promise<unknown> = () => {
|
||||
throw new Error();
|
||||
public apiRequest: () => Promise<R> = () => {
|
||||
throw new TypeError("No API request defined for ActionButton");
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.onError = this.onError.bind(this);
|
||||
public override callAction(): Promise<R> {
|
||||
return this.apiRequest();
|
||||
}
|
||||
|
||||
callAction = (): Promise<unknown> => {
|
||||
return this.apiRequest();
|
||||
};
|
||||
|
||||
async onError(error: Error | Response) {
|
||||
protected async onError(error: unknown) {
|
||||
super.onError(error);
|
||||
const message = error instanceof Error ? error.toString() : await error.text();
|
||||
const parsedError = await parseAPIResponseError(error);
|
||||
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: message,
|
||||
message: pluckErrorDetail(parsedError),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +1,116 @@
|
||||
import { EVENT_REFRESH } from "#common/constants";
|
||||
import { AKRefreshEvent } from "#common/events";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { listen } from "#elements/decorators/listen";
|
||||
|
||||
import { ConsoleLogger } from "#logger/browser";
|
||||
|
||||
import { html, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-dropdown")
|
||||
export class DropdownButton extends AKElement {
|
||||
menu: HTMLElement | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
window.addEventListener(EVENT_REFRESH, this.show);
|
||||
}
|
||||
|
||||
public show = (): void => {
|
||||
if (!this.menu) return;
|
||||
|
||||
this.menu.hidden = true;
|
||||
public static override shadowRootOptions: ShadowRootInit = {
|
||||
...AKElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
protected createRenderRoot(): HTMLElement | DocumentFragment {
|
||||
return this;
|
||||
}
|
||||
|
||||
const menu = this.querySelector<HTMLElement>(".pf-c-dropdown__menu");
|
||||
#menu: HTMLMenuElement | null = null;
|
||||
#toggleButton: HTMLButtonElement | null = null;
|
||||
#abortController: AbortController | null = null;
|
||||
|
||||
if (!menu) {
|
||||
console.warn("authentik/dropdown: No menu found");
|
||||
protected logger = ConsoleLogger.prefix("dropdown");
|
||||
|
||||
@listen(AKRefreshEvent)
|
||||
public hide = (): void => {
|
||||
if (!this.#menu || !this.#toggleButton) return;
|
||||
|
||||
this.#menu.hidden = true;
|
||||
this.#toggleButton.ariaExpanded = "false";
|
||||
};
|
||||
|
||||
public toggleMenu = (event: MouseEvent): void => {
|
||||
if (!this.#menu) return;
|
||||
|
||||
const button = event.currentTarget as HTMLButtonElement;
|
||||
|
||||
this.#menu.hidden = !this.#menu.hidden;
|
||||
button.ariaExpanded = this.#menu.hidden.toString();
|
||||
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
@listen("click", {
|
||||
passive: true,
|
||||
})
|
||||
protected clickHandler = (event: Event): void => {
|
||||
if (!this.#menu) return;
|
||||
|
||||
if (this.#menu.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.menu = menu;
|
||||
if (event.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.querySelectorAll("button.pf-c-dropdown__toggle").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
if (!this.menu) return;
|
||||
const target = event.target as HTMLElement;
|
||||
if (this.#menu.contains(target)) {
|
||||
return;
|
||||
}
|
||||
const toggle = this.querySelector<HTMLElement>("button.pf-c-dropdown__toggle");
|
||||
if (toggle && toggle.contains(target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.menu.hidden = !this.menu.hidden;
|
||||
btn.ariaExpanded = (!this.menu.hidden).toString();
|
||||
});
|
||||
this.hide();
|
||||
};
|
||||
|
||||
public override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.#abortController = new AbortController();
|
||||
|
||||
this.#menu = this.querySelector<HTMLMenuElement>("menu.pf-c-dropdown__menu");
|
||||
|
||||
if (!this.#menu) {
|
||||
this.logger.warn("No menu found");
|
||||
return;
|
||||
}
|
||||
|
||||
this.#toggleButton = this.querySelector<HTMLButtonElement>("button.pf-c-dropdown__toggle");
|
||||
|
||||
if (!this.#toggleButton) {
|
||||
this.logger.warn("No toggle button found");
|
||||
return;
|
||||
}
|
||||
|
||||
this.#menu.hidden = true;
|
||||
this.#toggleButton.ariaExpanded = "false";
|
||||
|
||||
this.#toggleButton.addEventListener("click", this.toggleMenu, {
|
||||
capture: true,
|
||||
signal: this.#abortController.signal,
|
||||
});
|
||||
|
||||
// TODO: Enable this after native <dialog> modals are used.
|
||||
// If enabled now, this would close the modal since it's technically within the dropdown.
|
||||
// const menuItemButtons = this.querySelectorAll<HTMLElement>(".pf-c-dropdown__menu-item");
|
||||
|
||||
// for (const menuItemButton of menuItemButtons) {
|
||||
// menuItemButton.addEventListener("click", this.hide, {
|
||||
// capture: true,
|
||||
// signal: this.#abortController.signal,
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
public override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener(EVENT_REFRESH, this.show);
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<slot></slot>`;
|
||||
this.#abortController?.abort();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,14 +59,12 @@ const SPINNER_TIMEOUT = 1000 * 1.5; // milliseconds
|
||||
*
|
||||
*/
|
||||
|
||||
export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) {
|
||||
eventPrefix = "ak-button";
|
||||
export abstract class BaseTaskButton<R = unknown> extends CustomEmitterElement(AKElement) {
|
||||
public eventPrefix = "ak-button";
|
||||
|
||||
static styles = [...buttonStyles];
|
||||
public static styles = [...buttonStyles];
|
||||
|
||||
callAction!: () => Promise<unknown>;
|
||||
|
||||
actionTask: Task;
|
||||
public callAction?(): Promise<R>;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public disabled = false;
|
||||
@@ -74,25 +72,24 @@ export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) {
|
||||
@property({ type: String })
|
||||
public label: string | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.onSuccess = this.onSuccess.bind(this);
|
||||
this.onError = this.onError.bind(this);
|
||||
this.onClick = this.onClick.bind(this);
|
||||
this.actionTask = this.buildTask();
|
||||
}
|
||||
|
||||
buildTask() {
|
||||
return new Task(this, {
|
||||
task: () => this.callAction(),
|
||||
protected buildTask() {
|
||||
return new Task<[], R>(this, {
|
||||
task: () => {
|
||||
if (typeof this.callAction !== "function") {
|
||||
throw new TypeError("No action defined for SpinnerButton");
|
||||
}
|
||||
return this.callAction();
|
||||
},
|
||||
args: () => [],
|
||||
autoRun: false,
|
||||
onComplete: (r: unknown) => this.onSuccess(r),
|
||||
onError: (r: unknown) => this.onError(r),
|
||||
onComplete: (r: R) => this.onSuccess(r),
|
||||
onError: (error) => this.onError(error),
|
||||
});
|
||||
}
|
||||
|
||||
onComplete() {
|
||||
protected actionTask: Task = this.buildTask();
|
||||
|
||||
protected onComplete() {
|
||||
setTimeout(() => {
|
||||
this.dispatchCustomEvent(`${this.eventPrefix}-reset`);
|
||||
// set-up for the next task...
|
||||
@@ -101,34 +98,35 @@ export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) {
|
||||
}, SPINNER_TIMEOUT);
|
||||
}
|
||||
|
||||
onSuccess(r: unknown) {
|
||||
protected onSuccess(result: R): void {
|
||||
this.dispatchCustomEvent(`${this.eventPrefix}-success`, {
|
||||
result: r,
|
||||
result,
|
||||
});
|
||||
this.onComplete();
|
||||
}
|
||||
|
||||
onError(error: unknown) {
|
||||
protected onError(error: unknown) {
|
||||
this.dispatchCustomEvent(`${this.eventPrefix}-failure`, {
|
||||
error,
|
||||
});
|
||||
this.onComplete();
|
||||
}
|
||||
|
||||
onClick() {
|
||||
protected onClick() {
|
||||
// Don't accept clicks when a task is in progress..
|
||||
if (this.actionTask.status === TaskStatus.PENDING) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispatchCustomEvent(`${this.eventPrefix}-click`);
|
||||
this.actionTask.run();
|
||||
}
|
||||
|
||||
private spinner = html`<span class="pf-c-button__progress">
|
||||
#spinner = html`<span class="pf-c-button__progress">
|
||||
<ak-spinner size=${PFSize.Medium}></ak-spinner>
|
||||
</span>`;
|
||||
|
||||
get buttonClasses() {
|
||||
public get buttonClasses() {
|
||||
return [
|
||||
...this.classList,
|
||||
StatusMap[this.actionTask.status],
|
||||
@@ -138,7 +136,7 @@ export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) {
|
||||
.trim();
|
||||
}
|
||||
|
||||
render() {
|
||||
protected override render() {
|
||||
return html`<button
|
||||
id="spinner-button"
|
||||
part="spinner-button"
|
||||
@@ -150,7 +148,7 @@ export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) {
|
||||
?disabled=${this.disabled}
|
||||
>
|
||||
${this.actionTask.render({
|
||||
pending: () => this.spinner,
|
||||
pending: () => this.#spinner,
|
||||
})}
|
||||
<slot></slot>
|
||||
</button>`;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user